From 534a4300e5a51db474be3c34636aca83b5d16235 Mon Sep 17 00:00:00 2001 From: Florian Bezannier Date: Tue, 22 Dec 2020 09:27:39 +0100 Subject: [PATCH] add tabs to dashboard, fix get_trips --- MyPSACC.py | 19 ++++++++------ Trip.py | 53 +++++++++++++++++++++++++++++++++++++++ Trips.py | 25 ------------------- requirements.txt | 28 ++++++++++----------- web/app.py | 65 ++++++++++++++++++++++++++---------------------- web/figures.py | 35 +++++++++++++++++--------- 6 files changed, 136 insertions(+), 89 deletions(-) create mode 100644 Trip.py delete mode 100644 Trips.py diff --git a/MyPSACC.py b/MyPSACC.py index 180f74f..0673f9f 100644 --- a/MyPSACC.py +++ b/MyPSACC.py @@ -10,20 +10,20 @@ from json import JSONEncoder from hashlib import md5 from time import sleep +import pytz from oauth2_client.credentials_manager import CredentialManager, ServiceInformation import paho.mqtt.client as mqtt from requests import Response from typing import List import psa_connectedcar as psac -from Trips import Trips +from Trip import Trip from psa_connectedcar import ApiClient from psa_connectedcar.rest import ApiException from MyLogger import logger from threading import Semaphore, Timer from functools import wraps - -from web.figures import convert_datetime +import sqlite3 oauhth_url = {"clientsB2CPeugeot":"https://idpcvs.peugeot.com/am/oauth2/access_token", "clientsB2CCitroen":"https://idpcvs.citroen.com/am/oauth2/access_token", @@ -406,7 +406,6 @@ class MyPSACC: self._record_enabled = value def record_position(self,vin, res:psac.models.status.Status): - import sqlite3 conn = sqlite3.connect('info.db') conn.execute( "CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY, VIN TEXT, longitude REAL, latitude REAL, mileage REAL, level INTEGER);") @@ -442,7 +441,7 @@ class MyPSACC: return geo_dumps(feature_collection, sort_keys=True) @staticmethod - def get_trips() -> List[Trips]: + def get_trips() -> List[Trip]: sqlite3.register_converter("DATETIME", convert_datetime) conn = sqlite3.connect('info.db', detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) conn.row_factory = sqlite3.Row @@ -450,11 +449,13 @@ class MyPSACC: start = res[0] end = res[1] trips = [] - tr = Trips() + tr = Trip() battery_power = 46 for next_el in res[2:]: distance = next_el["mileage"] - end["mileage"] # km - if distance == 0: + duration = (next_el["Timestamp"] - end["Timestamp"]).total_seconds()/3600 + speed = distance/duration + if distance == 0 or speed < 1: # check the speed to handle missing point tr.distance = end["mileage"] - start["mileage"] # km if tr.distance > 0: tr.start_at = start["Timestamp"] @@ -469,7 +470,7 @@ class MyPSACC: f"{start['Timestamp']} {tr.distance:.1f}km {tr.duration:.2f}h {tr.speed_average:.2f} km/h {tr.consumption:.2f} kw {tr.consumption_km:.2f}kw/100km") trips.append(tr) start = next_el - tr = Trips() + tr = Trip() else: tr.add_points(end["longitude"], end["latitude"]) end = next_el @@ -488,3 +489,5 @@ class MyPeugeotEncoder(JSONEncoder): return mpd +def convert_datetime(st): + return datetime.strptime(st.decode("utf-8"), "%Y-%m-%d %H:%M:%S+00:00").replace(tzinfo=pytz.UTC) \ No newline at end of file diff --git a/Trip.py b/Trip.py new file mode 100644 index 0000000..eb1dde1 --- /dev/null +++ b/Trip.py @@ -0,0 +1,53 @@ +from typing import List +from geojson import Feature, Point, FeatureCollection, MultiLineString +from geojson import dumps as geo_dumps + + +class Points(): + def __init__(self, latitude, longitude): + self.latitude = latitude + self.longitude = longitude + + def list(self): + return self.latitude, self.longitude + + +class Trips(list): + def __init__(self, *args): + list.__init__(self, *args) + + def to_geo_json(self): + feature_collection = FeatureCollection(self) + return feature_collection + + +class Trip: + def __init__(self): + self.start_at = None + self.end_at = None + self.positions: List[Points] = [] + self.speed_average = None + self.consumption = None + self.consumption_km = None + self.distance = None + self.duration = None + + def add_points(self, longitude, latitude): + self.positions.append(Points(longitude, latitude)) + + def get_consumption(self): + return { + 'date': self.start_at, + 'consumption': self.consumption_km, + } + + def to_geojson(self): + multi_line_string = MultiLineString(tuple(map(list, self.positions))) + return Feature(geometry=multi_line_string, properties={"start_at": self.start_at, "end_at": self.end_at, + "average speed": self.speed_average, + "average consumption": self.consumption_km}) + + def get_info(self): + res = {"start_at": self.start_at.strftime("%x %X"), "end_at": self.end_at.strftime("%x %X"), "duration": self.duration*60, + "speed_average": self.speed_average, "consumption_km": self.consumption_km, "distance": self.distance} + return res diff --git a/Trips.py b/Trips.py deleted file mode 100644 index 0672e00..0000000 --- a/Trips.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List -class Points(): - def __init__(self, latitude, longitude): - self.latitude = latitude - self.longitude = longitude - -class Trips: - def __init__(self): - self.start_at = None - self.end_at = None - self.positions: List[Points] = [] - self.speed_average = None - self.consumption = None - self.consumption_km = None - - def add_points(self, longitude, latitude): - self.positions.append(Points(longitude, latitude)) - - def get_consumption(self): - return { - 'date': self.start_at, - 'consumption': self.consumption_km, - } - - diff --git a/requirements.txt b/requirements.txt index e0cab82..7d0b9f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ +paho-mqtt>=1.5.0 +dash>=1.18.0 +plotly>=4 +pandas oauth2_client python-dateutil -Flask>=1.1.1 -certifi>=2018.4.16 -urllib3>=1.25.10 -six>=1.15.0 -requests>=2.24.0 -paho-mqtt>=1.5.0 -dash>=1.18.1 -numpy>=1.19.4 -pytz>=2019.3 -plotly>=4.5.0 -typing>=3.6.6 -pandas>=1.1.3 -argparse>=1.4.0 -androguard>=3.3.5 \ No newline at end of file +certifi +urllib3 +six +requests +numpy +pytz +typing +argparse +flask +dash_bootstrap_components \ No newline at end of file diff --git a/web/app.py b/web/app.py index 2c2b5c2..607feae 100644 --- a/web/app.py +++ b/web/app.py @@ -6,15 +6,21 @@ import dash_bootstrap_components as dbc from dash.dependencies import Output, Input import dash_core_components as dcc import dash_html_components as html - +import locale from MyLogger import logger from flask import jsonify, request, Response as FlaskResponse from web import figures from MyPSACC import MyPSACC +try: + locale = locale.getlocale()[0].split("_")[0] + locale_url = [f"https://cdn.plot.ly/plotly-locale-{locale}-latest.js"] + dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title="My car info") +except: + logger.warn("Can't get language") + dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP]) -dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP]) app = dash_app.server myp = None chc = None @@ -116,35 +122,34 @@ def save_config(my_peugeot: MyPSACC): trips = MyPSACC.get_trips() figures.get_figures(trips) -dash_app.layout = dbc.Container([ +dash_app.layout = dbc.Container(fluid=True, children=[ html.H1('My car info'), - html.Hr(), - html.Div([ - dcc.RangeSlider( - id='date-slider', - min=figures.unix_time_millis(figures.consumption_df["date"].min()), - max=figures.unix_time_millis(figures.consumption_df["date"].max()), - step=None, - marks=figures.get_marks_from_start_end(figures.consumption_df["date"].min(), - figures.consumption_df["date"].max()), - value=[figures.unix_time_millis(figures.consumption_df["date"].min()), - figures.unix_time_millis(figures.consumption_df["date"].max())], - ), - html.Div(id='updatemode-output-container', style={'margin-top': 20}) - ], - style={'margin-left': 20} + dcc.RangeSlider( + id='date-slider', + min=figures.unix_time_millis(figures.consumption_df["date"].min()), + max=figures.unix_time_millis(figures.consumption_df["date"].max()), + step=None, + marks=figures.get_marks_from_start_end(figures.consumption_df["date"].min(), + figures.consumption_df["date"].max()), + value=[figures.unix_time_millis(figures.consumption_df["date"].min()), + figures.unix_time_millis(figures.consumption_df["date"].max())], ), html.Div([ - html.Div([ - dcc.Graph(figure=figures.trips_map, id="trips_map") - ]), - html.Div([ - dcc.Graph(figure=figures.consumption_fig, id="consumption_fig") - ]), - html.H2(id="consumption", - children="Average consumption: {:.1f} kW/100km".format(float(figures.consumption_df.mean()))), - html.Div([ - dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed") - ]), - ], id="data-div"), + dbc.Tabs([ + dbc.Tab(label="Summary", tab_id="summary", children=[ + html.H2(id="consumption", + children="Average consumption: {:.1f} kW/100km".format( + float(figures.consumption_df.mean()))), + dcc.Graph(figure=figures.consumption_fig, id="consumption_fig"), + dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed") + ]), + dbc.Tab(label="Trips", tab_id="trips", children=[figures.table_fig]), + dbc.Tab(label="Map", tab_id="map", children=[ + dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]), + ], + id="tabs", + active_tab="summary", + ), + html.Div(id="tab-content", className="p-4"), + ]), ]) diff --git a/web/figures.py b/web/figures.py index ae4045e..33d6c7a 100644 --- a/web/figures.py +++ b/web/figures.py @@ -1,12 +1,15 @@ -from datetime import datetime +from copy import deepcopy from typing import List + +import dash_table import numpy as np -import pytz +from dash_table.Format import Format, Scheme, Symbol from dateutil.relativedelta import relativedelta from pandas import DataFrame import plotly.express as px import plotly.graph_objects as go -from Trips import Trips +from Trip import Trip +from pandas import options as pandas_options def unix_time_millis(dt): @@ -36,18 +39,16 @@ def get_marks_from_start_end(start, end): return marks -def convert_datetime(st): - return datetime.strptime(st.decode("utf-8"), "%Y-%m-%d %H:%M:%S+00:00").replace(tzinfo=pytz.UTC) - - consumption_fig = None consumption_df = None trips_map = None consumption_fig_by_speed = None +table_fig = None +pandas_options.display.float_format = '${:.2f}'.format -def get_figures(trips: List[Trips]): - global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed +def get_figures(trips: List[Trip]): + global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig lats = [] lons = [] names = [] @@ -60,6 +61,19 @@ def get_figures(trips: List[Trips]): lons = np.append(lons, None) names = np.append(names, None) + # table + nb_format = Format(precision=2, scheme=Scheme.fixed, symbol=Symbol.yes) + table_fig = dash_table.DataTable( + id='trips-table', + sort_action='native', + columns=[{'id': 'start_at', 'name': 'start at', 'type': 'datetime'}, + {'id': 'duration', 'name': 'duration', 'type': 'numeric', 'format': deepcopy(nb_format).symbol_suffix(" min").precision(0)}, + {'id': 'speed_average', 'name': 'average speed', 'type': 'numeric', 'format': deepcopy(nb_format).symbol_suffix(" km/h")}, + {'id': 'consumption_km', 'name': 'average consumption', 'type': 'numeric', 'format': deepcopy(nb_format).symbol_suffix(" kw/100km")}, + {'id': 'distance', 'name': 'distance', 'type': 'numeric', 'format': nb_format.symbol_suffix(" km")}], + data=[tr.get_info() for tr in trips], + ) + # map trips_map = px.line_mapbox(lat=lats, lon=lons, hover_name=names, mapbox_style="stamen-terrain", zoom=12) consumption_df = DataFrame.from_records([tr.get_consumption() for tr in trips]) @@ -75,6 +89,3 @@ def get_figures(trips: List[Trips]): go.Scatter(mode="markers", x=consum_df_by_speed["speed"], y=consum_df_by_speed["value"], name="Trips")) consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kW/100Km") - - -