update dashboard

This commit is contained in:
Florian Bezannier
2020-12-22 14:41:39 +01:00
parent 534a4300e5
commit f301a916eb
6 changed files with 119 additions and 91 deletions
+40 -36
View File
@@ -1,6 +1,5 @@
import json
import re
import sqlite3
import traceback
import uuid
from copy import copy
@@ -25,11 +24,11 @@ from threading import Semaphore, Timer
from functools import wraps
import sqlite3
oauhth_url = {"clientsB2CPeugeot":"https://idpcvs.peugeot.com/am/oauth2/access_token",
"clientsB2CCitroen":"https://idpcvs.citroen.com/am/oauth2/access_token",
oauhth_url = {"clientsB2CPeugeot": "https://idpcvs.peugeot.com/am/oauth2/access_token",
"clientsB2CCitroen": "https://idpcvs.citroen.com/am/oauth2/access_token",
"clientsB2CDS": "https://idpcvs.driveds.com/am/oauth2/access_token",
"clientsB2COpel":"https://idpcvs.opel.com/am/oauth2/access_token",
"clientsB2CVauxhall":"https://idpcvs.vauxhall.co.uk/am/oauth2/access_token"}
"clientsB2COpel": "https://idpcvs.opel.com/am/oauth2/access_token",
"clientsB2CVauxhall": "https://idpcvs.vauxhall.co.uk/am/oauth2/access_token"}
authorize_service = "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize"
remote_url = "https://api.groupe-psa.com/connectedcar/v4/virtualkey/remoteaccess/token?client_id="
@@ -40,6 +39,7 @@ MQTT_RESP_TOPIC = "psa/RemoteServices/to/cid/"
MQTT_EVENT_TOPIC = "psa/RemoteServices/events/MPHRTServices/"
MQTT_TOKEN_TTL = 890
def rate_limit(limit, every):
def limit_decorator(fn):
semaphore = Semaphore(limit)
@@ -58,6 +58,7 @@ def rate_limit(limit, every):
return limit_decorator
class OpenIdCredentialManager(CredentialManager):
def _grant_password_request(self, login: str, password: str, realm: str) -> dict:
return dict(grant_type='password',
@@ -129,7 +130,6 @@ def correlation_id(date):
class MyPSACC:
vehicles_url = "https://idpcvs.peugeot.com/api/connectedcar/v2/oauth/authorize"
def connect(self, user, password):
self.manager.init_with_user_credentials(user, password, self.realm)
@@ -155,9 +155,9 @@ class MyPSACC:
self.api_config.api_key['client_id'] = self.client_id
self.api_config.api_key['x-introspect-realm'] = self.realm
self.headers = {
"x-introspect-realm": realm,
"accept": "application/hal+json",
}
"x-introspect-realm": realm,
"accept": "application/hal+json",
}
self.remote_token_last_update = None
self._record_enabled = False
@@ -222,7 +222,7 @@ class MyPSACC:
def refresh_remote_token(self, force=False):
if not force and self.remote_token_last_update is not None:
last_update: datetime = self.remote_token_last_update
if (datetime.now()-last_update).total_seconds() < MQTT_TOKEN_TTL:
if (datetime.now() - last_update).total_seconds() < MQTT_TOKEN_TTL:
return
self.manager._refresh_token()
res = self.manager.post(remote_url + self.client_id,
@@ -243,7 +243,7 @@ class MyPSACC:
logger.info("Connected with result code " + str(rc))
topics = [MQTT_RESP_TOPIC + self.customer_id + "/#"]
for vin in self.getVIN():
topics.append(MQTT_EVENT_TOPIC+ vin + "/#")
topics.append(MQTT_EVENT_TOPIC + vin + "/#")
for topic in topics:
client.subscribe(topic)
logger.info("subscribe to " + topic)
@@ -356,7 +356,7 @@ class MyPSACC:
@rate_limit(3, 60 * 20)
def wakeup(self, vin):
logger.info("ask wakeup to "+vin)
logger.info("ask wakeup to " + vin)
msg = self.mqtt_request(vin, {"action": "state"})
logger.info(msg)
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/VehCharge/state", msg)
@@ -402,10 +402,10 @@ class MyPSACC:
str = f.read()
return MyPSACC(**json.loads(str))
def set_record(self,value:bool):
def set_record(self, value: bool):
self._record_enabled = value
def record_position(self,vin, res:psac.models.status.Status):
def record_position(self, vin, res: psac.models.status.Status):
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);")
@@ -433,9 +433,9 @@ class MyPSACC:
res = conn.execute('SELECT * FROM position ORDER BY Timestamp');
features_list = []
for row in res:
print(row)
feature = Feature(geometry=Point((row["longitude"], row["latitude"])),
properties={"vin": row["vin"], "date": row["Timestamp"], "mileage": row["mileage"], "level": row["level"]})
properties={"vin": row["vin"], "date": row["Timestamp"], "mileage": row["mileage"],
"level": row["level"]})
features_list.append(feature)
feature_collection = FeatureCollection(features_list)
return geo_dumps(feature_collection, sort_keys=True)
@@ -451,28 +451,32 @@ class MyPSACC:
trips = []
tr = Trip()
battery_power = 46
for next_el in res[2:]:
distance = next_el["mileage"] - end["mileage"] # km
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"]
tr.end_at = end["Timestamp"]
tr.add_points(end["longitude"], end["latitude"])
tr.duration = (end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600
tr.speed_average = tr.distance / tr.duration
print(start["level"] - end["level"])
tr.consumption = (start["level"] - end["level"]) / 100 * battery_power # kw
tr.consumption_km = 100 * tr.consumption / tr.distance # kw/100 km
print(
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
for x in range(0,len(res)-2):
next_el = res[x+2]
if end["mileage"] - start["mileage"] == 0 or \
(end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600 > 3:
start = end
tr = Trip()
else:
tr.add_points(end["longitude"], end["latitude"])
distance = next_el["mileage"] - end["mileage"] # km
duration = (next_el["Timestamp"] - end["Timestamp"]).total_seconds() / 3600
if (distance == 0 and duration > 0.08) or duration > 2: # check the speed to handle missing point
tr.distance = end["mileage"] - start["mileage"] # km
if tr.distance > 0:
tr.start_at = start["Timestamp"]
tr.end_at = end["Timestamp"]
tr.add_points(end["longitude"], end["latitude"])
tr.duration = (end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600
tr.speed_average = tr.distance / tr.duration
tr.consumption = (start["level"] - end["level"]) / 100 * battery_power # kw
tr.consumption_km = 100 * tr.consumption / tr.distance # kw/100 km
logger.debug(
f"Trip: {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 = Trip()
else:
tr.add_points(end["longitude"], end["latitude"])
end = next_el
return trips
+3 -1
View File
@@ -1,4 +1,6 @@
from typing import List
from dateutil.tz import tzlocal
from geojson import Feature, Point, FeatureCollection, MultiLineString
from geojson import dumps as geo_dumps
@@ -48,6 +50,6 @@ class Trip:
"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,
res = {"start_at": self.start_at.astimezone(None).strftime("%x %X"), "end_at": self.end_at.astimezone(None).strftime("%x %X"), "duration": self.duration*60,
"speed_average": self.speed_average, "consumption_km": self.consumption_km, "distance": self.distance}
return res
+2 -1
View File
@@ -13,4 +13,5 @@ pytz
typing
argparse
flask
dash_bootstrap_components
dash_bootstrap_components
geojson
+6 -3
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import atexit
import sys
from threading import Thread
from oauth2_client.credentials_manager import OAuthError
@@ -11,6 +12,7 @@ from web.app import app, save_config
parser = argparse.ArgumentParser()
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--config", help="config file", type=argparse.FileType('r'))
@@ -21,7 +23,7 @@ def parse_args():
parser.add_argument("-r", "--record-position", help="save vehicle position to db", action='store_true')
parser.add_argument("-m", "--mail", help="change the email address")
parser.add_argument("-P", "--password", help="change the password")
parser.add_argument("--remote-disable",help="disable remote control")
parser.add_argument("--remote-disable", help="disable remote control")
parser.parse_args()
return parser
@@ -37,6 +39,7 @@ if __name__ == "__main__":
app.myp = MyPSACC.load_config(name=args.config.name)
else:
app.myp = MyPSACC.load_config()
atexit.register(app.myp.save_config)
if args.record_position:
app.myp.set_record(True)
try:
@@ -50,10 +53,10 @@ if __name__ == "__main__":
client_password = input("mypeugeot password: ")
app.myp.connect(client_email, client_password)
logger.info(app.myp.get_vehicles())
t1 = Thread(target=app.run,kwargs={"host":args.listen,"port":int(args.port)})
t1 = Thread(target=app.run, kwargs={"host": args.listen, "port": int(args.port)})
t1.start()
if args.remote_disable:
logger.info("mqtt disabled")
logger.info("mqtt disabled")
else:
app.myp.start_mqtt()
if args.charge_control:
+45 -33
View File
@@ -1,5 +1,6 @@
import json
import threading
import traceback
from datetime import datetime, timezone
import dash
import dash_bootstrap_components as dbc
@@ -13,6 +14,7 @@ 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"]
@@ -30,6 +32,7 @@ chc = None
Output('consumption_fig', 'figure'),
Output('consumption_fig_by_speed', 'figure'),
Output('consumption', 'children'),
Output('tab_trips', 'children'),
Input('date-slider', 'value'))
def display_value(value):
min = datetime.fromtimestamp(value[0], tz=timezone.utc)
@@ -38,10 +41,9 @@ def display_value(value):
for trip in trips:
if min <= trip.start_at <= max:
filtered_trips.append(trip)
print(len(filtered_trips))
figures.get_figures(filtered_trips)
consumption = "Average consumption: {:.1f} kW/100km".format(float(figures.consumption_df.mean()))
return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, consumption
consumption = "Average consumption: {:.1f} kW/100km".format(float(figures.consumption_df.mean(numeric_only=True)))
return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, consumption, figures.table_fig
@app.route('/getvehicles')
@@ -120,36 +122,46 @@ def save_config(my_peugeot: MyPSACC):
threading.Timer(30, save_config, args=[my_peugeot]).start()
trips = MyPSACC.get_trips()
figures.get_figures(trips)
try:
trips = MyPSACC.get_trips()
min_date = trips[0].start_at
max_date = trips[-1].start_at
min_millis = figures.unix_time_millis(min_date)
max_millis = figures.unix_time_millis(max_date)
step = (max_millis - min_millis) / 100
figures.get_figures(trips)
data_div = html.Div([dcc.RangeSlider(
id='date-slider',
min=min_millis,
max=max_millis,
step=step,
marks=figures.get_marks_from_start_end(min_date,
max_date),
value=[min_millis, max_millis],
),
html.Div([
dbc.Tabs([
dbc.Tab(label="Summary", tab_id="summary", children=[
html.H2(id="consumption",
children=figures.info),
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", id="tab_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"),
])])
except:
logger.error("Failed to generate figure, there is probably not enough data yet")
logger.error(traceback.format_exc())
data_div = dbc.Alert("No data to show", color="danger")
dash_app.layout = dbc.Container(fluid=True, children=[
html.H1('My car info'),
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([
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"),
]),
data_div
])
+23 -17
View File
@@ -17,7 +17,7 @@ def unix_time_millis(dt):
def get_marks_from_start_end(start, end):
nb_marks = 5
nb_marks = 10
result = []
time_delta = int((end - start).total_seconds() / nb_marks)
current = start
@@ -28,11 +28,11 @@ def get_marks_from_start_end(start, end):
result[-1] = end
if time_delta < 3600 * 24:
if time_delta > 3600:
date_f = '%y-%m-%d %Hh'
date_f = '%x %Hh'
else:
date_f = '%y-%m-%d %Hh%M'
date_f = '%x %Hh%M'
else:
date_f = '%Y-%m'
date_f = '%x'
marks = {}
for date in result:
marks[unix_time_millis(date)] = str(date.strftime(date_f))
@@ -45,10 +45,11 @@ trips_map = None
consumption_fig_by_speed = None
table_fig = None
pandas_options.display.float_format = '${:.2f}'.format
info = ""
def get_figures(trips: List[Trip]):
global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig
global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info
lats = []
lons = []
names = []
@@ -60,32 +61,37 @@ def get_figures(trips: List[Trip]):
lats = np.append(lats, None)
lons = np.append(lons, None)
names = np.append(names, None)
trips_map = px.line_mapbox(lat=lats, lon=lons, hover_name=names,
mapbox_style="stamen-terrain", zoom=12)
# 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': '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_fig
consumption_df = DataFrame.from_records([tr.get_consumption() for tr in 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, "value": tr.consumption_km} for tr in trips])
consumption_fig_by_speed = px.histogram(consum_df_by_speed, x="speed", y="value", histfunc="avg",
[{"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",
title="Consumption by speed")
consumption_fig_by_speed.update_traces(xbins_size=15)
consumption_fig_by_speed.update_layout(bargap=0.1)
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["value"],
go.Scatter(mode="markers", x=consum_df_by_speed["speed"], y=consum_df_by_speed["consumption"],
name="Trips"))
consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kW/100Km")
consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km")
info = "Average consumption: {:.1f} kW/100km".format(
float(consumption_df.mean(numeric_only=True)))