code clean up and reduce algo complexity

This commit is contained in:
Florian Bezannier
2021-03-21 19:31:26 +01:00
parent 25c61433b8
commit c3adb194a3
14 changed files with 387 additions and 319 deletions
+30 -10
View File
@@ -10,20 +10,26 @@ ENERGY_CAPACITY = {'SUV 3008': {'BATTERY_POWER': 10.8, 'FUEL_CAPACITY': 43},
}
DEFAULT_BATTERY_POWER = 46
DEFAULT_FUEL_CAPACITY = 0
DEFAULT_MAX_ELEC_CONSUMPTION = 70
DEFAULT_MAX_FUEL_CONSUMPTION = 30
CARS_FILE = "cars.json"
class Car:
def __init__(self, vin, vehicle_id, brand, label="unknown", battery_power=None, fuel_capacity=None):
def __init__(self, vin, vehicle_id, brand, label="unknown", battery_power=None, fuel_capacity=None,
max_elec_consumption=None, max_fuel_consumption=None):
self.vin = vin
self.vehicle_id = vehicle_id
self.label = label
self.brand = brand
self.battery_power = None
self.fuel_capacity = None
self.set_energy_capacity(battery_power, fuel_capacity)
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
def set_energy_capacity(self, battery_power = None, fuel_capacity = None):
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
@@ -31,9 +37,17 @@ class Car:
self.battery_power = ENERGY_CAPACITY[self.label]["BATTERY_POWER"]
self.fuel_capacity = ENERGY_CAPACITY[self.label]["FUEL_CAPACITY"]
else:
logger.warning("Can't get car model please check cars.json")
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 is_electric(self) -> bool:
return self.fuel_capacity == 0 and self.battery_power > 0
@@ -44,6 +58,12 @@ class Car:
def is_hybrid(self) -> bool:
return self.fuel_capacity > 0 and self.battery_power > 0
def get_status(self):
if self.status is not None:
return self.status
logger.error("status of %s is None", self.vin)
raise ValueError("status of %s is None")
@classmethod
def from_json(cls, data: dict):
return cls(**data)
@@ -56,6 +76,7 @@ class Car:
def __str__(self):
return str(self.to_dict())
class Cars(list):
def __init__(self, *args):
list.__init__(self, *args)
@@ -64,13 +85,11 @@ class Cars(list):
for car in self:
if car.vin == vin:
return car
return None
def get_car_by_id(self, vehicle_id) -> Car:
for car in self:
if car.vehicle_id == vehicle_id:
return car
return None
def add(self, car: Car):
if self.get_car_by_id(car.vehicle_id) is None:
@@ -84,16 +103,17 @@ class Cars(list):
def __str__(self):
return str(list(map(str, self)))
def save_cars(self, name="cars.json"):
def save_cars(self, name=CARS_FILE):
config_str = json.dumps(self, default=lambda car: car.to_dict(), sort_keys=True, indent=4)
with open(name, "w") as f:
f.write(config_str)
@staticmethod
def load_cars(name="cars.json"):
def load_cars(name=CARS_FILE):
try:
with open(name, "r") as f:
json_str = f.read()
return Cars.from_json(json.loads(json_str))
except (FileNotFoundError, TypeError):
except (FileNotFoundError, TypeError) as e:
logger.debug(e)
return Cars()
+55 -45
View File
@@ -11,9 +11,14 @@ import pytz
from MyPSACC import MyPSACC
from MyLogger import logger
DISCONNECTED = "Disconnected"
INPROGRESS = "InProgress"
FAILURE = "Failure"
STOPPED = "Stopped"
FINISHED = "Finished"
class ChargeControl:
periodicity = 120
MQTT_TIMEOUT = 60
def __init__(self, psacc: MyPSACC, vin, percentage_threshold, stop_hour):
@@ -22,7 +27,6 @@ class ChargeControl:
self.set_stop_hour(stop_hour)
self.psacc = psacc
self.retry_count = 0
self.always_check = True
def set_stop_hour(self, stop_hour):
if stop_hour is None or stop_hour == [0, 0]:
@@ -37,47 +41,53 @@ class ChargeControl:
def get_stop_hour(self):
return self._stop_hour
def control_charge_with_ack(self, charge: bool):
self.psacc.charge_now(self.vin, charge)
self.retry_count += 1
sleep(ChargeControl.MQTT_TIMEOUT)
vehicle_status = self.psacc.get_vehicle_info(self.vin)
status = vehicle_status.get_energy('Electric').charging.status
if status in (FINISHED, DISCONNECTED):
logger.warning("Car state isn't compatible with charging %s", status)
if (status == INPROGRESS) != charge:
logger.warning("retry to control the charge of %s", self.vin)
self.psacc.charge_now(self.vin, charge)
self.retry_count += 1
return False
self.retry_count = 0
return True
def force_update(self):
# force update if the car doesn't send info during 10 minutes
last_update = self.psacc.vehicles_list.get_car_by_vin(self.vin).get_status().get_energy('Electric').updated_at
if (datetime.utcnow().replace(tzinfo=pytz.UTC) - last_update).total_seconds() > 60 * 10:
self.psacc.wakeup(self.vin)
def process(self):
now = datetime.now()
vehicle_status = self.psacc.vehicles_list.get_car_by_vin(self.vin).status
try:
if self._next_stop_hour is not None and self._next_stop_hour < now:
stop_charge = True
self._next_stop_hour += timedelta(days=1)
logger.info("it's time to stop the charge")
else:
stop_charge = False
if self.percentage_threshold != 100 or stop_charge or self.always_check:
if vehicle_status is not None:
status = vehicle_status.get_energy('Electric').charging.status
level = vehicle_status.get_energy('Electric').level
logger.info("charging status of %s is %s, battery level: %d", self.vin, status, level)
if status == "InProgress":
# force update if the car doesn't send info during 10 minutes
last_update = vehicle_status.get_energy('Electric').updated_at
if (datetime.utcnow().replace(tzinfo=pytz.UTC) - 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.retry_count += 1
sleep(ChargeControl.MQTT_TIMEOUT)
vehicle_status = self.psacc.get_vehicle_info(self.vin)
status = vehicle_status.get_energy('Electric').charging.status
if status == "InProgress":
logger.warning("retry to stop the charge of %s", 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()
if next_in_second < self.psacc.info_refresh_rate:
periodicity = next_in_second
self.thread = threading.Timer(periodicity, self.process, args=[vehicle_status])
self.thread.start()
vehicle_status = self.psacc.vehicles_list.get_car_by_vin(self.vin).get_status()
status = vehicle_status.get_energy('Electric').charging.status
level = vehicle_status.get_energy('Electric').level
logger.info("charging status of %s is %s, battery level: %d", self.vin, status, level)
if status == "InProgress":
self.force_update()
if level >= self.percentage_threshold and self.retry_count < 2:
self.control_charge_with_ack(False)
elif self._next_stop_hour is not None:
if self._next_stop_hour < now:
self._next_stop_hour += timedelta(days=1)
logger.info("it's time to stop the charge")
self.control_charge_with_ack(False)
else:
self.retry_count = 0
else:
logger.error("error can't retrieve vehicle info of %s", self.vin)
next_in_second = (self._next_stop_hour - now).total_seconds()
if next_in_second < self.psacc.info_refresh_rate:
periodicity = next_in_second
thread = threading.Timer(periodicity, self.process)
thread.setDaemon(True)
thread.start()
else:
self.retry_count = 0
except:
logger.error(traceback.format_exc())
@@ -95,10 +105,10 @@ class ChargeControls(dict):
def save_config(self, name="charge_config.json", force=False):
chd = {}
chargeControl: ChargeControl
for chargeControl in self.values():
chd[chargeControl.vin] = {"percentage_threshold": chargeControl.percentage_threshold,
"stop_hour": chargeControl.get_stop_hour()}
charge_control: ChargeControl
for charge_control in self.values():
chd[charge_control.vin] = {"percentage_threshold": charge_control.percentage_threshold,
"stop_hour": charge_control.get_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._config_hash != new_hash:
@@ -121,8 +131,8 @@ class ChargeControls(dict):
try:
return self[vin]
except KeyError:
return None
pass
def start(self):
def init(self):
for charge_control in self.values():
charge_control.psacc.info_callback.append(charge_control.process)
+63 -116
View File
@@ -10,7 +10,6 @@ from json import JSONEncoder
from hashlib import md5
from time import sleep
import requests
from oauth2_client.credentials_manager import CredentialManager, ServiceInformation
import paho.mqtt.client as mqtt
from requests import Response
@@ -22,13 +21,15 @@ from otp.Otp import load_otp, new_otp_session, save_otp, ConfigException, Otp
from psa_connectedcar import ApiClient
from psa_connectedcar.rest import ApiException
from MyLogger import logger
from threading import Semaphore, Timer
from functools import wraps
from web.db import get_db
from utils import get_temp, rate_limit
from web.db import get_db, clean_position
from geojson import Feature, Point, FeatureCollection
from geojson import dumps as geo_dumps
PSA_CORRELATION_DATE_FORMAT = "%Y%m%d%H%M%S%f"
PSA_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
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",
@@ -43,6 +44,7 @@ MQTT_REQ_TOPIC = "psa/RemoteServices/from/cid/"
MQTT_RESP_TOPIC = "psa/RemoteServices/to/cid/"
MQTT_EVENT_TOPIC = "psa/RemoteServices/events/MPHRTServices/"
MQTT_TOKEN_TTL = 890
CARS_FILE = "cars.json"
# add method to class Energy
@@ -56,25 +58,6 @@ def get_energy(self, energy_type):
psac.models.status.Status.get_energy = get_energy
def rate_limit(limit, every):
def limit_decorator(fn):
semaphore = Semaphore(limit)
@wraps(fn)
def wrapper(*args, **kwargs):
semaphore.acquire()
try:
return fn(*args, **kwargs)
finally: # don't catch but ensure semaphore release
timer = Timer(every, semaphore.release)
timer.setDaemon(True) # allows the timer to be canceled on exit
timer.start()
return wrapper
return limit_decorator
class OpenIdCredentialManager(CredentialManager):
def _grant_password_request(self, login: str, password: str, realm: str) -> dict:
return dict(grant_type='password',
@@ -110,7 +93,7 @@ class OauthAPIClient(ApiClient):
response_type=None, auth_settings=None, async_req=None,
_return_http_data_only=None, collection_formats=None,
_preload_content=True, _request_timeout=None):
for attempt in range(0, 2):
for _ in range(0, 2):
try:
if not async_req:
return self._ApiClient__call_api(resource_path, method,
@@ -134,11 +117,11 @@ class OauthAPIClient(ApiClient):
raise e
def correlation_id(date):
date_str = date.strftime("%Y%m%d%H%M%S%f")[:-3]
def gen_correlation_id(date):
date_str = date.strftime(PSA_CORRELATION_DATE_FORMAT)[:-3]
uuid_str = str(uuid.uuid4()).replace("-", "")
correlationId = uuid_str + date_str
return correlationId
correlation_id = uuid_str + date_str
return correlation_id
class MyPSACC:
@@ -162,10 +145,10 @@ class MyPSACC:
self.manager.refresh_token = refresh_token
self.remote_refresh_token = remote_refresh_token
self.remote_access_token = None
self.vehicles_list = Cars.load_cars()
self.setProxies(proxies)
self.vehicles_list = Cars.load_cars(CARS_FILE)
self.set_proxies(proxies)
self.customer_id = customer_id
self._configHash = None
self._config_hash = None
self.api_config.verify_ssl = False
self.api_config.api_key['client_id'] = self.client_id
self.api_config.api_key['x-introspect-realm'] = self.realm
@@ -192,7 +175,7 @@ class MyPSACC:
api_instance = psac.VehiclesApi(OauthAPIClient(self.api_config))
return api_instance
def setProxies(self, proxies):
def set_proxies(self, proxies):
if proxies is None:
self._proxies = dict(http='', https='')
self.api_config.proxy = None
@@ -204,10 +187,10 @@ class MyPSACC:
def get_vehicle_info(self, vin):
car = self.vehicles_list.get_car_by_vin(vin)
for attempt in range(0, 2):
for _ in range(0, 2):
res = self.api().get_vehicle_status(car.vehicle_id, extension=["odometer"])
car.status = res
if res is not None:
car.status = res
if self._record_enabled:
self.record_info(vin, res)
break
@@ -224,7 +207,7 @@ class MyPSACC:
callback()
# monitor doesn't seem to work
def newMonitor(self, vin, body):
def new_monitor(self, vin, body):
res = self.manager.post("https://api.groupe-psa.com/connectedcar/v4/user/vehicles/" +
self.vehicles_list.get_car_by_vin(vin).id + "/status?client_id=" + self.client_id,
headers=self.headers, data=body)
@@ -258,12 +241,12 @@ class MyPSACC:
# 6 otp by day
@rate_limit(6, 3600 * 24)
def getOtpCode(self):
def get_otp_code(self):
try:
otp_code = self.otp.getOtpCode()
otp_code = self.otp.get_otp_code()
except ConfigException:
self.load_otp(new=True)
otp_code = self.otp.getOtpCode()
otp_code = self.otp.get_otp_code()
save_otp(self.otp)
return otp_code
@@ -297,68 +280,53 @@ class MyPSACC:
else:
logger.error("can't refresh_remote_token: %s\n Create a new one", data)
self.remote_token_last_update = datetime.now()
otp_code = self.getOtpCode()
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)
return res
def on_mqtt_connect(self, client, userdata, rc, a):
try:
logger.info("Connected with result code %s", rc)
topics = [MQTT_RESP_TOPIC + self.customer_id + "/#"]
for car in self.vehicles_list:
topics.append(MQTT_EVENT_TOPIC + car.vin)
for topic in topics:
client.subscribe(topic)
logger.info("subscribe to %s", topic)
except:
logger.error(traceback.format_exc())
logger.info("Connected with result code %s", rc)
topics = [MQTT_RESP_TOPIC + self.customer_id + "/#"]
for car in self.vehicles_list:
topics.append(MQTT_EVENT_TOPIC + car.vin)
for topic in topics:
client.subscribe(topic)
logger.info("subscribe to %s", topic)
def on_mqtt_disconnect(self, client, userdata, rc):
try:
logger.warning("Disconnected with result code %d", rc)
if rc == 1:
self.refresh_remote_token(force=True)
else:
logger.warning(mqtt.error_string(rc))
except:
logger.error(traceback.format_exc())
logger.warning("Disconnected with result code %d", rc)
if rc == 1:
self.refresh_remote_token(force=True)
else:
logger.warning(mqtt.error_string(rc))
def on_mqtt_message(self, client, userdata, msg):
charge_not_detected = False
try:
logger.info("mqtt msg %s %s", msg.topic, msg.payload)
data = json.loads(msg.payload)
charge_info = None
if msg.topic.startswith(MQTT_RESP_TOPIC):
if "return_code" in data:
if data["return_code"] == "0":
pass
elif data["return_code"] == "400":
self.refresh_remote_token(force=True)
logger.error("retry last request, token was expired")
elif data["return_code"] == "300":
logger.error('%d', data["return_code"])
else:
logger.error('%s : %s', data["return_code"], data["reason"])
else:
if "return_code" not in data:
logger.debug("mqtt msg hasn't return code")
if msg.topic.startswith(MQTT_EVENT_TOPIC):
if data["charging_state"]['remaining_time'] != 0 and data["charging_state"]['rate'] == 0:
charge_not_detected = True
elif msg.topic.endswith("/VehicleState"):
try:
if data["resp_data"]["charging_state"]['remaining_time'] != 0 \
and data["resp_data"]["charging_state"]['rate'] == 0:
charge_not_detected = True
self.precond_programs[data["vin"]] = data["resp_data"]["precond_state"]["programs"]
except KeyError:
pass
if charge_not_detected:
elif data["return_code"] == "400":
self.refresh_remote_token(force=True)
logger.error("retry last request, token was expired")
elif data["return_code"] == "300":
logger.error('%d', data["return_code"])
elif data["return_code"] != "0":
logger.error('%s : %s', data["return_code"], data["reason"])
if msg.topic.endswith("/VehicleState"):
charge_info = data["resp_data"]["charging_state"]
self.precond_programs[data["vin"]] = data["resp_data"]["precond_state"]["programs"]
elif msg.topic.startswith(MQTT_EVENT_TOPIC):
charge_info = data["charging_state"]
if charge_info is not None and charge_info['remaining_time'] != 0 and charge_info['rate'] == 0:
# fix a psa server bug where charge beginning without status api being properly updated
logger.info("charge begin")
logger.warning("charge begin but API isn't updated")
sleep(60)
self.wakeup(data["vin"])
except:
except KeyError:
logger.error(traceback.format_exc())
def start_mqtt(self):
@@ -376,19 +344,18 @@ class MyPSACC:
def __keep_mqtt(self): # avoid token expiration
timeout = 3600 * 24 # 1 day
try:
if len(self.vehicles_list) > 0:
self.get_state(self.vehicles_list[0].vin)
except:
logger.warning("keep_mqtt error")
threading.Timer(timeout, self.__keep_mqtt).start()
t = threading.Timer(timeout, self.__keep_mqtt)
t.setDaemon(True)
t.start()
def mqtt_request(self, vin, req_parameters):
self.refresh_token()
date = datetime.utcnow()
date_f = "%Y-%m-%dT%H:%M:%SZ"
date_str = date.strftime(date_f)
date_str = date.strftime(PSA_DATE_FORMAT)
data = {"access_token": self.remote_access_token, "customer_id": self.customer_id,
"correlation_id": correlation_id(date), "req_date": date_str, "vin": vin,
"correlation_id": gen_correlation_id(date), "req_date": date_str, "vin": vin,
"req_parameters": req_parameters}
return json.dumps(data)
@@ -495,10 +462,10 @@ class MyPSACC:
def save_config(self, name="config.json", force=False):
config_str = json.dumps(self, cls=MyPeugeotEncoder, sort_keys=True, indent=4).encode("utf8")
new_hash = md5(config_str).hexdigest()
if force or self._configHash != new_hash:
if force or self._config_hash != new_hash:
with open(name, "wb") as f:
f.write(config_str)
self._configHash = new_hash
self._config_hash = new_hash
logger.info("save config change")
@staticmethod
@@ -543,16 +510,7 @@ class MyPSACC:
if conn.execute("SELECT Timestamp from position where Timestamp=?", (date,)).fetchone() is None:
temp = None
if self.weather_api is not None:
try:
weather_rep = requests.get("https://api.openweathermap.org/data/2.5/onecall",
params={"lat": latitude, "lon": longitude,
"exclude": "minutely,hourly,daily,alerts",
"appid": self.weather_api,
"units": "metric"})
temp = weather_rep.json()["current"]["temp"]
logger.debug("Temperature :%fc", temp)
except:
logger.error("Unable to get temperature from openweathermap :%s", traceback.format_exc())
temp = get_temp(latitude,longitude,self.weather_api)
if level_fuel == 0: # fix fuel level not provided when car is off
try:
@@ -571,19 +529,10 @@ class MyPSACC:
conn.commit()
logger.info("new position recorded for %s", vin)
res = conn.execute(
"SELECT Timestamp,mileage,level from position ORDER BY Timestamp DESC LIMIT 3;").fetchall()
# Clean DB
if len(res) == 3:
if res[0]["mileage"] == res[1]["mileage"] == res[2]["mileage"]:
if res[0]["level"] == res[1]["level"] == res[2]["level"]:
logger.debug("Delete duplicate line")
conn.execute("DELETE FROM position where Timestamp=?;", (res[1]["Timestamp"],))
conn.commit()
clean_position(conn)
else:
logger.debug("position already saved")
# todo handle battery status
if charging_status == "InProgress":
try:
in_progress = conn.execute("SELECT stop_at FROM battery WHERE VIN=? ORDER BY start_at DESC limit 1",
@@ -591,7 +540,7 @@ class MyPSACC:
except TypeError:
in_progress = False
if not in_progress:
res = conn.execute("INSERT INTO battery(start_at,start_level,VIN) VALUES(?,?,?)",
conn.execute("INSERT INTO battery(start_at,start_level,VIN) VALUES(?,?,?)",
(charge_date, level, vin))
conn.commit()
else:
@@ -603,14 +552,12 @@ class MyPSACC:
if in_progress:
co2_per_kw = Ecomix.get_co2_per_kw(start_at, charge_date, latitude, longitude)
kw = (level - start_level) / 100 * self.vehicles_list.get_car_by_vin(vin).battery_power
res = conn.execute(
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 TypeError:
logger.debug("battery table is empty")
except:
logger.debug("Error when saving status %s", traceback.format_exc())
conn.close()
@staticmethod
+35 -55
View File
@@ -4,8 +4,9 @@ from typing import List
from geojson import Feature, FeatureCollection, MultiLineString
from Car import Cars
from Car import Cars, Car
from MyLogger import logger
from trip_parser import TripParser
from web.db import get_db
@@ -24,33 +25,36 @@ class Trip:
self.end_at = None
self.positions: List[Points] = []
self.speed_average = None
self.consumption = None
self.consumption_km = None
self.consumption_fuel = None
self.consumption_fuel_km = None
self.consumption = 0
self.consumption_km = 0
self.consumption_fuel = 0
self.consumption_fuel_km = 0
self.distance = None
self.duration = None
self.mileage = None
self.car: Car = None
def add_points(self, latitude, longitude):
self.positions.append(Points(latitude, longitude))
def set_consumption(self, consumption: float):
def set_consumption(self, diff_level: float) -> float:
if self.distance is None:
raise Exception("Distance not set")
if consumption < 0:
raise ValueError("Distance not set")
if diff_level < 0:
logger.debugv("trip has negative consumption")
consumption = 0
self.consumption = consumption
diff_level = 0
self.consumption = diff_level * self.car.battery_power/100
self.consumption_km = 100 * self.consumption / self.distance # kw/100 km
return self.consumption_km
def set_fuel_consumption(self, consumption):
def set_fuel_consumption(self, consumption) -> float:
if self.distance is None:
raise Exception("Distance not set")
raise ValueError("Distance not set")
if consumption < 0:
logger.debugv("trip has negative fuel consumption")
self.consumption_fuel = round(consumption, 2) # L
self.consumption_fuel_km = round(100 * self.consumption_fuel / self.distance, 2) # L/100 km
return self.consumption_fuel_km
def get_consumption(self):
return {
@@ -95,12 +99,12 @@ class Trips(list):
"consumption": tr.consumption})
return res
@staticmethod
def __charge_detection(charge, distance):
# A margin of two is set because battery level can increase with regeneration system or temperature change.
# If distance is bigger than 0 but charge bigger than five there is probably missing point and we assume that
# regeneration/temperature can't increase by 5 percent the battery level
return charge > 2 and (distance == 0 or charge > 5)
def check_and_append(self,tr:Trip):
if tr.consumption_km <= tr.car.max_elec_consumption and tr.consumption_fuel_km <= tr.car.max_fuel_consumption:
self.append(tr)
return True
logger.debugv("trip discarded")
return False
@staticmethod
def get_trips(vehicles_list: Cars) -> dict[str, Trips]:
@@ -111,15 +115,15 @@ class Trips(list):
for vin in vehicles:
trips = Trips()
vin = vin[0]
car = vehicles_list.get_car_by_vin(vin)
battery_capacity = car.battery_power
fuel_capacity = car.fuel_capacity
res = conn.execute('SELECT * FROM position WHERE VIN=? ORDER BY Timestamp', (vin,)).fetchall()
if len(res) > 1:
car = vehicles_list.get_car_by_vin(vin)
assert car is not None
trip_parser = TripParser(car)
start = res[0]
end = res[1]
tr = Trip()
# res = list(map(dict,res))
# for debugging use this line res = list(map(dict,res))
for x in range(0, len(res) - 2):
logger.debugv("%s mileage:%.1f level:%s level_fuel:%s",
res[x]['Timestamp'], res[x]['mileage'], res[x]['level'], res[x]['level_fuel'])
@@ -130,18 +134,9 @@ class Trips(list):
speed_average = distance / duration
except ZeroDivisionError:
speed_average = 0
charge = end["level"] - start["level"]
if end["level_fuel"] is not None and start["level_fuel"] is not None:
refuel = end["level_fuel"] - start["level_fuel"]
else:
refuel = 0
restart_trip = False
if refuel > 0:
if trip_parser.is_refuel(start, end, distance ):
restart_trip = True
logger.debugv("refuel detected")
elif Trips.__charge_detection(charge, distance):
restart_trip = True
logger.debugv("charge detected")
elif speed_average < 0.2 and duration > 0.05:
restart_trip = True
logger.debugv("low speed detected")
@@ -157,18 +152,9 @@ class Trips(list):
speed_average = distance / duration
except ZeroDivisionError:
speed_average = 0
charge = next_el["level"] - end["level"]
if next_el["level_fuel"] is not None and end["level_fuel"] is not None:
refuel = next_el["level_fuel"] - end["level_fuel"]
else:
refuel = 0
end_trip = False
if refuel > 0:
if trip_parser.is_refuel(end, next_el, distance):
end_trip = True
logger.debugv("refuel detected")
elif Trips.__charge_detection(charge, distance):
end_trip = True
logger.debugv("charge detected")
elif speed_average < 0.2 and duration > 0.05:
# (distance == 0 and duration > 0.08) or duration > 2 or
# check the speed to handle missing point
@@ -192,14 +178,12 @@ class Trips(list):
tr.add_points(end["longitude"], end["latitude"])
tr.duration = (end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600
tr.speed_average = tr.distance / tr.duration
diff_level = start["level"] - end["level"]
tr.set_consumption(diff_level / 100 * battery_capacity) # kw
if start["level_fuel"] is not None and end["level_fuel"] is not None:
diff_level_fuel = start["level_fuel"] - end["level_fuel"]
tr.set_fuel_consumption(diff_level_fuel / 100 * fuel_capacity)
else:
tr.consumption_fuel = 0
tr.consumption_fuel_km = 0
diff_level, diff_level_fuel = trip_parser.get_level_consumption(start, end)
tr.car = car
if diff_level != 0:
tr.set_consumption(diff_level)# kw
if diff_level_fuel != 0:
tr.set_fuel_consumption(diff_level_fuel)
tr.mileage = end["mileage"]
logger.debugv("Trip: %s -> %s %.1fkm %.2fh %.0fkm/h %.2fkWh %.2fkWh/100km %.2fL "
"%.2fL/100km %.1fkm",
@@ -207,11 +191,7 @@ class Trips(list):
tr.speed_average, tr.consumption, tr.consumption_km,
tr.consumption_fuel, tr.consumption_fuel_km, tr.mileage)
# filter bad value
if tr.consumption_km < 70 and (
tr.consumption_fuel_km is None or tr.consumption_fuel_km < 30):
trips.append(tr)
else:
logger.debugv("trip discarded")
trips.check_and_append(tr)
start = next_el
tr = Trip()
else:
+34 -37
View File
@@ -42,7 +42,7 @@ base36 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
"w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
def numberToBase36(n):
def number_to_base36(n):
b = 36
if n == 0:
return [0]
@@ -69,11 +69,10 @@ class Otp:
iw_host = "https://otp.mpsa.com"
proxies = None
def __init__(self, inweboAccessId):
def __init__(self, inwebo_access_id):
self.Kiw = None
self.pinmode = None
self.Kfact = None
self.Kiw = None
self.pinmode = None
self.needsync = None
self.serviceid = None
@@ -88,29 +87,29 @@ class Otp:
self.isMac = True
self.data = IWData(self)
self.cipher = None
self.macid = inweboAccessId
self.macid = inwebo_access_id
self.smsCode = None
self.mode = Otp.ACTIVATE_MODE
self.defi = 0
self.otp_count = 0
def init(self, Kfact=None, Kiw=None, pinmode=None):
self.Kfact = Kfact
def init(self, kfact=None, kiw=None, pinmode=None):
self.Kfact = kfact
self.pinmode = pinmode
self.Kiw = self.decode_oaep(Kiw, self.Kfact)
self.Kiw = self.decode_oaep(kiw, self.Kfact)
key = RSA.construct((int(self.Kiw, 16), Otp.exponent))
self.cipher = oaep.new(key, hashAlgo=Hash.SHA256)
self.cipher = oaep.new(key, hash_algo=Hash.SHA256)
def getSerial(self):
def get_serial(self):
return self.device_id + "/_/" + self.iwalea
def generateKMA(self, codepin):
serial = self.getSerial()
def generate_kma(self, codepin):
serial = self.get_serial()
kma_str = codepin + ";" + serial
kma = hashlib.sha256(kma_str.encode("utf-8")).hexdigest()[:32]
return kma
def getR(self):
def get_r(self):
if self.action == "upgrade":
iw = self.data.iwK1
# not correctly implemented
@@ -121,7 +120,7 @@ class Otp:
else:
R2 = self.challenge + ";" + iw + ";"
R0 = self.challenge + ";" + iw + ";" + self.getSerial()
R0 = self.challenge + ";" + iw + ";" + self.get_serial()
R1 = self.challenge + ";" + iw + ";" + self.data.iwK1
logger.debug("%s\n%s\n%s", R0, R1, R2)
return {"R0": hashlib.sha256(R0.encode("utf-8")).hexdigest(),
@@ -132,7 +131,7 @@ class Otp:
def decode_oaep(enc, key):
modulus = int(key, 16)
key = RSA.construct((modulus, Otp.exponent))
cipher = oaep.new(key, hashAlgo=Hash.SHA256)
cipher = oaep.new(key, hash_algo=Hash.SHA256)
block_size = 128
dec_string = ""
enc_b = bytes.fromhex(enc)
@@ -167,10 +166,9 @@ class Otp:
if setup:
return etree_to_dict(ElT.XML(raw_xml))["ActionSetup"]
return etree_to_dict(ElT.XML(raw_xml))["ActionFinalize"]
except:
except KeyError:
logger.debug(raw_xml)
raise Exception("Bad return from server")
raise ValueError("Bad response from server")
def activation_start(self):
param = {"action": "ActionSetup", "mode": self.mode, "id": self.data.iwid, "lastsync": self.data.iwTsync,
@@ -192,7 +190,7 @@ class Otp:
def activation_finalyze(self, random_bytes=None):
R = self.getR()
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",
"lang": "fr", "ack": "", "macid": self.macid}
@@ -200,16 +198,16 @@ class Otp:
params.update({"keytype": '0', "sid": self.data.iwsecid})
elif self.mode == Otp.ACTIVATE_MODE:
kma_crypt = self.cipher.encrypt(bytes.fromhex(self.generateKMA(self.codepin))).hex()
kma_crypt = self.cipher.encrypt(bytes.fromhex(self.generate_kma(self.codepin))).hex()
pin_crypt = self.cipher.encrypt(self.codepin.encode("utf-8")).hex()
params.update({"serial": self.getSerial(), "code": self.smsCode,
params.update({"serial": self.get_serial(), "code": self.smsCode,
"Kma": kma_crypt, "pin": pin_crypt, "name": "Android SDK built for x86_64 / UNKNOWN", })
params.update(R)
xml = self.request(params)
if xml["err"] != "OK":
return Otp.NOK
self.data.synchro(xml, self.generateKMA(self.codepin))
self.data.synchro(xml, self.generate_kma(self.codepin))
if self.mode == Otp.OTP_MODE:
try:
@@ -221,7 +219,6 @@ class Otp:
return Otp.OTP_TWICE
return Otp.OK
if "ms_n" not in xml or xml["ms_n"] == 0:
logger.debug("no ms_n request needed")
return Otp.OK
@@ -234,34 +231,34 @@ class Otp:
self.action = "synchro"
res = self.decode_oaep(xml["ms_key"], self.Kfact)
temp_key = RSA.construct((int(res, 16), self.exponent))
temp_cipher = oaep.new(temp_key, hashAlgo=Hash.SHA256)
temp_cipher = oaep.new(temp_key, hash_algo=Hash.SHA256)
if random_bytes is None:
random_bytes = token_bytes(16)
KpubEncode = temp_cipher.encrypt(random_bytes)
kpub_encode = temp_cipher.encrypt(random_bytes)
aes_cipher = AES.new(bytes.fromhex(self.generateKMA(self.codepin)), AES.MODE_ECB)
encodeAesFromHex = aes_cipher.encrypt(random_bytes).hex()
self.data.iwsecval = encodeAesFromHex
aes_cipher = AES.new(bytes.fromhex(self.generate_kma(self.codepin)), AES.MODE_ECB)
encode_aes_from_hex = aes_cipher.encrypt(random_bytes).hex()
self.data.iwsecval = encode_aes_from_hex
self.data.iwsecid = xml["s_id"]
self.data.iwsecn = 1
req_param = {"action": "ActionFinalize", "mode": Otp.MS_MODE, "ms_id" + ms_n: xml["ms_id"],
"ms_val" + ms_n: KpubEncode.hex(), "macid": self.macid}
"ms_val" + ms_n: kpub_encode.hex(), "macid": self.macid}
req_param.update({"id": self.data.iwid, "lastsync": self.data.iwTsync, "ms_n": 1})
req_param.update(self.getR())
req_param.update(self.get_r())
xml = self.request(req_param)
self.data.synchro(xml, self.generateKMA(self.codepin))
self.data.synchro(xml, self.generate_kma(self.codepin))
return Otp.OK
def _getOtpCode(self):
password = self.data.iwK1 + ":" + self.defi + ":" + self.data.iwsecval
def _get_otp_code(self):
password = self.data.iwK1 + ":" + str(self.defi) + ":" + self.data.iwsecval
res = bytes(hashlib.sha256(password.encode("utf-8")).digest())
nb = ((int.from_bytes(res[:4], byteorder="big") & 0xfffffff) * 1024) + (
int.from_bytes(res[4:8], byteorder="big") & 1023)
otp = numberToBase36(nb)
otp = number_to_base36(nb)
return otp
def getOtpCode(self):
def get_otp_code(self):
self.mode = Otp.OTP_MODE
otp_code = None
if self.activation_start():
@@ -271,7 +268,7 @@ class Otp:
self.mode = Otp.OTP_MODE
self.activation_start()
self.activation_finalyze()
otp_code = self._getOtpCode()
otp_code = self._get_otp_code()
logger.debug("otp code: %s", otp_code)
if otp_code is None:
raise ConfigException("Can't get otp code")
@@ -286,7 +283,7 @@ class Otp:
self.__dict__.update(dict_param)
if self.Kiw is not None:
key = RSA.construct((int(self.Kiw, 16), Otp.exponent))
self.cipher = oaep.new(key, hashAlgo=Hash.SHA256)
self.cipher = oaep.new(key, hash_algo=Hash.SHA256)
@staticmethod
def set_proxies(proxies):
@@ -294,7 +291,7 @@ class Otp:
def encode_oeap(text, key):
cipher = oaep.new(bytes.fromhex(key), hashAlgo=Hash.SHA256)
cipher = oaep.new(bytes.fromhex(key), hash_algo=Hash.SHA256)
return cipher.encrypt(text)
+4 -4
View File
@@ -7,14 +7,14 @@ class Tokenizer:
def nextToken(self):
if self.currentIndex >= len(self.s):
return ""
indexOf = self.currentIndex + self.s[self.currentIndex:].index(self.delimiter)
if indexOf == -1:
index_of = self.currentIndex + self.s[self.currentIndex:].index(self.delimiter)
if index_of == -1:
substring = self.s[self.currentIndex:]
self.currentIndex = self.s.length()
return substring
substring2 = self.s[self.currentIndex:indexOf]
self.currentIndex = indexOf + len(self.delimiter)
substring2 = self.s[self.currentIndex:index_of]
self.currentIndex = index_of + len(self.delimiter)
return substring2
def nextTokenI(self):
+24 -22
View File
@@ -26,9 +26,9 @@ class MyOAEP(PKCS1OAEP_Cipher):
"""
# See 7.1.2 in RFC3447
modBits = Cryptodome.Util.number.size(self._key.n)
k = ceil_div(modBits, 8) # Convert from bits to bytes
hLen = self._hashObj.digest_size
mod_bits = Cryptodome.Util.number.size(self._key.n)
k = ceil_div(mod_bits, 8) # Convert from bits to bytes
h_len = self._hashObj.digest_size
# Step 1b and 1c
if len(ciphertext) != k:
@@ -36,47 +36,49 @@ class MyOAEP(PKCS1OAEP_Cipher):
# Step 2a (O2SIP)
ct_int = bytes_to_long(ciphertext)
# Step 2b (RSADP)
#m_int = self._key._decrypt(ct_int)
m_int = pow(ct_int,self._key.e,self._key.n)
# m_int = self._key._decrypt(ct_int)
m_int = pow(ct_int, self._key.e, self._key.n)
# Complete step 2c (I2OSP)
em = long_to_bytes(m_int, k)
# Step 3a
lHash = self._hashObj.new(self._label).digest()
l_hash = self._hashObj.new(self._label).digest()
# Step 3b
y = em[0]
# y must be 0, but we MUST NOT check it here in order not to
# allow attacks like Manger's (http://dl.acm.org/citation.cfm?id=704143)
maskedSeed = em[1:hLen + 1]
maskedDB = em[hLen + 1:]
masked_seed = em[1:h_len + 1]
masked_db = em[h_len + 1:]
# Step 3c
seedMask = self._mgf(maskedDB, hLen)
seed_mask = self._mgf(masked_db, h_len)
# Step 3d
seed = strxor(maskedSeed, seedMask)
seed = strxor(masked_seed, seed_mask)
# Step 3e
dbMask = self._mgf(seed, k - hLen - 1)
db_mask = self._mgf(seed, k - h_len - 1)
# Step 3f
db = strxor(maskedDB, dbMask)
db = strxor(masked_db, db_mask)
# Step 3g
one_pos = db[hLen:].find(b'\x01')
lHash1 = db[:hLen]
one_pos = db[h_len:].find(b'\x01')
l_hash1 = db[:h_len]
invalid = bord(y) | int(one_pos < 0)
hash_compare = strxor(lHash1, lHash)
hash_compare = strxor(l_hash1, l_hash)
for x in hash_compare:
invalid |= bord(x)
for x in db[hLen:one_pos]:
for x in db[h_len:one_pos]:
invalid |= bord(x)
if invalid != 0:
raise ValueError("Incorrect decryption.")
# Step 4
return db[hLen + one_pos + 1:]
return db[h_len + one_pos + 1:]
def new(key, hashAlgo=None, mgfunc=None, label=b'', randfunc=None):
if randfunc is None:
randfunc = Random.get_random_bytes
return MyOAEP(key, hashAlgo, mgfunc, label, randfunc)
#for testing
def new(key, hash_algo=None, mgfunc=None, label=b'', rand_func=None):
if rand_func is None:
rand_func = Random.get_random_bytes
return MyOAEP(key, hash_algo, mgfunc, label, rand_func)
# for testing
def notrandom(x):
if x == 32:
return b'\xf56\xccL`\x8a\x97l\nX0\xf4\x11\x9a\x0e\xce\x99K^\xe6\xcbU\xf3W+It"\xf5\x84\x1d\xe6'
+5 -2
View File
@@ -77,9 +77,12 @@ if __name__ == "__main__":
web.app.myp.info_refresh_rate = args.refresh * 60
if args.charge_control:
web.app.chc = ChargeControls.load_config(web.app.myp, name=args.charge_control)
web.app.chc.start()
Thread(target=web.app.myp.refresh_vehicle_info).start()
web.app.chc.init()
t2 = Thread(target=web.app.myp.refresh_vehicle_info)
t2.setDaemon(True)
t2.start()
save_config(web.app.myp)
t1 = Thread(target=start_app, args=["My car info", args.base_path, logger.level < 20, args.listen, int(args.port)])
t1.setDaemon(True)
t1.start()
+64
View File
@@ -0,0 +1,64 @@
from collections.abc import Callable
from Car import Car
from MyLogger import logger
LEVEL = "level"
LEVEL_FUEL = "level_fuel"
class TripParser:
def __init__(self, car: Car):
self.car = car
self.get_level_consumption, self.is_refuel = self.__get_energy_method()
def __get_energy_method(self) -> [Callable]:
if self.car.is_electric():
return TripParser.get_elec_consumption, self.__is_recharging
if self.car.is_thermal():
return TripParser.get_thermal_consumption, self.__is_refuel
if self.car.is_hybrid():
return TripParser.get_hybrid_consumption, self.__is_refuel_or_recharging
@staticmethod
def get_thermal_consumption(start, end):
return [0, start[LEVEL_FUEL] - end[LEVEL_FUEL]]
@staticmethod
def get_elec_consumption(start, end):
return [start[LEVEL] - end[LEVEL], 0]
@staticmethod
def get_hybrid_consumption(start, end):
res = []
for energy in [LEVEL, LEVEL_FUEL]:
res.append(start[energy] - end[energy])
return res
def __is_refuel_or_recharging(self, start, end, distance):
decharge, fuel_consumption = self.get_level_consumption(start, end)
if fuel_consumption < 0:
logger.debugv("refuel detected")
return True
elif TripParser.is_recharging(decharge, fuel_consumption, distance):
logger.debugv("charge detected")
return True
return False
def __is_refuel(self, start, end, distance):
decharge, fuel_consumption = self.get_level_consumption(start, end)
if fuel_consumption < 0:
logger.debugv("refuel detected")
return True
return False
def __is_recharging(self, start, end, distance):
decharge, fuel_consumption = self.get_level_consumption(start, end)
return TripParser.is_recharging(decharge, fuel_consumption, distance)
@staticmethod
def is_recharging(decharge, _, distance):
# A margin of two is set because battery level can increase with regeneration system or temperature change.
# If distance is bigger than 0 but charge bigger than five there is probably missing point and we assume that
# regeneration/temperature can't increase by 5 percent the battery level
return decharge < -2 and (distance == 0 or decharge < -5)
+41
View File
@@ -0,0 +1,41 @@
import traceback
from functools import wraps
from threading import Semaphore, Timer
import requests
from MyLogger import logger
def get_temp(latitude, longitude, api_key):
try:
weather_rep = requests.get("https://api.openweathermap.org/data/2.5/onecall",
params={"lat": latitude, "lon": longitude,
"exclude": "minutely,hourly,daily,alerts",
"appid": api_key,
"units": "metric"})
temp = weather_rep.json()["current"]["temp"]
logger.debug("Temperature :%fc", temp)
except ConnectionError:
logger.error("Can't connect to openweathermap :%s", traceback.format_exc())
except KeyError:
logger.error("Unable to get temperature from openweathermap :%s", traceback.format_exc())
def rate_limit(limit, every):
def limit_decorator(fn):
semaphore = Semaphore(limit)
@wraps(fn)
def wrapper(*args, **kwargs):
semaphore.acquire()
try:
return fn(*args, **kwargs)
finally: # don't catch but ensure semaphore release
timer = Timer(every, semaphore.release)
timer.setDaemon(True) # allows the timer to be canceled on exit
timer.start()
return wrapper
return limit_decorator
+3 -1
View File
@@ -43,7 +43,9 @@ def start_app(title, base_path, debug: bool, host, port):
return run_simple(host, port, application, use_reloader=False, use_debugger=debug)
myp = None
# noinspection PyTypeChecker
myp:MyPSACC = None
# noinspection PyTypeChecker
chc: ChargeControls = None
+13
View File
@@ -4,6 +4,8 @@ from datetime import datetime
import pytz
from typing import Callable
from MyLogger import logger
callback_fct: Callable[[], None] = lambda: None
default_db_file = 'info.db'
@@ -33,3 +35,14 @@ def get_db(db_file=default_db_file):
"SELECT update_trips(); END;")
conn.commit()
return conn
def clean_position(conn):
res = conn.execute(
"SELECT Timestamp,mileage,level from position ORDER BY Timestamp DESC LIMIT 3;").fetchall()
# Clean DB
if len(res) == 3 and res[0]["mileage"] == res[1]["mileage"] == res[2]["mileage"] and \
res[0]["level"] == res[1]["level"] == res[2]["level"]:
logger.debug("Delete duplicate line")
conn.execute("DELETE FROM position where Timestamp=?;", (res[1]["Timestamp"],))
conn.commit()
+4 -7
View File
@@ -108,18 +108,15 @@ def get_figures(trips: Trips, charging: List[dict]):
# charging
charging_data = DataFrame.from_records(charging)
try:
co2_per_kw = charging_data["co2"].sum() / charging_data["kw"].sum()
except ZeroDivisionError:
co2_per_kw = 0
except KeyError: # when there is no data yet:
co2_data = charging_data[charging_data["co2"] > 0]
co2_per_kw = co2_data["co2"].sum() / co2_data["kw"].sum()
except (ZeroDivisionError, KeyError):
co2_per_kw = 0
co2_per_km = co2_per_kw * kw_per_km / 100
try:
charge_speed = 3600 * charging_data["kw"].mean() / \
(charging_data["stop_at"] - charging_data["start_at"]).mean().total_seconds()
except TypeError: # when there is no data yet:
charge_speed = 0
except KeyError: # when there is no data yet:
except (TypeError, KeyError): # when there is no data yet:
charge_speed = 0
battery_info = dash_table.DataTable(
+12 -20
View File
@@ -52,7 +52,7 @@ def get_vehicules():
@app.route('/get_vehicleinfo/<string:vin>')
def get_vehicle_Info(vin):
def get_vehicle_info(vin):
response = app.response_class(
response=json.dumps(myp.get_vehicle_info(vin).to_dict(), default=str),
status=200,
@@ -87,18 +87,15 @@ def get_position(vin):
try:
coordinates = res.last_position.geometry.coordinates
except AttributeError:
coordinates = []
if len(coordinates) == 3: # altitude is not always availlable
longitude, latitude, altitude = coordinates
return jsonify(
{"longitude": longitude, "latitude": latitude, "altitude": altitude,
"url": f"http://maps.google.com/maps?q={latitude},{longitude}"})
if len(coordinates) == 2:
longitude, latitude = coordinates
return jsonify(
{"longitude": longitude, "latitude": latitude,
"url": f"http://maps.google.com/maps?q={latitude},{longitude}"})
return jsonify({'error':'last_position not available from api'})
return jsonify({'error': 'last_position not available from api'})
longitude, latitude, altitude = coordinates[:2]
if len(coordinates) == 3: # altitude is not always available
altitude = coordinates[2]
else:
altitude = None
return jsonify(
{"longitude": longitude, "latitude": latitude, "altitude": altitude,
"url": f"https://maps.google.com/maps?q={latitude},{longitude}"})
# Set a battery threshold and schedule an hour to stop the charge
@@ -136,7 +133,7 @@ def update_trips():
trips_by_vin = Trips.get_trips(myp.vehicles_list)
trips = next(iter(trips_by_vin.values())) # todo handle multiple car
chargings = MyPSACC.get_chargings()
except:
except (StopIteration, AssertionError):
logger.error("update_trips: %s", traceback.format_exc())
# update for slider
global min_date, max_date, min_millis, max_millis, step, marks
@@ -147,7 +144,7 @@ def update_trips():
max_millis = figures.unix_time_millis(max_date)
step = (max_millis - min_millis) / 100
marks = figures.get_marks_from_start_end(min_date, max_date)
except:
except (ValueError, IndexError):
logger.error("update_trips (slider): %s", traceback.format_exc())
@@ -186,11 +183,6 @@ except (IndexError, TypeError):
logger.debug("Failed to generate figure, there is probably not enough data yet %s", traceback.format_exc())
data_div = dbc.Alert("No data to show, there is probably no trips recorded yet", color="danger")
except:
logger.error("Failed to generate figure, there is probably not enough data yet")
logger.error(traceback.format_exc())
data_div = dbc.Alert("No data to show", color="danger")
dash_app.layout = dbc.Container(fluid=True, children=[
html.H1('My car info'),
data_div