mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-26 10:17:18 +00:00
add charge price
This commit is contained in:
+5
-7
@@ -6,6 +6,7 @@ from flask import Flask
|
||||
import locale
|
||||
|
||||
from werkzeug import run_simple
|
||||
|
||||
try:
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
except ImportError:
|
||||
@@ -18,6 +19,10 @@ from MyPSACC import MyPSACC
|
||||
app = None
|
||||
dash_app = None
|
||||
dispatcher = None
|
||||
# noinspection PyTypeChecker
|
||||
myp:MyPSACC = None
|
||||
# noinspection PyTypeChecker
|
||||
chc: ChargeControls = None
|
||||
|
||||
def start_app(title, base_path, debug: bool, host, port):
|
||||
global app, dash_app, dispatcher
|
||||
@@ -42,13 +47,6 @@ def start_app(title, base_path, debug: bool, host, port):
|
||||
import web.views
|
||||
return run_simple(host, port, application, use_reloader=False, use_debugger=debug)
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
myp:MyPSACC = None
|
||||
# noinspection PyTypeChecker
|
||||
chc: ChargeControls = None
|
||||
|
||||
|
||||
def save_config(my_peugeot: MyPSACC, name):
|
||||
my_peugeot.save_config(name)
|
||||
threading.Timer(30, save_config, args=[my_peugeot, name]).start()
|
||||
|
||||
@@ -8,24 +8,40 @@ from MyLogger import logger
|
||||
|
||||
callback_fct: Callable[[], None] = lambda: None
|
||||
default_db_file = 'info.db'
|
||||
db_initialized = False
|
||||
|
||||
|
||||
def convert_datetime(st):
|
||||
return datetime.strptime(st.decode("utf-8"), "%Y-%m-%d %H:%M:%S+00:00").replace(tzinfo=pytz.UTC)
|
||||
def convert_datetime_from_bytes(bytes_string):
|
||||
return datetime.strptime(bytes_string.decode("utf-8"), "%Y-%m-%d %H:%M:%S+00:00").replace(tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
def convert_datetime_from_string(st):
|
||||
return datetime.strptime(st, "%Y-%m-%dT%H:%M:%S+00:00").replace(tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
def update_callback():
|
||||
callback_fct()
|
||||
|
||||
|
||||
def get_db(db_file=default_db_file):
|
||||
sqlite3.register_converter("DATETIME", convert_datetime)
|
||||
conn = sqlite3.connect(db_file, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
|
||||
conn.row_factory = sqlite3.Row
|
||||
def set_db_callback(callbackfct):
|
||||
global callback_fct
|
||||
callback_fct = callbackfct
|
||||
|
||||
|
||||
def backup(conn):
|
||||
back_conn = sqlite3.connect("info_backup.db")
|
||||
conn.backup(back_conn)
|
||||
back_conn.close()
|
||||
|
||||
|
||||
def init_db(conn):
|
||||
global db_initialized
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY, VIN TEXT, longitude REAL, "
|
||||
"latitude REAL, mileage REAL, level INTEGER, level_fuel INTEGER, moving BOOLEAN, temperature INTEGER);")
|
||||
make_backup = False
|
||||
try:
|
||||
conn.execute("ALTER TABLE position ADD level_fuel INTEGER;")
|
||||
make_backup = True
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME,VIN TEXT, "
|
||||
@@ -33,10 +49,34 @@ def get_db(db_file=default_db_file):
|
||||
conn.create_function("update_trips", 0, update_callback)
|
||||
conn.execute("CREATE TEMP TRIGGER IF NOT EXISTS update_trigger AFTER INSERT ON position BEGIN "
|
||||
"SELECT update_trips(); END;")
|
||||
try:
|
||||
conn.execute("ALTER TABLE battery ADD price INTEGER;")
|
||||
make_backup = True
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
if make_backup:
|
||||
backup(conn)
|
||||
clean_battery(conn)
|
||||
conn.commit()
|
||||
db_initialized = True
|
||||
|
||||
|
||||
def get_db(db_file=default_db_file):
|
||||
sqlite3.register_converter("DATETIME", convert_datetime_from_bytes)
|
||||
conn = sqlite3.connect(db_file, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
|
||||
conn.row_factory = sqlite3.Row
|
||||
if not db_initialized:
|
||||
init_db(conn)
|
||||
return conn
|
||||
|
||||
|
||||
def clean_battery(conn):
|
||||
# delete charging longer than 17h
|
||||
conn.execute("DElETE FROM battery WHERE JULIANDAY(stop_at)-JULIANDAY(start_at)>0.7;")
|
||||
conn.execute("DELETE FROM battery WHERE start_level==end_level;")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def clean_position(conn):
|
||||
res = conn.execute(
|
||||
"SELECT Timestamp,mileage,level from position ORDER BY Timestamp DESC LIMIT 3;").fetchall()
|
||||
@@ -55,3 +95,13 @@ def get_last_temp(vin):
|
||||
if res is None:
|
||||
return None
|
||||
return res[0]
|
||||
|
||||
|
||||
def set_chargings_price(conn, start_at, price):
|
||||
if isinstance(start_at, str):
|
||||
start_at = convert_datetime_from_string(start_at)
|
||||
update = conn.execute("UPDATE battery SET price=? WHERE start_at=?", (price, start_at)).rowcount == 1
|
||||
conn.commit()
|
||||
if not update:
|
||||
logger.error("Can't find line to update in the database")
|
||||
return update
|
||||
+21
-4
@@ -1,5 +1,4 @@
|
||||
from copy import deepcopy
|
||||
from typing import Tuple
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
import dash_table
|
||||
@@ -14,6 +13,8 @@ from Trip import Trips
|
||||
from pandas import options as pandas_options
|
||||
import dash_html_components as html
|
||||
|
||||
from libs.elec_price import ElecPrice
|
||||
|
||||
|
||||
def unix_time_millis(dt):
|
||||
return int(dt.timestamp())
|
||||
@@ -55,7 +56,7 @@ battery_info = dbc.Alert("No data to show", color="danger")
|
||||
battery_table = None
|
||||
|
||||
|
||||
def get_figures(trips: Trips, charging: Tuple[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, \
|
||||
battery_table, consumption_graph_by_temp
|
||||
lats = []
|
||||
@@ -116,8 +117,10 @@ def get_figures(trips: Trips, charging: Tuple[dict]):
|
||||
try:
|
||||
charge_speed = 3600 * charging_data["kw"].mean() / \
|
||||
(charging_data["stop_at"] - charging_data["start_at"]).mean().total_seconds()
|
||||
price_kw = (charging_data["price"] / charging_data["kw"]).mean()
|
||||
except (TypeError, KeyError): # when there is no data yet:
|
||||
charge_speed = 0
|
||||
price_kw = 0
|
||||
|
||||
battery_info = dash_table.DataTable(
|
||||
id='battery_info',
|
||||
@@ -145,7 +148,18 @@ def get_figures(trips: Trips, charging: Tuple[dict]):
|
||||
html.Td("Average charge speed:"),
|
||||
html.Td("{:.3f} kW".format(charge_speed))
|
||||
]
|
||||
)
|
||||
),
|
||||
html.Tr(
|
||||
[
|
||||
html.Td('Average Price:', rowSpan=2),
|
||||
html.Td("{:.2f} {}/100km".format(price_kw*kw_per_km, ElecPrice.currency)),
|
||||
]
|
||||
),
|
||||
html.Tr(
|
||||
[
|
||||
"{:.2f} {}/kW".format(price_kw, ElecPrice.currency),
|
||||
]
|
||||
),
|
||||
])
|
||||
|
||||
battery_table = dash_table.DataTable(
|
||||
@@ -159,8 +173,11 @@ def get_figures(trips: Trips, charging: Tuple[dict]):
|
||||
{'id': 'co2', 'name': 'CO2', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" g/kWh").precision(1)},
|
||||
{'id': 'kw', 'name': 'consumption', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" kWh").precision(3)}],
|
||||
'format': deepcopy(nb_format).symbol_suffix(" kWh").precision(2)},
|
||||
{'id': 'price', 'name': 'price', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" "+ElecPrice.currency).precision(2)}],
|
||||
data=charging,
|
||||
editable=True
|
||||
)
|
||||
consumption_by_temp_df = consumption_df[consumption_df["consumption_by_temp"].notnull()]
|
||||
if len(consumption_by_temp_df) > 0:
|
||||
|
||||
+53
-9
@@ -2,33 +2,57 @@ import json
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash.dependencies import Output, Input, MATCH
|
||||
from dash.dependencies import Output, Input, MATCH, State
|
||||
import dash_core_components as dcc
|
||||
import dash_html_components as html
|
||||
import dash_daq as daq
|
||||
import pandas as pd
|
||||
from dash.exceptions import PreventUpdate
|
||||
|
||||
from MyLogger import logger
|
||||
from flask import jsonify, request, Response as FlaskResponse
|
||||
|
||||
from MyPSACC import MyPSACC
|
||||
from Trip import Trips
|
||||
|
||||
from libs.charging import Charging
|
||||
from web import figures
|
||||
|
||||
from web.app import app, dash_app, myp, chc
|
||||
import web.db
|
||||
from web.db import set_chargings_price, get_db, set_db_callback
|
||||
|
||||
RESPONSE = "-response"
|
||||
|
||||
EMPTY_DIV = "empty-div"
|
||||
|
||||
ABRP_SWITCH = 'abrp-switch'
|
||||
|
||||
ERROR_DIV = dbc.Alert("No data to show, there is probably no trips recorded yet", color="danger")
|
||||
trips: Trips
|
||||
chargings: dict
|
||||
chargings: list[dict]
|
||||
min_date = max_date = min_millis = max_millis = step = marks = cached_layout = None
|
||||
|
||||
|
||||
def diff_dashtable(data, data_previous, row_id_name="row_id"):
|
||||
df, df_previous = pd.DataFrame(data=data), pd.DataFrame(data_previous)
|
||||
for _df in [df, df_previous]:
|
||||
assert row_id_name in _df.columns
|
||||
_df = _df.set_index(row_id_name)
|
||||
mask = df.ne(df_previous)
|
||||
df_diff = df[mask].dropna(how="all", axis="columns").dropna(how="all", axis="rows")
|
||||
changes = []
|
||||
for idx, row in df_diff.iterrows():
|
||||
row_id = row.name
|
||||
row.dropna(inplace=True)
|
||||
for change in row.iteritems():
|
||||
changes.append(
|
||||
{
|
||||
row_id_name: data[row.name][row_id_name],
|
||||
"column_name": change[0],
|
||||
"current_value": change[1],
|
||||
"previous_value": df_previous.at[row_id, change[0]],
|
||||
}
|
||||
)
|
||||
return changes
|
||||
|
||||
|
||||
@dash_app.callback(Output('trips_map', 'figure'),
|
||||
Output('consumption_fig', 'figure'),
|
||||
Output('consumption_fig_by_speed', 'figure'),
|
||||
@@ -48,7 +72,7 @@ def display_value(value):
|
||||
for trip in trips:
|
||||
if mini <= trip.start_at <= maxi:
|
||||
filtered_trips.append(trip)
|
||||
filtered_chargings = MyPSACC.get_chargings(mini, maxi)
|
||||
filtered_chargings = Charging.get_chargings(mini, maxi)
|
||||
figures.get_figures(filtered_trips, filtered_chargings)
|
||||
consumption = "Average consumption: {:.1f} kWh/100km".format(float(figures.consumption_df["consumption_km"].mean()))
|
||||
return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, \
|
||||
@@ -56,6 +80,25 @@ def display_value(value):
|
||||
figures.battery_table, max_millis, step, marks
|
||||
|
||||
|
||||
@dash_app.callback(
|
||||
Output(EMPTY_DIV, "children"),
|
||||
[Input("battery-table", "data_timestamp")],
|
||||
[
|
||||
State("battery-table", "data"),
|
||||
State("battery-table", "data_previous"),
|
||||
],
|
||||
)
|
||||
def capture_diffs(ts, data, data_previous):
|
||||
if ts is None:
|
||||
raise PreventUpdate
|
||||
diff_data = diff_dashtable(data, data_previous, "start_at")
|
||||
for el in diff_data:
|
||||
if el['column_name'] == 'price':
|
||||
if not set_chargings_price(get_db(), el['start_at'], el['current_value']):
|
||||
logger.error("Can't find line to update in the database")
|
||||
return ""
|
||||
|
||||
|
||||
@dash_app.callback(Output({'role': ABRP_SWITCH + RESPONSE, 'vin': MATCH}, 'children'),
|
||||
Input({'role': ABRP_SWITCH, 'vin': MATCH}, 'id'),
|
||||
Input({'role': ABRP_SWITCH, 'vin': MATCH}, 'value'))
|
||||
@@ -176,7 +219,7 @@ def update_trips():
|
||||
trips_by_vin = Trips.get_trips(myp.vehicles_list)
|
||||
trips = next(iter(trips_by_vin.values())) # todo handle multiple car
|
||||
assert len(trips) > 0
|
||||
chargings = MyPSACC.get_chargings()
|
||||
chargings = Charging.get_chargings()
|
||||
except (StopIteration, AssertionError):
|
||||
logger.debug("No trips yet")
|
||||
return
|
||||
@@ -258,7 +301,8 @@ def serve_layout():
|
||||
|
||||
|
||||
try:
|
||||
web.db.callback_fct = update_trips
|
||||
set_db_callback(update_trips)
|
||||
Charging.set_default_price()
|
||||
update_trips()
|
||||
except (IndexError, TypeError):
|
||||
logger.debug("Failed to get trips, there is probably not enough data yet %s", traceback.format_exc())
|
||||
|
||||
Reference in New Issue
Block a user