From ed3ee579b9da6db9fb7075891853ff2ceddd918b Mon Sep 17 00:00:00 2001 From: Florian Bezannier Date: Thu, 31 Dec 2020 00:26:33 +0100 Subject: [PATCH] update api, update battery record --- ChargeControl.py | 39 ++--- MyPSACC.py | 83 ++++++---- ecomix.py | 47 ++++++ psa_connectedcar/__init__.py | 3 + psa_connectedcar/models/__init__.py | 3 + psa_connectedcar/models/energy.py | 230 +++++++++++++++++++++++++++- requirements.txt | 3 +- web/callback.py | 18 ++- web/db.py | 4 +- web/figures.py | 20 ++- 10 files changed, 391 insertions(+), 59 deletions(-) create mode 100644 ecomix.py diff --git a/ChargeControl.py b/ChargeControl.py index 9b0b213..0141f36 100644 --- a/ChargeControl.py +++ b/ChargeControl.py @@ -9,25 +9,28 @@ from MyPSACC import MyPSACC from MyLogger import logger from psa_connectedcar.rest import ApiException + class ChargeControl: periodicity = 120 MQTT_TIMEOUT = 60 - def __init__(self, psacc:MyPSACC, vin, percentage_threshold, stop_hour): + + def __init__(self, psacc: MyPSACC, vin, percentage_threshold, stop_hour): self.vin = vin self.percentage_threshold = percentage_threshold self.set_stop_hour(stop_hour) self.psacc = psacc self.retry_count = 0 - self.thread:threading.Timer = None + self.thread: threading.Timer = None + self.always_check = True - def set_stop_hour(self,stop_hour): + def set_stop_hour(self, stop_hour): if stop_hour is None or stop_hour == [0, 0]: self._stop_hour = None self._next_stop_hour = None else: self._stop_hour = stop_hour self._next_stop_hour = datetime.now().replace(hour=stop_hour[0], minute=stop_hour[1], second=0) - if self._next_stop_hour < datetime.now(): + if self._next_stop_hour < datetime.now(): self._next_stop_hour += timedelta(days=1) def start(self): @@ -38,18 +41,18 @@ class ChargeControl: stop_charge = True self._next_stop_hour += timedelta(days=1) logger.info("it's time to stop the charge") - else : + else: stop_charge = False - if self.percentage_threshold != 100 or stop_charge: + if self.percentage_threshold != 100 or stop_charge or self.always_check: res = None try: res = self.psacc.get_vehicle_info(self.vin) except ApiException: logger.error(traceback.format_exc()) if res is not None: - status = res.energy[0]['charging']['status'] - level = res.energy[0]["level"] + status = res.energy[0].charging.status + level = res.energy[0].level logger.info(f"charging status of {self.vin} is {status}, battery level: {level}") if status == "InProgress": # force update if the car doesn't send info during 10 minutes @@ -57,17 +60,17 @@ class ChargeControl: if (datetime.utcnow() - last_update).total_seconds() > 60 * 10: self.psacc.wakeup(self.vin) if (level >= self.percentage_threshold and self.retry_count < 2) or stop_charge: - self.psacc.charge_now(self.vin,False) + self.psacc.charge_now(self.vin, False) self.retry_count += 1 sleep(ChargeControl.MQTT_TIMEOUT) res = self.psacc.get_vehicle_info(self.vin) - status = res.energy[0]['charging']['status'] + status = res.energy[0].charging.status if status == "InProgress": logger.warn(f"retry to stop the charge of {self.vin}") self.psacc.charge_now(self.vin, False) self.retry_count += 1 if self._next_stop_hour is not None: - next_in_second = (self._next_stop_hour- now).total_seconds() + next_in_second = (self._next_stop_hour - now).total_seconds() if next_in_second < periodicity: periodicity = next_in_second else: @@ -95,25 +98,25 @@ class ChargeControls: def save_config(self, name="charge_config.json", force=False): chd = {} for key, el in self.list.items(): - chd[el.vin] = {"percentage_threshold": el.percentage_threshold, "stop_hour": el._stop_hour} + chd[el.vin] = {"percentage_threshold": el.percentage_threshold, "stop_hour": el._stop_hour} config_str = json.dumps(chd, sort_keys=True, indent=4).encode('utf-8') new_hash = md5(config_str).hexdigest() - if force or self._confighash != new_hash : + if force or self._confighash != new_hash: with open(name, "wb") as f: f.write(config_str) self._confighash = new_hash logger.info("save config change") - def load_config(psacc:MyPSACC, name="charge_config.json"): + def load_config(psacc: MyPSACC, name="charge_config.json"): with open(name, "r") as f: str = f.read() - chd = json.loads(str) + chd = json.loads(str) charge_control_list = ChargeControls() for vin, el in chd.items(): - charge_control_list.list[vin] = ChargeControl(psacc,vin,**el) + charge_control_list.list[vin] = ChargeControl(psacc, vin, **el) return charge_control_list - def get(self,vin) -> ChargeControl: + def get(self, vin) -> ChargeControl: try: return self.list[vin] except KeyError: @@ -122,5 +125,3 @@ class ChargeControls: def start(self): for vin, charge_control in self.list.items(): charge_control.start() - - diff --git a/MyPSACC.py b/MyPSACC.py index 70e7075..1d88578 100644 --- a/MyPSACC.py +++ b/MyPSACC.py @@ -16,6 +16,7 @@ from typing import List import psa_connectedcar as psac from Trip import Trip +from ecomix import Ecomix from psa_connectedcar import ApiClient from psa_connectedcar.rest import ApiException from MyLogger import logger @@ -25,6 +26,8 @@ import sqlite3 from web.db import get_db +BATTERY_POWER = 46 + 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", @@ -185,7 +188,7 @@ class MyPSACC: if res is None: res = self.api().get_vehicle_status(self.get_vehicle_id_with_vin(vin), extension=["odometer"]) if self._record_enabled: - self.record_position(vin, res) + self.record_info(vin, res) return res # monitor doesn't seem to work @@ -311,7 +314,7 @@ class MyPSACC: def get_charge_hour(self, vin): reg = r"PT([0-9]{1,2})H([0-9]{1,2})?" data = self.get_vehicle_info(vin) - hour_str = data.energy[0]['charging']['nextDelayedTime'] + hour_str = data.energy[0].charging.next_delayed_time hour = re.findall(reg, hour_str)[0] h = int(hour[0]) if hour[1] == '': @@ -322,7 +325,7 @@ class MyPSACC: def get_charge_status(self, vin): data = self.get_vehicle_info(vin) - status = data.energy[0]['charging']['status'] + status = data.energy[0].charging.status return status def veh_charge_request(self, vin, hour, miinute, charge_type): @@ -407,32 +410,50 @@ class MyPSACC: self._record_enabled = value @staticmethod - def record_position(vin, res: psac.models.status.Status): - longitude = res.last_position.geometry.coordinates[0] - latitude = res.last_position.geometry.coordinates[1] - date = res.last_position.properties.updated_at - mileage = res.timed_odometer.mileage - level = res.energy[0]["level"] - charging_status = res.energy[0]['charging']['status'] - if mileage == 0: # fix a bug of the api + def record_info(vin, status: psac.models.status.Status): + longitude = status.last_position.geometry.coordinates[0] + latitude = status.last_position.geometry.coordinates[1] + date = status.last_position.properties.updated_at + mileage = status.timed_odometer.mileage + level = status.energy[0].level + charging_status = status.energy[0].charging.status + conn = get_db() + if mileage == 0: # fix a bug of the api logger.error(f"The api return a wrong mileage for {vin} : {mileage}") else: try: - conn = get_db() conn.execute("INSERT INTO position(Timestamp,VIN,longitude,latitude,mileage,level) VALUES(?,?,?,?,?,?)", (date, vin, longitude, latitude, mileage, level)) conn.commit() logger.info(f"new position recorded for {vin}") except sqlite3.IntegrityError: logger.debug("position already saved") - finally: - conn.close() - #todo handle battery status - if charging_status is "InProgress": - #create a new line - pass - elif charging_status is "Stopped" or "Finished": - pass + # todo handle battery status + charge_date = status.energy[0].updated_at + if charging_status == "InProgress": + try: + in_progress = conn.execute("SELECT stop_at FROM battery WHERE VIN=? ORDER BY start_at DESC limit 1", (vin,)).fetchone()[0] is None + except TypeError: + in_progress = False + if not in_progress: + res = conn.execute("INSERT INTO battery(start_at,start_level,VIN) VALUES(?,?,?)", + (charge_date, level, vin)) + conn.commit() + else: + try: + start_at, stop_at, start_level = conn.execute("SELECT start_at, stop_at, start_level, from battery WHERE VIN=? ORDER BY start_at " + "DESC limit 1", (vin,)).fetchone() + in_progress = stop_at is None + if in_progress: + co2_per_kw = Ecomix.get_co2_per_kw(start_at, stop_at, latitude, longitude) + kw = (level-start_level)/100*BATTERY_POWER + res = 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)) + conn.commit() + except: + logger.debug("Error when saving status " + traceback.format_exc()) + pass + conn.close() @staticmethod def get_recorded_position(): @@ -458,9 +479,8 @@ class MyPSACC: end = res[1] trips = [] tr = Trip() - battery_power = 46 - for x in range(0,len(res)-2): - next_el = res[x+2] + 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 @@ -476,7 +496,7 @@ class MyPSACC: 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 = (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") @@ -488,6 +508,19 @@ class MyPSACC: end = next_el return trips + @staticmethod + def get_chargings(min=None, max=None): + conn = get_db() + if min is not None: + if max is not None: + res = conn.execute("select * from battery WHERE start_at>=? and start_at<=?",(min,max)).fetchall() + else: + res = conn.execute("select * from battery WHERE start_at>=?",(min,)).fetchall() + elif max is not None: + res = conn.execute("select * from battery WHERE start_at<=?", (max,)).fetchall() + else: + res = conn.execute("select * from battery").fetchall() + return tuple(map(dict,res)) class MyPeugeotEncoder(JSONEncoder): def default(self, mp: MyPSACC): @@ -499,5 +532,3 @@ class MyPeugeotEncoder(JSONEncoder): for el in ["client_id", "realm", "remote_refresh_token", "customer_id"]: mpd[el] = data[el] return mpd - - diff --git a/ecomix.py b/ecomix.py new file mode 100644 index 0000000..87ac428 --- /dev/null +++ b/ecomix.py @@ -0,0 +1,47 @@ +from datetime import datetime, timedelta +from statistics import mean +import xml.etree.ElementTree as ET +import requests +import reverse_geocode + + +class Ecomix: + @staticmethod + def get_data_france(start, end): + start_str = start.strftime("%d/%m/%Y") + end_str = end.strftime("%d/%m/%Y") + res = requests.get( + f"https://eco2mix.rte-france.com/curves/eco2mixWeb?type=co2&&dateDeb={start_str}&dateFin={end_str}&mode=NORM", + headers={ + "Origin": "https://www.rte-france.com", + "Referer": "https://www.rte-france.com/eco2mix/les-emissions-de-co2-par-kwh-produit-en-france", + } + ) + + etree = ET.fromstring(res.text) + period_start = (start.hour + int(start.minute / 30)) * 4 + period_end = (end.hour + int(end.minute / 30)) * 4 + + valeurs = etree.iter("valeur") + co2_per_kw = [] + + valeur = next(valeurs) + while int(valeur.attrib["periode"]) != period_start: + valeur = next(valeurs) + while int(valeur.attrib["periode"]) != period_end: + co2_per_kw.append(int(valeur.text)) + valeur = next(valeurs) + + return mean(co2_per_kw) + + @staticmethod + def get_co2_per_kw(start: datetime, end: datetime, latitude, longitude): + location = reverse_geocode.search([(latitude, longitude)])[0] + country_code = location["country_code"] + # todo implement other countries + if country_code == 'FR': + co2_per_kw = Ecomix.get_data_france(start, end) + else: + co2_per_kw = None + return co2_per_kw + diff --git a/psa_connectedcar/__init__.py b/psa_connectedcar/__init__.py index 84b8f78..50495b2 100644 --- a/psa_connectedcar/__init__.py +++ b/psa_connectedcar/__init__.py @@ -55,6 +55,9 @@ from psa_connectedcar.models.e_coaching import ECoaching from psa_connectedcar.models.e_coaching_links import ECoachingLinks from psa_connectedcar.models.e_coaching_scores import ECoachingScores from psa_connectedcar.models.energy import Energy +from psa_connectedcar.models.energy_battery import EnergyBattery +from psa_connectedcar.models.energy_battery_health import EnergyBatteryHealth +from psa_connectedcar.models.energy_charging import EnergyCharging from psa_connectedcar.models.engine import Engine from psa_connectedcar.models.engine_oil import EngineOil from psa_connectedcar.models.environment import Environment diff --git a/psa_connectedcar/models/__init__.py b/psa_connectedcar/models/__init__.py index 94991ed..757a026 100644 --- a/psa_connectedcar/models/__init__.py +++ b/psa_connectedcar/models/__init__.py @@ -46,6 +46,9 @@ from psa_connectedcar.models.e_coaching import ECoaching from psa_connectedcar.models.e_coaching_links import ECoachingLinks from psa_connectedcar.models.e_coaching_scores import ECoachingScores from psa_connectedcar.models.energy import Energy +from psa_connectedcar.models.energy_battery import EnergyBattery +from psa_connectedcar.models.energy_battery_health import EnergyBatteryHealth +from psa_connectedcar.models.energy_charging import EnergyCharging from psa_connectedcar.models.engine import Engine from psa_connectedcar.models.engine_oil import EngineOil from psa_connectedcar.models.environment import Environment diff --git a/psa_connectedcar/models/energy.py b/psa_connectedcar/models/energy.py index 7aec515..bafcaca 100644 --- a/psa_connectedcar/models/energy.py +++ b/psa_connectedcar/models/energy.py @@ -31,15 +31,243 @@ class Energy(object): and the value is json key in definition. """ swagger_types = { + 'updated_at': 'datetime', + 'autonomy': 'float', + 'battery': 'EnergyBattery', + 'charging': 'EnergyCharging', + 'consumption': 'float', + 'level': 'float', + 'residual': 'float', + 'type': 'str' } attribute_map = { + 'updated_at': 'updatedAt', + 'autonomy': 'autonomy', + 'battery': 'battery', + 'charging': 'charging', + 'consumption': 'consumption', + 'level': 'level', + 'residual': 'residual', + 'type': 'type' } - def __init__(self): # noqa: E501 + def __init__(self, updated_at=None, autonomy=None, battery=None, charging=None, consumption=None, level=None, residual=None, type=None): # noqa: E501 """Energy - a model defined in Swagger""" # noqa: E501 + + self._updated_at = None + self._autonomy = None + self._battery = None + self._charging = None + self._consumption = None + self._level = None + self._residual = None + self._type = None self.discriminator = None + if updated_at is not None: + self.updated_at = updated_at + if autonomy is not None: + self.autonomy = autonomy + if battery is not None: + self.battery = battery + if charging is not None: + self.charging = charging + if consumption is not None: + self.consumption = consumption + if level is not None: + self.level = level + if residual is not None: + self.residual = residual + if type is not None: + self.type = type + + @property + def updated_at(self): + """Gets the updated_at of this Energy. # noqa: E501 + + Date when the resource has been updated. # noqa: E501 + + :return: The updated_at of this Energy. # noqa: E501 + :rtype: datetime + """ + return self._updated_at + + @updated_at.setter + def updated_at(self, updated_at): + """Sets the updated_at of this Energy. + + Date when the resource has been updated. # noqa: E501 + + :param updated_at: The updated_at of this Energy. # noqa: E501 + :type: datetime + """ + + self._updated_at = updated_at + + @property + def autonomy(self): + """Gets the autonomy of this Energy. # noqa: E501 + + Vehicle autonomy for this energy class expressed in KM. # noqa: E501 + + :return: The autonomy of this Energy. # noqa: E501 + :rtype: float + """ + return self._autonomy + + @autonomy.setter + def autonomy(self, autonomy): + """Sets the autonomy of this Energy. + + Vehicle autonomy for this energy class expressed in KM. # noqa: E501 + + :param autonomy: The autonomy of this Energy. # noqa: E501 + :type: float + """ + + self._autonomy = autonomy + + @property + def battery(self): + """Gets the battery of this Energy. # noqa: E501 + + + :return: The battery of this Energy. # noqa: E501 + :rtype: EnergyBattery + """ + return self._battery + + @battery.setter + def battery(self, battery): + """Sets the battery of this Energy. + + + :param battery: The battery of this Energy. # noqa: E501 + :type: EnergyBattery + """ + + self._battery = battery + + @property + def charging(self): + """Gets the charging of this Energy. # noqa: E501 + + + :return: The charging of this Energy. # noqa: E501 + :rtype: EnergyCharging + """ + return self._charging + + @charging.setter + def charging(self, charging): + """Sets the charging of this Energy. + + + :param charging: The charging of this Energy. # noqa: E501 + :type: EnergyCharging + """ + + self._charging = charging + + @property + def consumption(self): + """Gets the consumption of this Energy. # noqa: E501 + + Instant consumption for thermic vehicles. # noqa: E501 + + :return: The consumption of this Energy. # noqa: E501 + :rtype: float + """ + return self._consumption + + @consumption.setter + def consumption(self, consumption): + """Sets the consumption of this Energy. + + Instant consumption for thermic vehicles. # noqa: E501 + + :param consumption: The consumption of this Energy. # noqa: E501 + :type: float + """ + + self._consumption = consumption + + @property + def level(self): + """Gets the level of this Energy. # noqa: E501 + + + :return: The level of this Energy. # noqa: E501 + :rtype: float + """ + return self._level + + @level.setter + def level(self, level): + """Sets the level of this Energy. + + + :param level: The level of this Energy. # noqa: E501 + :type: float + """ + if level is not None and level > 100: # noqa: E501 + raise ValueError("Invalid value for `level`, must be a value less than or equal to `100`") # noqa: E501 + if level is not None and level < 0: # noqa: E501 + raise ValueError("Invalid value for `level`, must be a value greater than or equal to `0`") # noqa: E501 + + self._level = level + + @property + def residual(self): + """Gets the residual of this Energy. # noqa: E501 + + Residual electric energy avaialble only for electric energy class expressed in KWh. # noqa: E501 + + :return: The residual of this Energy. # noqa: E501 + :rtype: float + """ + return self._residual + + @residual.setter + def residual(self, residual): + """Sets the residual of this Energy. + + Residual electric energy avaialble only for electric energy class expressed in KWh. # noqa: E501 + + :param residual: The residual of this Energy. # noqa: E501 + :type: float + """ + + self._residual = residual + + @property + def type(self): + """Gets the type of this Energy. # noqa: E501 + + + :return: The type of this Energy. # noqa: E501 + :rtype: str + """ + return self._type + + @type.setter + def type(self, type): + """Sets the type of this Energy. + + + :param type: The type of this Energy. # noqa: E501 + :type: str + """ + allowed_values = ["Fuel", "Electric"] # noqa: E501 + if type not in allowed_values: + raise ValueError( + "Invalid value for `type` ({0}), must be one of {1}" # noqa: E501 + .format(type, allowed_values) + ) + + self._type = type + def to_dict(self): """Returns the model properties as a dict""" result = {} diff --git a/requirements.txt b/requirements.txt index da99b12..840c015 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,9 +11,10 @@ argparse flask dash_bootstrap_components geojson +reverse_geocode #swagger req certifi >= 14.05.14 six >= 1.10 python_dateutil >= 2.5.3 -urllib3 >= 1.15.1 \ No newline at end of file +urllib3 >= 1.15.1 diff --git a/web/callback.py b/web/callback.py index 1141719..192afe6 100644 --- a/web/callback.py +++ b/web/callback.py @@ -16,12 +16,15 @@ from web.app import app, dash_app, myp, chc import web.db trips = None +chargings = None + @dash_app.callback(Output('trips_map', 'figure'), Output('consumption_fig', 'figure'), Output('consumption_fig_by_speed', 'figure'), Output('consumption', 'children'), Output('tab_trips', 'children'), + Output('tab_battery', 'children'), Input('date-slider', 'value')) def display_value(value): min = datetime.fromtimestamp(value[0], tz=timezone.utc) @@ -30,7 +33,8 @@ def display_value(value): for trip in trips: if min <= trip.start_at <= max: filtered_trips.append(trip) - figures.get_figures(filtered_trips) + filtered_chargings = MyPSACC.get_chargings(min,max) + figures.get_figures(filtered_trips,filtered_chargings) 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 @@ -107,23 +111,25 @@ def after_request(response): def update_trips(): - global trips - logger.info("update_trips") + global trips, chargings + logger.info("update_data") try: trips = MyPSACC.get_trips() + chargings = MyPSACC.get_chargings() except: - logger.error("update_trips: "+traceback.format_exc()) + logger.error("update_trips: " + traceback.format_exc()) try: web.db.callback_fct = update_trips update_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) + figures.get_figures(trips, chargings) data_div = html.Div([dcc.RangeSlider( id='date-slider', min=min_millis, @@ -142,6 +148,7 @@ try: 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="Battery", tab_id="battery", id="tab_battery", children=[figures.battery_info]), dbc.Tab(label="Map", tab_id="map", children=[ dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]), ], @@ -159,4 +166,3 @@ dash_app.layout = dbc.Container(fluid=True, children=[ html.H1('My car info'), data_div ]) - diff --git a/web/db.py b/web/db.py index 4246542..b8d3176 100644 --- a/web/db.py +++ b/web/db.py @@ -19,8 +19,8 @@ def get_db(): conn.row_factory = sqlite3.Row conn.execute("CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY, VIN TEXT, longitude REAL, " "latitude REAL, mileage REAL, level INTEGER);") - conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME, start_level, " - "end_level)") + conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME,VIN TEXT, " + "start_level INTEGER, end_level INTEGER, co2 INTEGER, kw INTEGER);") 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;") diff --git a/web/figures.py b/web/figures.py index 6fac114..798464b 100644 --- a/web/figures.py +++ b/web/figures.py @@ -1,6 +1,7 @@ from copy import deepcopy from typing import List +import dash_bootstrap_components as dbc import dash_table import numpy as np from dash_table.Format import Format, Scheme, Symbol @@ -46,10 +47,11 @@ consumption_fig_by_speed = None table_fig = None pandas_options.display.float_format = '${:.2f}'.format info = "" +battery_info = dbc.Alert("No data to show", color="danger") -def get_figures(trips: List[Trip]): - global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info +def get_figures(trips: List[Trip], charging: List[dict]): + global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info, battery_info lats = [] lons = [] names = [] @@ -94,5 +96,15 @@ def get_figures(trips: List[Trip]): 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 kWh/100Km") - info = "Average consumption: {:.1f} kW/100km".format( - float(consumption_df.mean(numeric_only=True))) + kw_per_km = float(consumption_df.mean(numeric_only=True)) + info = "Average consumption: {:.1f} kW/100km".format(kw_per_km) + + # charging + charging_data = DataFrame.from_records(charging) + co2_per_kw = charging_data["co2"].sum() / charging_data["kw"].sum() + co2_per_km = co2_per_kw * kw_per_km / 100 + charge_speed = 3600 * charging_data["kw"].mean() / \ + (charging_data["stop_at"] - charging_data["start_at"]).mean().total_seconds() + battery_info = "Average gC02/kW: {:.1f}\n" \ + "Average gC02/km: {:1f}\n" \ + "Average Charge SPEED {:1f} gC02/kW".format(co2_per_kw, co2_per_km, charge_speed)