mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-22 09:26:16 +00:00
add dashboard
This commit is contained in:
+4
-1
@@ -9,6 +9,7 @@ from MyPSACC import MyPSACC
|
||||
from MyLogger import logger
|
||||
from psa_connectedcar.rest import ApiException
|
||||
|
||||
|
||||
class ChargeControls:
|
||||
|
||||
def __init__(self):
|
||||
@@ -121,4 +122,6 @@ class ChargeControl:
|
||||
chd = copy(self.__dict__)
|
||||
chd.pop("psacc")
|
||||
chd.pop("thread")
|
||||
return chd
|
||||
return chd
|
||||
|
||||
|
||||
|
||||
+45
-2
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import traceback
|
||||
import uuid
|
||||
from copy import copy
|
||||
@@ -12,13 +13,17 @@ from time import sleep
|
||||
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 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
|
||||
|
||||
oauhth_url = {"clientsB2CPeugeot":"https://idpcvs.peugeot.com/am/oauth2/access_token",
|
||||
"clientsB2CCitroen":"https://idpcvs.citroen.com/am/oauth2/access_token",
|
||||
@@ -419,7 +424,8 @@ class MyPSACC:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_recorded_position(self):
|
||||
@staticmethod
|
||||
def get_recorded_position():
|
||||
import sqlite3
|
||||
from geojson import Feature, Point, FeatureCollection
|
||||
from geojson import dumps as geo_dumps
|
||||
@@ -435,6 +441,41 @@ class MyPSACC:
|
||||
feature_collection = FeatureCollection(features_list)
|
||||
return geo_dumps(feature_collection, sort_keys=True)
|
||||
|
||||
@staticmethod
|
||||
def get_trips() -> List[Trips]:
|
||||
sqlite3.register_converter("DATETIME", convert_datetime)
|
||||
conn = sqlite3.connect('info.db', detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
|
||||
conn.row_factory = sqlite3.Row
|
||||
res = conn.execute('SELECT * FROM position ORDER BY Timestamp').fetchall()
|
||||
start = res[0]
|
||||
end = res[1]
|
||||
trips = []
|
||||
tr = Trips()
|
||||
battery_power = 46
|
||||
for next_el in res[2:]:
|
||||
distance = next_el["mileage"] - end["mileage"] # km
|
||||
if distance == 0:
|
||||
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
|
||||
tr = Trips()
|
||||
else:
|
||||
tr.add_points(end["longitude"], end["latitude"])
|
||||
end = next_el
|
||||
return trips
|
||||
|
||||
|
||||
class MyPeugeotEncoder(JSONEncoder):
|
||||
def default(self, mp: MyPSACC):
|
||||
data = copy(mp.__dict__)
|
||||
@@ -442,6 +483,8 @@ class MyPeugeotEncoder(JSONEncoder):
|
||||
mpd["proxies"] = data["_proxies"]
|
||||
mpd["refresh_token"] = mp.manager.refresh_token
|
||||
mpd["client_secret"] = mp.service_information.client_secret
|
||||
for el in ["client_id", "realm", "remote_refresh_token","customer_id"]:
|
||||
for el in ["client_id", "realm", "remote_refresh_token", "customer_id"]:
|
||||
mpd[el] = data[el]
|
||||
return mpd
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
+16
-5
@@ -1,8 +1,19 @@
|
||||
oauth2_client
|
||||
Flask
|
||||
certifi
|
||||
urllib3
|
||||
six
|
||||
requests
|
||||
Flask>=1.1.1
|
||||
certifi>=2018.4.16
|
||||
urllib3>=1.25.10
|
||||
six>=1.15.0
|
||||
requests>=2.24.0
|
||||
paho_mqtt
|
||||
python_dateutil
|
||||
|
||||
paho-mqtt>=1.5.0
|
||||
dash>=1.18.1
|
||||
numpy>=1.19.4
|
||||
pytz>=2019.3
|
||||
plotly>=4.5.0
|
||||
typing>=3.6.6
|
||||
python-dateutil>=2.8.0
|
||||
pandas>=1.1.3
|
||||
argparse>=1.4.0
|
||||
androguard>=3.3.5
|
||||
@@ -1,91 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import threading
|
||||
from threading import Thread
|
||||
|
||||
from oauth2_client.credentials_manager import OAuthError
|
||||
|
||||
from ChargeControl import ChargeControls
|
||||
from MyLogger import my_logger
|
||||
from MyPSACC import *
|
||||
from flask import Flask, request, jsonify
|
||||
from flask import Response as FlaskResponse
|
||||
import argparse
|
||||
from MyLogger import logger
|
||||
from MyPSACC import MyPSACC
|
||||
from web.app import app, save_config
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/getvehicles')
|
||||
def getvehicules():
|
||||
return jsonify(myp.getVIN())
|
||||
|
||||
|
||||
@app.route('/get_vehicleinfo/<string:vin>')
|
||||
def get_vehicle_Info(vin):
|
||||
response = app.response_class(
|
||||
response=json.dumps(myp.get_vehicle_info(vin).to_dict(), default=str),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/charge_now/<string:vin>/<int:charge>')
|
||||
def charge_now(vin, charge):
|
||||
return jsonify(myp.charge_now(vin, charge != 0))
|
||||
|
||||
|
||||
@app.route('/charge_hour')
|
||||
def change_charge_hour():
|
||||
return jsonify(myp.change_charge_hour(request.form['vin'], request.form['hour'], request.form['minute']))
|
||||
|
||||
|
||||
@app.route('/wakeup/<string:vin>')
|
||||
def wakeup(vin):
|
||||
return jsonify(myp.wakeup(vin))
|
||||
|
||||
|
||||
@app.route('/preconditioning/<string:vin>/<int:activate>')
|
||||
def preconditioning(vin, activate):
|
||||
return jsonify(myp.preconditioning(vin, activate))
|
||||
|
||||
@app.route('/position/<string:vin>')
|
||||
def get_position(vin):
|
||||
res = myp.get_vehicle_info(vin)
|
||||
longitude, latitude = res.last_position.geometry.coordinates
|
||||
return jsonify({"longitude":longitude,"latitude":latitude,"url":f"http://maps.google.com/maps?q={latitude},{longitude}"})
|
||||
|
||||
def save_config(mypeugeot: MyPSACC):
|
||||
myp.save_config()
|
||||
threading.Timer(30, save_config, args=[mypeugeot]).start()
|
||||
|
||||
|
||||
# Set a battery threshold and schedule an hour to stop the charge
|
||||
@app.route('/charge_control')
|
||||
def charge_control():
|
||||
logger.info(request)
|
||||
vin = request.args['vin']
|
||||
charge_control = chc.get(vin)
|
||||
if charge_control is None:
|
||||
return jsonify("error: VIN not in list")
|
||||
if 'hour' in request.args or 'minute' in request.args:
|
||||
charge_control.set_stop_hour([int(request.args["hour"]), int(request.args["minute"])])
|
||||
if 'percentage' in request.args:
|
||||
charge_control.percentage_threshold = int(request.args['percentage'])
|
||||
chc.save_config()
|
||||
return jsonify(charge_control.get_dict())
|
||||
|
||||
@app.route('/positions')
|
||||
def get_recorded_position():
|
||||
return FlaskResponse(myp.get_recorded_position(), mimetype='application/json')
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
header = response.headers
|
||||
header['Access-Control-Allow-Origin'] = '*'
|
||||
return response
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -110,30 +34,29 @@ if __name__ == "__main__":
|
||||
my_logger(handler_level=args.debug)
|
||||
logger.info("server start")
|
||||
if args.config:
|
||||
myp = MyPSACC.load_config(name=args.config.name)
|
||||
app.myp = MyPSACC.load_config(name=args.config.name)
|
||||
else:
|
||||
myp = MyPSACC.load_config()
|
||||
app.myp = MyPSACC.load_config()
|
||||
if args.record_position:
|
||||
myp.set_record(True)
|
||||
app.myp.set_record(True)
|
||||
try:
|
||||
myp.manager._refresh_token()
|
||||
app.myp.manager._refresh_token()
|
||||
except OAuthError:
|
||||
if args.mail and args.password:
|
||||
client_email = args.mail
|
||||
client_paswword = args.password
|
||||
client_password = args.password
|
||||
else:
|
||||
client_email = input("mypeugeot email: ")
|
||||
client_paswword = input("mypeugeot password: ")
|
||||
myp.connect(client_email, client_paswword)
|
||||
logger.info(myp.get_vehicles())
|
||||
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.start()
|
||||
if args.remote_disable:
|
||||
logger.info("mqtt disabled")
|
||||
else:
|
||||
myp.start_mqtt()
|
||||
app.myp.start_mqtt()
|
||||
if args.charge_control:
|
||||
chc = ChargeControls.load_config(myp, name=args.charge_control)
|
||||
chc.start()
|
||||
save_config(myp)
|
||||
|
||||
app.chc = ChargeControls.load_config(app.myp, name=args.charge_control)
|
||||
app.chc.start()
|
||||
save_config(app.myp)
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import json
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
import dash
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash.dependencies import Output, Input
|
||||
import dash_core_components as dcc
|
||||
import dash_html_components as html
|
||||
|
||||
from MyLogger import logger
|
||||
from flask import jsonify, request, Response as FlaskResponse
|
||||
|
||||
from web import figures
|
||||
|
||||
from MyPSACC import MyPSACC
|
||||
|
||||
dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
|
||||
app = dash_app.server
|
||||
myp = None
|
||||
chc = None
|
||||
|
||||
|
||||
@dash_app.callback(Output('trips_map', 'figure'),
|
||||
Output('consumption_fig', 'figure'),
|
||||
Output('consumption_fig_by_speed', 'figure'),
|
||||
Output('consumption', 'children'),
|
||||
Input('date-slider', 'value'))
|
||||
def display_value(value):
|
||||
min = datetime.fromtimestamp(value[0], tz=timezone.utc)
|
||||
max = datetime.fromtimestamp(value[1], tz=timezone.utc)
|
||||
filtered_trips = []
|
||||
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
|
||||
|
||||
|
||||
@app.route('/getvehicles')
|
||||
def getvehicules():
|
||||
return jsonify(myp.getVIN())
|
||||
|
||||
|
||||
@app.route('/get_vehicleinfo/<string:vin>')
|
||||
def get_vehicle_Info(vin):
|
||||
response = app.response_class(
|
||||
response=json.dumps(myp.get_vehicle_info(vin).to_dict(), default=str),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/charge_now/<string:vin>/<int:charge>')
|
||||
def charge_now(vin, charge):
|
||||
return jsonify(myp.charge_now(vin, charge != 0))
|
||||
|
||||
|
||||
@app.route('/charge_hour')
|
||||
def change_charge_hour():
|
||||
return jsonify(myp.change_charge_hour(request.form['vin'], request.form['hour'], request.form['minute']))
|
||||
|
||||
|
||||
@app.route('/wakeup/<string:vin>')
|
||||
def wakeup(vin):
|
||||
return jsonify(myp.wakeup(vin))
|
||||
|
||||
|
||||
@app.route('/preconditioning/<string:vin>/<int:activate>')
|
||||
def preconditioning(vin, activate):
|
||||
return jsonify(myp.preconditioning(vin, activate))
|
||||
|
||||
|
||||
@app.route('/position/<string:vin>')
|
||||
def get_position(vin):
|
||||
res = myp.get_vehicle_info(vin)
|
||||
longitude, latitude = res.last_position.geometry.coordinates
|
||||
return jsonify(
|
||||
{"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
|
||||
@app.route('/charge_control')
|
||||
def charge_control():
|
||||
logger.info(request)
|
||||
vin = request.args['vin']
|
||||
charge_control = chc.get(vin)
|
||||
if charge_control is None:
|
||||
return jsonify("error: VIN not in list")
|
||||
if 'hour' in request.args or 'minute' in request.args:
|
||||
charge_control.set_stop_hour([int(request.args["hour"]), int(request.args["minute"])])
|
||||
if 'percentage' in request.args:
|
||||
charge_control.percentage_threshold = int(request.args['percentage'])
|
||||
save_config()
|
||||
return jsonify(charge_control.get_dict())
|
||||
|
||||
|
||||
@app.route('/positions')
|
||||
def get_recorded_position():
|
||||
return FlaskResponse(myp.get_recorded_position(), mimetype='application/json')
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
header = response.headers
|
||||
header['Access-Control-Allow-Origin'] = '*'
|
||||
return response
|
||||
|
||||
|
||||
def save_config(my_peugeot: MyPSACC):
|
||||
my_peugeot.save_config()
|
||||
threading.Timer(30, save_config, args=[my_peugeot]).start()
|
||||
|
||||
|
||||
trips = MyPSACC.get_trips()
|
||||
figures.get_figures(trips)
|
||||
dash_app.layout = dbc.Container([
|
||||
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}
|
||||
),
|
||||
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"),
|
||||
])
|
||||
@@ -0,0 +1,80 @@
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
import numpy as np
|
||||
import pytz
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pandas import DataFrame
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from Trips import Trips
|
||||
|
||||
|
||||
def unix_time_millis(dt):
|
||||
return int(dt.timestamp())
|
||||
|
||||
|
||||
def get_marks_from_start_end(start, end):
|
||||
nb_marks = 5
|
||||
result = []
|
||||
time_delta = int((end - start).total_seconds() / nb_marks)
|
||||
current = start
|
||||
if time_delta > 0:
|
||||
while current <= end:
|
||||
result.append(current)
|
||||
current += relativedelta(seconds=time_delta)
|
||||
result[-1] = end
|
||||
if time_delta < 3600 * 24:
|
||||
if time_delta > 3600:
|
||||
date_f = '%y-%m-%d %Hh'
|
||||
else:
|
||||
date_f = '%y-%m-%d %Hh%M'
|
||||
else:
|
||||
date_f = '%Y-%m'
|
||||
marks = {}
|
||||
for date in result:
|
||||
marks[unix_time_millis(date)] = str(date.strftime(date_f))
|
||||
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
|
||||
|
||||
|
||||
def get_figures(trips: List[Trips]):
|
||||
global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed
|
||||
lats = []
|
||||
lons = []
|
||||
names = []
|
||||
for trip in trips:
|
||||
for points in trip.positions:
|
||||
lats = np.append(lats, points.longitude)
|
||||
lons = np.append(lons, points.latitude)
|
||||
names = np.append(names, [str(trip.start_at)])
|
||||
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)
|
||||
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')
|
||||
|
||||
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",
|
||||
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.add_trace(
|
||||
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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user