diff --git a/Car.py b/Car.py index a857c78..68e3c49 100644 --- a/Car.py +++ b/Car.py @@ -2,66 +2,33 @@ import json from copy import copy from MyLogger import logger +from libs.car_model import CarModel +from libs.car_status import CarStatus -ENERGY_CAPACITY = {'SUV 3008': {'BATTERY_POWER': 10.8, 'FUEL_CAPACITY': 43}, - 'C5 Aircross': {'BATTERY_POWER': 10.8, 'FUEL_CAPACITY': 43}, - 'e-208': {'BATTERY_POWER': 46, 'FUEL_CAPACITY': 0, "ABRP_NAME": "peugeot:e208:20:50"}, - 'e-2008': {'BATTERY_POWER': 46, 'FUEL_CAPACITY': 0, "ABRP_NAME": "peugeot:e2008:20:48"}, - 'corsa-e': {'BATTERY_POWER': 46, 'FUEL_CAPACITY': 0, "ABRP_NAME": "opel:corsae:20:50"} - } -DEFAULT_BATTERY_POWER = 46 -DEFAULT_FUEL_CAPACITY = 0 -DEFAULT_MAX_ELEC_CONSUMPTION = 70 -DEFAULT_MAX_FUEL_CONSUMPTION = 30 -DEFAULT_ABRP_NAME = "peugeot:e208:20:50" CARS_FILE = "cars.json" class Car: - def __init__(self, vin, vehicle_id, brand, label="unknown", battery_power=None, fuel_capacity=None, - max_elec_consumption=None, max_fuel_consumption=None): + def __init__(self, vin, vehicle_id, brand, label=None, battery_power=None, fuel_capacity=None, + max_elec_consumption=None, max_fuel_consumption=None, abrp_name=None): self.vin = vin + if label is not None: + model = CarModel.find_model_by_name(label) + else: + model = CarModel.find_model_by_vin(self.vin) + label = model.name self.vehicle_id = vehicle_id self.label = label self.brand = brand - self.battery_power = None - self.fuel_capacity = None - self.max_elec_consumption = 0 # kwh/100Km - self.max_fuel_consumption = 0 # L/100Km - self.set_energy_capacity(battery_power, fuel_capacity, max_elec_consumption, max_fuel_consumption) - self.status = None + self._status = None + self.abrp_name = abrp_name or model.abrp_name + self.battery_power = battery_power or model.battery_power + self.fuel_capacity = fuel_capacity or model.fuel_capacity + self.max_elec_consumption = max_elec_consumption or model.max_elec_consumption # kwh/100Km + self.max_fuel_consumption = max_fuel_consumption or model.max_fuel_consumption # L/100Km - def set_energy_capacity(self, battery_power=None, fuel_capacity=None, max_elec_consumption=None, - max_fuel_consumption=None): - if battery_power is not None and fuel_capacity is not None: - self.battery_power = battery_power - self.fuel_capacity = fuel_capacity - elif self.__get_model_name() is not None: - model_name = self.__get_model_name() - self.battery_power = ENERGY_CAPACITY[model_name]["BATTERY_POWER"] - self.fuel_capacity = ENERGY_CAPACITY[model_name]["FUEL_CAPACITY"] - else: - logger.warning("Can't get car model please check %s", CARS_FILE) - self.battery_power = DEFAULT_BATTERY_POWER - self.fuel_capacity = DEFAULT_FUEL_CAPACITY - if self.is_electric(): - self.max_fuel_consumption = 0 - else: - self.max_fuel_consumption = max_fuel_consumption or DEFAULT_MAX_FUEL_CONSUMPTION - if self.is_thermal(): - self.max_elec_consumption = 0 - else: - self.max_elec_consumption = max_elec_consumption or DEFAULT_MAX_ELEC_CONSUMPTION - - def __get_model_name(self): - if self.label in ENERGY_CAPACITY: - return self.label - if self.__is_opel_corsa(): - return "corsa-e" - return None - - def __is_opel_corsa(self): - return self.brand == "C" and self.label is None + def set_model_name(self, name): + self.label = name def is_electric(self) -> bool: return self.fuel_capacity == 0 and self.battery_power > 0 @@ -84,12 +51,27 @@ class Car: def to_dict(self): car_dict = copy(self.__dict__) - car_dict.pop("status") + car_dict.pop("_status") return car_dict def __str__(self): return str(self.to_dict()) + def get_abrp_name(self): + if self.abrp_name is not None: + return self.abrp_name + raise ValueError("ABRP model is not set") + + @property + def status(self): + return self._status + + @status.setter + def status(self, value: CarStatus): + self._status = value + if self._status is not None and self.status.__class__ != CarStatus: + self._status.__class__ = CarStatus + self._status.correct() class Cars(list): def __init__(self, *args): @@ -129,7 +111,9 @@ class Cars(list): try: with open(name, "r") as f: json_str = f.read() - return Cars.from_json(json.loads(json_str)) + cars=Cars.from_json(json.loads(json_str)) + cars.save_cars() + return cars except (FileNotFoundError, TypeError) as e: logger.debug(e) return Cars() diff --git a/MyPSACC.py b/MyPSACC.py index 1d7b6ab..6b7deae 100644 --- a/MyPSACC.py +++ b/MyPSACC.py @@ -3,7 +3,6 @@ import re import threading import traceback import uuid -from copy import copy from datetime import datetime from http import HTTPStatus from json import JSONEncoder @@ -24,7 +23,8 @@ from psa_connectedcar.rest import ApiException from MyLogger import logger from utils import get_temp, rate_limit -from web.db import get_db, clean_position +from web.abrp import Abrp +from web.db import get_db, clean_position, get_last_temp from geojson import Feature, Point, FeatureCollection from geojson import dumps as geo_dumps @@ -48,17 +48,9 @@ MQTT_RESP_TOPIC = "psa/RemoteServices/to/cid/" MQTT_EVENT_TOPIC = "psa/RemoteServices/events/MPHRTServices/" MQTT_TOKEN_TTL = 890 CARS_FILE = "cars.json" +DEFAULT_CONFIG_FILENAME = "config.json" -# add method to class Energy -def get_energy(self, energy_type): - for energy in self._energy: - if energy.type == energy_type: - return energy - return psac.models.energy.Energy(charging=psac.models.energy_charging.EnergyCharging()) - - -psac.models.status.Status.get_energy = get_energy class OpenIdCredentialManager(CredentialManager): @@ -128,13 +120,11 @@ def gen_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) def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token, customer_id, realm, country_code, - proxies=None, weather_api=None): + proxies=None, weather_api=None, abrp=None): self.realm = realm self.service_information = ServiceInformation(authorize_service, realm_info[self.realm]['oauth_url'], @@ -149,7 +139,6 @@ class MyPSACC: self.remote_refresh_token = remote_refresh_token self.remote_access_token = None self.vehicles_list = Cars.load_cars(CARS_FILE) - self.set_proxies(proxies) self.customer_id = customer_id self._config_hash = None self.api_config.verify_ssl = False @@ -169,12 +158,19 @@ class MyPSACC: self.precond_programs = {} self.info_callback = [] self.info_refresh_rate = 120 + if abrp is None: + self.abrp = Abrp() + else: + self.abrp: Abrp = Abrp(**abrp) + self.set_proxies(proxies) + self.config_file = DEFAULT_CONFIG_FILENAME def get_app_name(self): return realm_info[self.realm]['app_name'] def refresh_token(self): self.manager._refresh_token() + self.save_config() def api(self) -> psac.VehiclesApi: self.api_config.access_token = self.manager._access_token @@ -188,6 +184,7 @@ class MyPSACC: else: self._proxies = proxies self.api_config.proxy = proxies['http'] + self.abrp.proxies = proxies self.manager.proxies = self._proxies Otp.set_proxies(proxies) @@ -198,9 +195,10 @@ class MyPSACC: try: res = self.api().get_vehicle_status(car.vehicle_id, extension=["odometer"]) if res is not None: + car.status = res if self._record_enabled: - self.record_info(vin, res) - break + self.record_info(car) + return res except ApiException: logger.error(traceback.format_exc()) car.status = res @@ -210,7 +208,7 @@ class MyPSACC: if self.info_refresh_rate is not None: while True: sleep(self.info_refresh_rate) - logger.info("refresh_vehicle_info") + logger.debug("refresh_vehicle_info") for car in self.vehicles_list: self.get_vehicle_info(car.vin) for callback in self.info_callback: @@ -276,7 +274,7 @@ class MyPSACC: last_update: datetime = self.remote_token_last_update if (datetime.now() - last_update).total_seconds() < MQTT_TOKEN_TTL: return None - self.manager._refresh_token() + self.refresh_token() if self.remote_refresh_token is None: logger.error("remote_refresh_token isn't defined") self.load_otp(force_new=True) @@ -295,6 +293,7 @@ class MyPSACC: otp_code = self.get_otp_code() res = self.get_remote_access_token(otp_code) self.mqtt_client.username_pw_set("IMA_OAUTH_ACCESS_TOKEN", self.remote_access_token) + self.save_config() return res def on_mqtt_connect(self, client, userdata, rc, a): @@ -471,7 +470,9 @@ class MyPSACC: self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/ThermalPrecond", msg) return True - def save_config(self, name="config.json", force=False): + def save_config(self, name=None, force=False): + if name is None: + name = self.config_file config_str = json.dumps(self, cls=MyPeugeotEncoder, sort_keys=True, indent=4).encode("utf8") new_hash = md5(config_str).hexdigest() if force or self._config_hash != new_hash: @@ -487,37 +488,35 @@ class MyPSACC: config = dict(**json.loads(config_str)) if "country_code" not in config: config["country_code"] = input("What is your country code ? (ex: FR, GB, DE, ES...)\n") - return MyPSACC(**config) + if "abrp" not in config: + config["abrp"] = None + psacc = MyPSACC(**config) + psacc.config_file = name + return psacc def set_record(self, value: bool): self._record_enabled = value - def record_info(self, vin, status: psac.models.status.Status): - mileage = status.timed_odometer.mileage - level = status.get_energy('Electric').level - level_fuel = status.get_energy('Fuel').level - charge_date = status.get_energy('Electric').updated_at - try: - moving = status.kinetic.moving - logger.debug("") - except AttributeError: - logger.error("kinetic not available from api") - moving = None - try: - longitude = status.last_position.geometry.coordinates[0] - latitude = status.last_position.geometry.coordinates[1] - date = status.last_position.properties.updated_at - except AttributeError: - logger.error("last_position not available from api") - longitude = latitude = None + def record_info(self, car: Car): + mileage = car.status.timed_odometer.mileage + level = car.status.get_energy('Electric').level + level_fuel = car.status.get_energy('Fuel').level + charge_date = car.status.get_energy('Electric').updated_at + moving = car.status.kinetic.moving + + longitude = car.status.last_position.geometry.coordinates[0] + latitude = car.status.last_position.geometry.coordinates[1] + date = car.status.last_position.properties.updated_at + if date is None: date = charge_date logger.debug("vin:%s longitude:%s latitude:%s date:%s mileage:%s level:%s charge_date:%s level_fuel:" - "%s moving:%s", vin, longitude, latitude, date, mileage, level, charge_date, level_fuel, + "%s moving:%s", car.vin, longitude, latitude, date, mileage, level, charge_date, level_fuel, moving) - self.record_position(vin, mileage, latitude, longitude, date, level, level_fuel, moving) + self.record_position(car.vin, mileage, latitude, longitude, date, level, level_fuel, moving) + self.abrp.call(car, get_last_temp(car.vin)) try: - charging_status = status.get_energy('Electric').charging.status - self.record_charging(vin, charging_status, charge_date, level, latitude, longitude) + charging_status = car.status.get_energy('Electric').charging.status + self.record_charging(car.vin, charging_status, charge_date, level, latitude, longitude) logger.debug("charging_status:%s ", charging_status) except AttributeError: logger.error("charging status not available from api") @@ -547,8 +546,9 @@ class MyPSACC: conn.commit() logger.info("new position recorded for %s", vin) clean_position(conn) - else: - logger.debug("position already saved") + return True + logger.debug("position already saved") + return False def record_charging(self, vin, charging_status, charge_date, level, latitude, longitude): conn = get_db() @@ -609,12 +609,15 @@ class MyPSACC: 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 class MyPeugeotEncoder(JSONEncoder): def default(self, mp: MyPSACC): - data = copy(mp.__dict__) + data = dict(mp) mpd = {"proxies": data["_proxies"], "refresh_token": mp.manager.refresh_token, - "client_secret": mp.service_information.client_secret} + "client_secret": mp.service_information.client_secret, "abrp":dict(mp.abrp)} for el in ["client_id", "realm", "remote_refresh_token", "customer_id", "weather_api", "country_code"]: mpd[el] = data[el] return mpd diff --git a/README.md b/README.md index d00097f..f1f378a 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,8 @@ We will retrieve these informations: ## FAQ If you have a problem, or a question please check if the answer isn't in the [FAQ](FAQ.md). - +## Connect to A better Route Planner +You can connect the app to ABRP, see [this page](docs/abrp.md) ## API documentation The api documentation is described here : [api_spec.md](api_spec.md). You can use all functions from the doc, for example : diff --git a/docs/abrp.md b/docs/abrp.md new file mode 100644 index 0000000..8a67a5d --- /dev/null +++ b/docs/abrp.md @@ -0,0 +1,39 @@ +## Connect to A better Route Planner + Thank's to this you will no longer have to edit manually: +- Exterior temperature +- Autonomy left +- Consumption + +ABRP will optimize your itinerary with these parameters. + +### Prerequisite +1. A working last version of psa_car_controller +2. A abetterrouteplanner account +3. label must be set in cars.json + +### Procedure +1. Go to [https://abetterrouteplanner.com/](https://abetterrouteplanner.com/) +2. Edit the parameter of your car, you should have something like this: +![](abrp.png) +3. Click on Generic Link +4. Click on copy +5. Open the following url after replacing YOURTOKEN by the value you just copied: +``` +http://localhost:5000/abrp?token=YOURTOKEN +``` +7. Go to [http://localhost:5000](http://localhost:5000) +8. Click on control tab +9. Enable ABRP for your car +10. Open cars.json +11. If abrp_name is null: + + 11.1 Get the name from this list : [model list](https://api.iternio.com/1/tlm/get_carmodels_list?api_key=32b2162f-9599-4647-8139-66e9f9528370) + + 11.2 stop the psa_car_controller + + 11.3 replace null with the correct name + + 11.4 save the file + + 11.5 restart psa_car_controller +12. Enjoy \ No newline at end of file diff --git a/docs/abrp.png b/docs/abrp.png new file mode 100644 index 0000000..df1d949 Binary files /dev/null and b/docs/abrp.png differ diff --git a/libs/car_model.py b/libs/car_model.py new file mode 100644 index 0000000..6f9957b --- /dev/null +++ b/libs/car_model.py @@ -0,0 +1,55 @@ +import re + +from MyLogger import logger + +DEFAULT_BATTERY_POWER = 46 +DEFAULT_FUEL_CAPACITY = 0 +DEFAULT_MAX_ELEC_CONSUMPTION = 70 +DEFAULT_MAX_FUEL_CONSUMPTION = 30 + + +class CarModel: + def __init__(self, name, battery_power, fuel_capacity, abrp_name=None, reg=None, + max_elec_consumption=DEFAULT_MAX_ELEC_CONSUMPTION, max_fuel_consumption=DEFAULT_MAX_FUEL_CONSUMPTION): + self.name = name + self.battery_power = battery_power + self.fuel_capacity = fuel_capacity + self.abrp_name = abrp_name + self.reg = reg + self.max_elec_consumption = max_elec_consumption + self.max_fuel_consumption = max_fuel_consumption + + def match(self, vin): + return re.match(self.reg, vin) is not None + + @staticmethod + def find_model_by_vin(vin): + for carmodel in carmodels: + if carmodel.match(vin): + return carmodel + logger.warning("Can't get car model, please report an issue on github with your car model" + " and first ten letter of your VIN") + return CarModel("unknown", DEFAULT_BATTERY_POWER, DEFAULT_FUEL_CAPACITY) + + @staticmethod + def find_model_by_name(name): + for carmodel in carmodels: + if carmodel.name == name: + return carmodel + return None + + +class ElecModel(CarModel): + def __init__(self, name, battery_power, abrp_name=None, reg=None, + max_elec_consumption=DEFAULT_MAX_ELEC_CONSUMPTION): + super().__init__(name, battery_power, 0, abrp_name, reg, max_elec_consumption, 0) + + +carmodels = [ + ElecModel("e-208", 46, "peugeot:e208:20:50", r"VR3UHZKX.*"), + ElecModel("e-2008", 46, "peugeot:e2008:20:48", r"VR3UKZKX.*"), + ElecModel("e-Spacetourer", 46, "peugeot:etraveler:21:50:citroen", r"VF7VZZKX.*"), + ElecModel("corsa-e", 46, "opel:corsae:20:50", r"VXKUHZKX.*"), + CarModel("SUV 3008", 10.8, 43), + CarModel("C5 Aircross", 10.8, 43) +] diff --git a/libs/car_status.py b/libs/car_status.py new file mode 100644 index 0000000..82ebea4 --- /dev/null +++ b/libs/car_status.py @@ -0,0 +1,37 @@ +from MyLogger import logger +from psa_connectedcar import Position, Geometry, PositionProperties, Kinetic, Energy, EnergyCharging, Status + + +class CarStatus(Status): + def __init__(self, embedded=None, links=None, battery=None, doors_state=None, energy=None, environment=None, + ignition=None, kinetic=None, last_position=None, preconditionning=None, privacy=None, safety=None, + service=None, timed_odometer=None): # noqa: E501 + super().__init__(embedded, links, battery, doors_state, energy, environment, ignition, kinetic, last_position, + preconditionning, privacy, safety, service, timed_odometer) + self.correct() + + def correct(self): + try: + if len(self.last_position.geometry.coordinates) < 2: + raise AttributeError() + if len(self.last_position.geometry.coordinates) < 3: + # set altitude none + self.last_position.geometry.coordinates.append(None) + except AttributeError: + self.last_position = Position(geometry=Geometry(coordinates=[None, None, None], type="Point"), + properties=PositionProperties(updated_at=None)) + if self.kinetic is None: + self.kinetic = Kinetic() + + def is_moving(self): + try: + return self.kinetic.moving + except AttributeError: + logger.error("kinetic not available from api") + return None + + def get_energy(self, energy_type) -> Energy: + for energy in self._energy: + if energy.type == energy_type: + return energy + return Energy(charging=EnergyCharging()) diff --git a/otp/Otp.py b/otp/Otp.py index 21fa158..77d0539 100644 --- a/otp/Otp.py +++ b/otp/Otp.py @@ -189,7 +189,6 @@ class Otp: return False def activation_finalyze(self, random_bytes=None): - R = self.get_r() params = {"action": "ActionFinalize", "mode": self.mode, "id": self.data.iwid, "lastsync": self.data.iwTsync, "version": "Generator-1.0/0.2.11", diff --git a/requirements.txt b/requirements.txt index 15421cf..ddc8c37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ paho-mqtt>=1.5.0 dash>=1.18.0 +dash_daq plotly>=4 cryptography>=3.0 Werkzeug>=1.0.0 diff --git a/server.py b/server.py index 4f4a8da..fdc2dcc 100755 --- a/server.py +++ b/server.py @@ -61,7 +61,7 @@ if __name__ == "__main__": logger.info("offline mode") else: try: - web.app.myp.manager._refresh_token() + web.app.myp.refresh_token() except OAuthError: if args.mail and args.password: client_email = args.mail diff --git a/web/abrp.py b/web/abrp.py new file mode 100644 index 0000000..f45fabd --- /dev/null +++ b/web/abrp.py @@ -0,0 +1,51 @@ +import json +import traceback +from datetime import datetime + +import requests + +from Car import Car +from MyLogger import logger + + +class Abrp: + api_key = "1e28ad14-df16-49f0-97da-364c9154b44a" + url = "https://api.iternio.com/1/tlm/send" + + def __init__(self, token: str = "", abrp_enable_vin=None): + if abrp_enable_vin is None: + abrp_enable_vin = list() + self.token = token + self.abrp_enable_vin = set(abrp_enable_vin) + self.proxies = None + + def call(self, car: Car, ext_temp: float = None): + try: + if self.token is None or len(self.token) == 0: + logger.error("No token provided") + elif car.vin in self.abrp_enable_vin: + energy = car.status.get_energy('Electric') + tlm = {"utc": int(datetime.timestamp(energy.updated_at)), + "soc": energy.level, + "speed": getattr(car.status.kinetic, "speed", None), + "car_model": car.get_abrp_name(), + "current": car.status.battery.current, + "is_charging": energy.charging.status == "InProgress", + "lat": car.status.last_position.geometry.coordinates[1], + "lon": car.status.last_position.geometry.coordinates[0], + "power": energy.consumption + } + if ext_temp is not None: + tlm["ext_temp"] = ext_temp + params = {"tlm": json.dumps(tlm), "token": self.token, "api_key": self.api_key} + response = requests.request("POST", self.url, params=params, proxies=self.proxies, + verify=self.proxies is None) + logger.debug(response.text) + return response.json()["status"] == "ok" + except (AttributeError, IndexError, ValueError): + logger.error(traceback.format_exc()) + return False + + def __iter__(self): + yield "abrp_enable_vin", list(self.abrp_enable_vin) + yield "token", self.token diff --git a/web/db.py b/web/db.py index d54bac8..a8cc21d 100644 --- a/web/db.py +++ b/web/db.py @@ -46,3 +46,12 @@ def clean_position(conn): logger.debug("Delete duplicate line") conn.execute("DELETE FROM position where Timestamp=?;", (res[1]["Timestamp"],)) conn.commit() + + +def get_last_temp(vin): + conn = get_db() + res = conn.execute("SELECT temperature FROM position WHERE VIN=? ORDER BY Timestamp DESC limit 1", + (vin,)).fetchone() + if res is None: + return None + return res[0] diff --git a/web/figures.py b/web/figures.py index 1dbda8a..485b626 100644 --- a/web/figures.py +++ b/web/figures.py @@ -173,10 +173,10 @@ def get_figures(trips: Trips, charging: Tuple[dict]): y=consumption_by_temp_df["consumption_km"], name="Trips")) consumption_fig_by_temp.update_layout(xaxis_title="average temperature in °C", yaxis_title="Consumption kWh/100Km") - consumption_graph_by_temp = Graph(figure=consumption_fig_by_temp, id="consumption_fig_by_temp") + consumption_graph_by_temp = html.Div(Graph(figure=consumption_fig_by_temp), id="consumption_graph_by_temp") else: - consumption_graph_by_temp = Graph(style={'display': 'none'}) + consumption_graph_by_temp = html.Div(Graph(style={'display': 'none'}), id="consumption_graph_by_temp") def __calculate_co2_per_kw(charging_data): diff --git a/web/views.py b/web/views.py index 45d30e5..313725b 100644 --- a/web/views.py +++ b/web/views.py @@ -2,9 +2,10 @@ import json import traceback from datetime import datetime, timezone import dash_bootstrap_components as dbc -from dash.dependencies import Output, Input +from dash.dependencies import Output, Input, MATCH import dash_core_components as dcc import dash_html_components as html +import dash_daq as daq from MyLogger import logger from flask import jsonify, request, Response as FlaskResponse @@ -16,6 +17,12 @@ from web import figures from web.app import app, dash_app, myp, chc import web.db +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 @@ -25,7 +32,7 @@ min_date = max_date = min_millis = max_millis = step = marks = cached_layout = N @dash_app.callback(Output('trips_map', 'figure'), Output('consumption_fig', 'figure'), Output('consumption_fig_by_speed', 'figure'), - Output('consumption_fig_by_temp', 'graph'), + Output('consumption_graph_by_temp', 'children'), Output('consumption', 'children'), Output('tab_trips', 'children'), Output('tab_battery', 'children'), @@ -49,6 +56,19 @@ def display_value(value): figures.battery_table, max_millis, step, marks +@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')) +def update_abrp(div_id, value): + vin = div_id["vin"] + if value: + myp.abrp.abrp_enable_vin.add(vin) + else: + myp.abrp.abrp_enable_vin.discard(vin) + myp.save_config() + return " " + + @app.route('/get_vehicles') def get_vehicules(): response = app.response_class( @@ -127,6 +147,21 @@ def get_recorded_position(): return FlaskResponse(myp.get_recorded_position(), mimetype='application/json') +@app.route('/abrp') +def abrp(): + vin = request.args.get('vin', None) + enable = request.args.get('enable', None) + token = request.args.get('token', None) + if vin is not None and enable is not None: + if enable == '1': + myp.abrp.abrp_enable_vin.add(vin) + else: + myp.abrp.abrp_enable_vin.discard(vin) + if token is not None: + myp.abrp.token = token + return jsonify(dict(myp.abrp)) + + @app.after_request def after_request(response): header = response.headers @@ -160,6 +195,25 @@ def update_trips(): logger.error("update_trips (slider): %s", traceback.format_exc()) return + +def __get_control_tabs(): + tabs = [] + for car in myp.vehicles_list: + if car.label is None: + label = car.vin + else: + label = car.label + tabs.append(dbc.Tab(label=label, id="tab-" + car.vin, children=[ + daq.ToggleSwitch( + id={'role': ABRP_SWITCH, 'vin': car.vin}, + value=car.vin in myp.abrp.abrp_enable_vin, + label="Send data to ABRP" + ), + html.Div(id={'role': ABRP_SWITCH + RESPONSE, 'vin': car.vin}) + ])) + return tabs + + def serve_layout(): global cached_layout if cached_layout is None: @@ -188,13 +242,14 @@ def serve_layout(): dbc.Tab(label="Charge", tab_id="charge", id="tab_charge", children=[figures.battery_table]), dbc.Tab(label="Map", tab_id="map", children=[ dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]), + dbc.Tab(label="Control", tab_id="control", children=dbc.Tabs(id="control-tabs", + children=__get_control_tabs())) ], id="tabs", active_tab="summary", ), - html.Div(id="tab-content", className="p-4"), + html.Div(id=EMPTY_DIV), ])]) - except (IndexError, TypeError, NameError): logger.warning("Failed to generate figure, there is probably not enough data yet") logger.debug(traceback.format_exc())