add charge price

This commit is contained in:
Florian Bezannier
2021-04-20 11:10:52 +02:00
parent 42411d8818
commit 98baca6601
9 changed files with 276 additions and 44 deletions
+1
View File
@@ -140,6 +140,7 @@ cython_debug/
.idea/
backup.ab
*.apk
*.ini
info.db
otp.bin
charge_config1.json
+4
View File
@@ -1,3 +1,7 @@
doc-warnings: false
pylint:
disable:
- C0114
- C0111
ignore-paths:
- psa_connectedcar
+3 -18
View File
@@ -12,11 +12,11 @@ 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 Tuple
import psa_connectedcar as psac
from Car import Cars, Car
from ecomix import Ecomix
from libs.charging import Charging
from otp.Otp import load_otp, new_otp_session, save_otp, ConfigException, Otp
from psa_connectedcar import ApiClient
from psa_connectedcar.rest import ApiException
@@ -573,9 +573,8 @@ class MyPSACC:
co2_per_kw = Ecomix.get_co2_per_kw(start_at, charge_date, latitude, longitude,
from_cache=self.co2_signal_api is not None)
kw = (level - start_level) / 100 * self.vehicles_list.get_car_by_vin(vin).battery_power
conn.execute(
"UPDATE battery set stop_at=?, end_level=?, co2=?, kw=? WHERE start_at=? and VIN=?",
(charge_date, level, co2_per_kw, kw, start_at, vin))
Charging.update_chargings(conn, start_at, charge_date, level, co2_per_kw, kw, vin)
conn.commit()
except TypeError:
logger.debug("battery table is empty")
@@ -598,20 +597,6 @@ class MyPSACC:
conn.close()
return geo_dumps(feature_collection, sort_keys=True)
@staticmethod
def get_chargings(mini=None, maxi=None) -> Tuple[dict]:
conn = get_db()
if mini is not None:
if maxi is not None:
res = conn.execute("select * from battery WHERE start_at>=? and start_at<=?", (mini, maxi)).fetchall()
else:
res = conn.execute("select * from battery WHERE start_at>=?", (mini,)).fetchall()
elif maxi is not None:
res = conn.execute("select * from battery WHERE start_at<=?", (maxi,)).fetchall()
else:
res = conn.execute("select * from battery").fetchall()
return tuple(map(dict, res))
def __iter__(self):
for key, value in self.__dict__.items():
yield key, value
+39
View File
@@ -0,0 +1,39 @@
from libs.elec_price import ElecPrice
from web.db import get_db, set_chargings_price, clean_battery
elec_price = ElecPrice.read_config()
class Charging:
@staticmethod
def get_chargings(mini=None, maxi=None) -> list[dict]:
conn = get_db()
if mini is not None:
if maxi is not None:
res = conn.execute("select * from battery WHERE start_at>=? and start_at<=?", (mini, maxi)).fetchall()
else:
res = conn.execute("select * from battery WHERE start_at>=?", (mini,)).fetchall()
elif maxi is not None:
res = conn.execute("select * from battery WHERE start_at<=?", (maxi,)).fetchall()
else:
res = conn.execute("select * from battery").fetchall()
conn.close()
return list(map(dict, res))
@staticmethod
def set_default_price():
if elec_price.is_enable():
conn = get_db()
charge_list = list(map(dict, conn.execute("SELECT * FROM battery WHERE price IS NULL").fetchall()))
for el in charge_list:
el["price"] = elec_price.get_price(el["start_at"], el["stop_at"], el["kw"])
set_chargings_price(conn, el["start_at"], el["price"])
conn.close()
@staticmethod
def update_chargings(conn, start_at, stop_at, level, co2_per_kw, kw, vin):
price = elec_price.get_price(start_at, stop_at, kw)
conn.execute(
"UPDATE battery set stop_at=?, end_level=?, co2=?, kw=?, price=? WHERE start_at=? and VIN=?",
(stop_at, level, co2_per_kw, kw, price, start_at, vin))
clean_battery(conn)
+94
View File
@@ -0,0 +1,94 @@
from datetime import datetime, timezone, timedelta
import configparser
from statistics import mean
CONFIG_FILENAME = "config.ini"
def set_number(value):
try:
return float(value)
except ValueError:
return None
def utc_to_local(utc_dt):
return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
class ElecPrice:
currency = ""
def __init__(self, day_price, night_price=None, nights_hours=None):
self.day_price = set_number(day_price)
self.night_price = set_number(night_price)
self.nights_hour = None
self.set_night_hour(nights_hours)
self.config_filename = CONFIG_FILENAME
def set_night_hour(self, value):
if value is not None and isinstance(value, list):
self.nights_hour = []
for hours in value:
self.nights_hour.append(list(map(int, hours)))
def compare_hour(self, date: datetime, hour, minute):
if date.hour < hour:
return False
if date.hour == hour and date.minute < minute:
return False
return True
def get_instant_price(self, date):
local_date = utc_to_local(date)
if self.night_price is None:
return self.day_price
if self.compare_hour(local_date, self.nights_hour[0][0], self.nights_hour[0][1]) or \
not self.compare_hour(local_date, self.nights_hour[1][0], self.nights_hour[1][1]):
return self.night_price
return self.day_price
def get_price(self, start, end, consumption):
prices = []
date = start
while date < end:
prices.append(self.get_instant_price(date))
date = date + timedelta(minutes=30)
return round(consumption * mean(prices), 2)
def is_enable(self):
return self.day_price is not None
@staticmethod
def read_config(name=CONFIG_FILENAME):
config = configparser.ConfigParser()
if len(config.read(name)) == 0:
ElecPrice.write_default_config(name)
config.read(name)
elec_config = config["Electricity config"]
if len(elec_config["night price"]) > 0:
night_hours = []
night_price = elec_config["night price"]
for hour in [elec_config["night hour start"], elec_config["night hour end"]]:
night_hours.append(hour.split("h"))
else:
night_hours = None
night_price = None
ElecPrice.currency = config["General"]["currency"]
return ElecPrice(elec_config["day price"], night_price, night_hours)
@staticmethod
def write_default_config(name=CONFIG_FILENAME):
config = configparser.ConfigParser()
config["General"] = {
"currency": ""
}
config["Electricity config"] = {
"day price": "",
"night price": "",
"night hour start": "",
"night hour end": ""
}
with open(name, "w") as f:
config.write(f)
+5 -7
View File
@@ -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()
+56 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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())