Feature abrp (#79)

* add ABRP connection

* update doc

* fix get_last_temp()

* add image

* update abrp

* fix abrpname

* fix no last postition

* add type point

* find model by vin

* add Spacetourer

* fix indent
This commit is contained in:
Florian BEZANNIER
2021-04-13 12:48:37 +02:00
committed by GitHub
parent e2327768b5
commit 25b86c0149
14 changed files with 342 additions and 108 deletions
+36 -52
View File
@@ -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()
+50 -47
View File
@@ -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
+2 -1
View File
@@ -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 :
+39
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

+55
View File
@@ -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)
]
+37
View File
@@ -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())
-1
View File
@@ -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",
+1
View File
@@ -1,5 +1,6 @@
paho-mqtt>=1.5.0
dash>=1.18.0
dash_daq
plotly>=4
cryptography>=3.0
Werkzeug>=1.0.0
+1 -1
View File
@@ -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
+51
View File
@@ -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
+9
View File
@@ -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]
+2 -2
View File
@@ -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):
+59 -4
View File
@@ -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())