diff --git a/Trip.py b/Trip.py index 4bfcf26..0b78a30 100644 --- a/Trip.py +++ b/Trip.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import List from geojson import Feature, FeatureCollection, MultiLineString @@ -68,18 +70,27 @@ class Trips(list): feature_collection = FeatureCollection(self) return feature_collection + def get_long_trips(self): + res = [] + for tr in self: + if tr.consumption > 1.8: + res.append({"speed": tr.speed_average, "consumption_km": tr.consumption_km, "date": tr.start_at, + "consumption": tr.consumption}) + return res + @staticmethod - def get_trips(vehicles_list: Cars) -> List[Trip]: + def get_trips(vehicles_list: Cars) -> dict[str, Trips]: conn = get_db() vehicles = conn.execute( "SELECT DISTINCT vin FROM position;").fetchall() + trips_by_vin = {} for vin in vehicles: + trips = Trips() vin = vin[0] car = vehicles_list.get_car_by_vin(vin) battery_capacity = car.battery_power fuel_capacity = car.fuel_capacity - res = conn.execute('SELECT * FROM position ORDER BY Timestamp').fetchall() - trips = [] + res = conn.execute('SELECT * FROM position WHERE VIN=? ORDER BY Timestamp', (vin,)).fetchall() if len(res) > 1: start = res[0] end = res[1] @@ -104,7 +115,7 @@ class Trips(list): if refuel > 0: restart_trip = True logger.debugv("refuel detected") - elif distance == 0 and charge > 2: + elif charge > 0: restart_trip = True logger.debugv("charge detected") elif speed_average < 0.2 and duration > 0.05: # think again if duration is really needed @@ -133,7 +144,7 @@ class Trips(list): if refuel > 0: end_trip = True logger.debugv("refuel detected") - elif distance == 0 and charge > 2: + elif charge > 0: end_trip = True logger.debugv("charge detected") elif speed_average < 0.2 and duration > 0.05: @@ -165,12 +176,14 @@ class Trips(list): if start["level_fuel"] is not None and end["level_fuel"] is not None: diff_level_fuel = start["level_fuel"] - end["level_fuel"] tr.consumption_fuel = round(diff_level_fuel / 100 * fuel_capacity, 2) # L - tr.consumption_fuel_km = round(100 * tr.consumption_fuel / tr.distance, 2) # L/100 km + tr.consumption_fuel_km = round(100 * tr.consumption_fuel / tr.distance, + 2) # L/100 km else: tr.consumption_fuel = 0 tr.consumption_fuel_km = 0 tr.mileage = end["mileage"] - logger.debugv("Trip: %s -> %s %.1fkm %.2fh %.0fkm/h %.2fkWh %.2fkWh/100km %.2fL %.2fL/100km %.1fkm", + logger.debugv("Trip: %s -> %s %.1fkm %.2fh %.0fkm/h %.2fkWh %.2fkWh/100km %.2fL " + "%.2fL/100km %.1fkm", tr.start_at, tr.end_at, tr.distance, tr.duration, tr.speed_average, tr.consumption, tr.consumption_km, tr.consumption_fuel, tr.consumption_fuel_km, tr.mileage) @@ -185,4 +198,5 @@ class Trips(list): else: tr.add_points(end["longitude"], end["latitude"]) end = next_el - return trips + trips_by_vin[vin] = trips + return trips_by_vin diff --git a/web/app.py b/web/app.py index 4c4a663..1d3bdcf 100644 --- a/web/app.py +++ b/web/app.py @@ -25,7 +25,7 @@ def start_app(title, base_path, debug: bool, host, port): try: lang = locale.getlocale()[0].split("_")[0] locale_url = [f"https://cdn.plot.ly/plotly-locale-{lang}-latest.js"] - except: + except IndexError: locale_url = None logger.warning("Can't get language") app = Flask(__name__) @@ -39,7 +39,7 @@ def start_app(title, base_path, debug: bool, host, port): dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title=title, server=app, requests_pathname_prefix=requests_pathname_prefix) # keep this line - import web.callback + import web.views return run_simple(host, port, application, use_reloader=False, use_debugger=debug) diff --git a/web/figures.py b/web/figures.py index c005958..14ed4e4 100644 --- a/web/figures.py +++ b/web/figures.py @@ -9,10 +9,11 @@ from dateutil.relativedelta import relativedelta from pandas import DataFrame import plotly.express as px import plotly.graph_objects as go -from Trip import Trip +from Trip import Trips from pandas import options as pandas_options import dash_html_components as html + def unix_time_millis(dt): return int(dt.timestamp()) @@ -51,7 +52,7 @@ info = "" battery_info = dbc.Alert("No data to show", color="danger") -def get_figures(trips: List[Trip], charging: List[dict]): +def get_figures(trips: Trips, charging: List[dict]): global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info, battery_info lats = [] lons = [] @@ -81,26 +82,26 @@ def get_figures(trips: List[Trip], charging: List[dict]): 'format': deepcopy(nb_format).symbol_suffix(" kWh/100km")}, {'id': 'consumption_fuel_km', 'name': 'average consumption fuel', 'type': 'numeric', 'format': deepcopy(nb_format).symbol_suffix(" L/100km")}, - {'id': 'distance', 'name': 'distance', 'type': 'numeric', 'format': nb_format.symbol_suffix(" km").precision(1)}, - {'id': 'mileage', 'name': 'mileage', 'type': 'numeric', 'format': nb_format.symbol_suffix(" km").precision(1)}], + {'id': 'distance', 'name': 'distance', 'type': 'numeric', + 'format': nb_format.symbol_suffix(" km").precision(1)}, + {'id': 'mileage', 'name': 'mileage', 'type': 'numeric', + 'format': nb_format.symbol_suffix(" km").precision(1)}], data=[tr.get_info() for tr in trips], ) # consumption_fig - consumption_df = DataFrame.from_records([tr.get_consumption() for tr in trips]) + consumption_df = DataFrame.from_records(trips.get_long_trips()) consumption_fig = px.line(consumption_df, x="date", y="consumption", title='Consumption of the car') consumption_fig.update_layout(yaxis_title="Consumption kWh/100Km") - consum_df_by_speed = DataFrame.from_records( - [{"speed": tr.speed_average, "consumption": tr.consumption_km} for tr in trips]) - consumption_fig_by_speed = px.histogram(consum_df_by_speed, x="speed", y="consumption", histfunc="avg", + consumption_fig_by_speed = px.histogram(consumption_df, x="speed", y="consumption_km", histfunc="avg", title="Consumption by speed") consumption_fig_by_speed.update_traces(xbins_size=15) consumption_fig_by_speed.update_layout(bargap=0.05) consumption_fig_by_speed.add_trace( - go.Scatter(mode="markers", x=consum_df_by_speed["speed"], y=consum_df_by_speed["consumption"], + go.Scatter(mode="markers", x=consumption_df["speed"], y=consumption_df["consumption_km"], name="Trips")) consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km") - kw_per_km = float(consumption_df.mean(numeric_only=True)) + kw_per_km = float(consumption_df["consumption_km"].mean()) info = "Average consumption: {:.1f} kWh/100km".format(kw_per_km) # charging @@ -119,7 +120,32 @@ def get_figures(trips: List[Trip], charging: List[dict]): charge_speed = 0 except KeyError: # when there is no data yet: charge_speed = 0 - battery_info = html.Div(children=[ - html.P("Average C02 emission: {:.1f} g/kWh".format(co2_per_kw)), - html.P("Average CO2 emission: {:.1f} g/km".format(co2_per_km)), - html.P("Average charge speed: {:.3f} kW".format(charge_speed))]) + + battery_info = dash_table.DataTable( + id='battery_info', + sort_action='native', + columns=[{'id': 'name', 'name': ''}, + {'id': 'value', 'name': ''}], + style_header={'display': 'none'}, + style_data={'border': '0px'}, + data=[{"name": "Average emission:", "value": "{:.1f} g/km".format(co2_per_km)}, + {"name": " ", "value:": "{:.1f} g/kWh".format(co2_per_kw)}, + {"name": "Average charge speed:", "value": "{:.3f} kW".format(charge_speed)}]) + battery_info = html.Div(children=[html.Tr( + [ + html.Td('Average emission:', rowSpan=2), + html.Td("{:.1f} g/km".format(co2_per_km)), + ] + ), + html. Tr( + [ + "{:.1f} g/kWh".format(co2_per_kw), + ] + ), + html.Tr( + [ + html.Td("Average charge speed:"), + html.Td("{:.3f} kW".format(charge_speed)) + ] + ) + ]) diff --git a/web/callback.py b/web/views.py similarity index 95% rename from web/callback.py rename to web/views.py index 0464ff3..395049d 100644 --- a/web/callback.py +++ b/web/views.py @@ -34,7 +34,7 @@ min_date = max_date = min_millis = max_millis = step = marks = None def display_value(value): mini = datetime.fromtimestamp(value[0], tz=timezone.utc) maxi = datetime.fromtimestamp(value[1], tz=timezone.utc) - filtered_trips = [] + filtered_trips = Trips() for trip in trips: if mini <= trip.start_at <= maxi: filtered_trips.append(trip) @@ -91,8 +91,8 @@ def get_position(vin): "url": f"http://maps.google.com/maps?q={latitude},{longitude}"}) longitude, latitude = coordinates return jsonify( - {"longitude": longitude, "latitude": latitude, - "url": f"http://maps.google.com/maps?q={latitude},{longitude}"}) + {"longitude": longitude, "latitude": latitude, + "url": f"http://maps.google.com/maps?q={latitude},{longitude}"}) # Set a battery threshold and schedule an hour to stop the charge @@ -127,7 +127,8 @@ def update_trips(): global trips, chargings logger.info("update_data") try: - trips = Trips.get_trips(myp.vehicles_list) + trips_by_vin = Trips.get_trips(myp.vehicles_list) + trips = next(iter(trips_by_vin.values())) # todo handle multiple car chargings = MyPSACC.get_chargings() except: logger.error("update_trips: %s", traceback.format_exc()) @@ -175,7 +176,7 @@ try: html.Div(id="tab-content", className="p-4"), ])]) except (IndexError, TypeError): - logger.debug("Failed to generate figure, there is probably not enough data yet") + logger.debug("Failed to generate figure, there is probably not enough data yet %s", traceback.format_exc()) data_div = dbc.Alert("No data to show, there is probably no trips recorded yet", color="danger") except: