mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-26 10:17:18 +00:00
code clean up and reduce algo complexity
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user