From c3adb194a345f7d0c7df9c87f3cacab2722d06fe Mon Sep 17 00:00:00 2001 From: Florian Bezannier Date: Sun, 21 Mar 2021 19:24:16 +0100 Subject: [PATCH] code clean up and reduce algo complexity --- Car.py | 40 ++++++++--- ChargeControl.py | 100 ++++++++++++++------------ MyPSACC.py | 179 +++++++++++++++++------------------------------ Trip.py | 90 +++++++++--------------- otp/Otp.py | 71 +++++++++---------- otp/Tokenizer.py | 8 +-- otp/oaep.py | 46 ++++++------ server.py | 7 +- trip_parser.py | 64 +++++++++++++++++ utils.py | 41 +++++++++++ web/app.py | 4 +- web/db.py | 13 ++++ web/figures.py | 11 ++- web/views.py | 32 ++++----- 14 files changed, 387 insertions(+), 319 deletions(-) create mode 100644 trip_parser.py create mode 100644 utils.py diff --git a/Car.py b/Car.py index 3dabbb5..40dc381 100644 --- a/Car.py +++ b/Car.py @@ -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() diff --git a/ChargeControl.py b/ChargeControl.py index e66b308..5230a97 100644 --- a/ChargeControl.py +++ b/ChargeControl.py @@ -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) diff --git a/MyPSACC.py b/MyPSACC.py index 05b3965..8041a93 100644 --- a/MyPSACC.py +++ b/MyPSACC.py @@ -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 diff --git a/Trip.py b/Trip.py index 62b43a7..d79e373 100644 --- a/Trip.py +++ b/Trip.py @@ -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: diff --git a/otp/Otp.py b/otp/Otp.py index 699c84b..40c35a8 100644 --- a/otp/Otp.py +++ b/otp/Otp.py @@ -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) diff --git a/otp/Tokenizer.py b/otp/Tokenizer.py index a08a5f8..ce5ac1f 100644 --- a/otp/Tokenizer.py +++ b/otp/Tokenizer.py @@ -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): diff --git a/otp/oaep.py b/otp/oaep.py index 7cf14ea..ff4f90a 100644 --- a/otp/oaep.py +++ b/otp/oaep.py @@ -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' diff --git a/server.py b/server.py index 9e0d770..9ad97cb 100755 --- a/server.py +++ b/server.py @@ -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() diff --git a/trip_parser.py b/trip_parser.py new file mode 100644 index 0000000..19b991c --- /dev/null +++ b/trip_parser.py @@ -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) \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..1a93c7a --- /dev/null +++ b/utils.py @@ -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 \ No newline at end of file diff --git a/web/app.py b/web/app.py index 1d3bdcf..4374c98 100644 --- a/web/app.py +++ b/web/app.py @@ -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 diff --git a/web/db.py b/web/db.py index e7e3f2d..d54bac8 100644 --- a/web/db.py +++ b/web/db.py @@ -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() diff --git a/web/figures.py b/web/figures.py index 73e2872..7d7054d 100644 --- a/web/figures.py +++ b/web/figures.py @@ -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( diff --git a/web/views.py b/web/views.py index 806ebf4..f429786 100644 --- a/web/views.py +++ b/web/views.py @@ -52,7 +52,7 @@ def get_vehicules(): @app.route('/get_vehicleinfo/') -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