diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8ef0d3b..c83a11ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,27 +9,27 @@ on: workflow_dispatch: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Set up python id: setup-python uses: actions/setup-python@v2 with: python-version: 3.7 - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-in-project: true - uses: actions/checkout@v2 - name: Load cached venv id: cached-poetry-dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: .venv key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root + run: | + python -m pip install --upgrade pip + pip install poetry + sudo apt-get install libblas3 liblapack3 liblapack-dev libblas-dev gfortran libatlas-base-dev + poetry config --local virtualenvs.in-project true + poetry install --no-interaction - name: check quality run: | source .venv/bin/activate diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62388b7b..e9120377 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,13 @@ -default_language_version: - python: python3.7 repos: - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: 'v1.6.0' # Use the sha / tag you want to point at + rev: 'v2.0.4' # Use the sha / tag you want to point at + language: system hooks: - id: autopep8 language: system - args: - - "--exit-code 0" - repo: https://github.com/PyCQA/prospector - rev: 1.7.7 # The version of Prospector to use, if not 'master' for latest + rev: v1.10.3 # The version of Prospector to use, if not 'master' for latest hooks: - id: prospector - language: system \ No newline at end of file + language: system + diff --git a/.prospector.yaml b/.prospector.yaml index 7c1855a8..b23f5799 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -1,8 +1,11 @@ doc-warnings: false max-line-length: 120 +autodetect: false +uses: + - flask ignore-paths: - tests - - connected_car_api + - psa_car_controller/psa/connected_car_api pycodestyle: disable: - E722 diff --git a/config.ini b/config.ini index 1b16fcda..02a75995 100644 --- a/config.ini +++ b/config.ini @@ -1,9 +1,12 @@ [General] currency = € +# define format for data export, can be csv or xlsx +export_format = csv # minimum trip length in km so it's added to stats and map in website minimum trip length = 10 - +# for future use length unit = km +export format = csv [Electricity config] # price by kw/h day price = 0.15 diff --git a/psa_car_controller/__init__.py b/psa_car_controller/__init__.py index db8e3057..db5f8628 100644 --- a/psa_car_controller/__init__.py +++ b/psa_car_controller/__init__.py @@ -1,6 +1,5 @@ import sys - -if sys.version_info[:2] >= (3, 8): +if sys.version_info >= (3, 8): from importlib import metadata else: import importlib_metadata as metadata diff --git a/psa_car_controller/common/utils.py b/psa_car_controller/common/utils.py index 1ba973aa..b949a5bc 100644 --- a/psa_car_controller/common/utils.py +++ b/psa_car_controller/common/utils.py @@ -3,6 +3,7 @@ from typing import List import requests +TIMEOUT_IN_S = 10 def rate_limit(limit, every): @@ -16,7 +17,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) finally: # don't catch but ensure semaphore release timer = Timer(every, semaphore.release) - timer.setDaemon(True) + timer.daemon = True timer.start() else: raise RateLimitException @@ -56,5 +57,6 @@ def get_positions(locations): locations_str += str(line[latitude]) + "," + str(line[longitude]) + "|" locations_str = locations_str[:-1] res = requests.get("https://api.opentopodata.org/v1/srtm30m", - params={"locations": locations_str}) + params={"locations": locations_str}, + timeout=TIMEOUT_IN_S) return res.json()["results"] diff --git a/psa_car_controller/psa/RemoteClient.py b/psa_car_controller/psa/RemoteClient.py index 7e8d047a..65f0d59c 100644 --- a/psa_car_controller/psa/RemoteClient.py +++ b/psa_car_controller/psa/RemoteClient.py @@ -125,7 +125,7 @@ def __keep_mqtt(self): # avoid token expiration except RateLimitException: logger.exception("__keep_mqtt") t = threading.Timer(timeout, self.__keep_mqtt) - t.setDaemon(True) + t.daemon = True t.start() def veh_charge_request(self, vin, hour, minute, charge_type): diff --git a/psa_car_controller/psa/oauth.py b/psa_car_controller/psa/oauth.py index 7d7d03ef..26a45907 100644 --- a/psa_car_controller/psa/oauth.py +++ b/psa_car_controller/psa/oauth.py @@ -19,10 +19,8 @@ def __init__(self, service_information: ServiceInformation, proxies: Optional[di self.refresh_callbacks = [] def _grant_password_request_realm(self, login: str, password: str, realm: str) -> dict: - return dict(grant_type='password', - username=login, - scope=' '.join(self.service_information.scopes), - password=password, realm=realm) + return {"grant_type": 'password', "username": login, "scope": ' '.join(self.service_information.scopes), + "password": password, "realm": realm} def init_with_user_credentials_realm(self, login: str, password: str, realm: str): self._token_request(self._grant_password_request_realm(login, password, realm), True) diff --git a/psa_car_controller/psa/otp/otp.py b/psa_car_controller/psa/otp/otp.py index 1803c9c5..7dc01874 100644 --- a/psa_car_controller/psa/otp/otp.py +++ b/psa_car_controller/psa/otp/otp.py @@ -16,6 +16,8 @@ # pylint: disable=invalid-name CONFIG_NAME = "otp.bin" +TIMEOUT_IN_S = 10 + logger = logging.getLogger(__name__) @@ -161,7 +163,8 @@ def request(self, param, setup=False): }, params=param, proxies=self.proxies, - verify=self.proxies is None + verify=self.proxies is None, + timeout=TIMEOUT_IN_S ).text try: raw_xml = raw_xml[raw_xml.index("?>") + 2:] diff --git a/psa_car_controller/psa/setup/app_decoder.py b/psa_car_controller/psa/setup/app_decoder.py index d66be6fa..c6f4128a 100755 --- a/psa_car_controller/psa/setup/app_decoder.py +++ b/psa_car_controller/psa/setup/app_decoder.py @@ -16,6 +16,7 @@ APP_VERSION = "1.33.0" GITHUB_USER = "flobz" GITHUB_REPO = "psa_apk" +TIMEOUT_IN_S = 10 def get_content_from_apk(filename: str, country_code: str) -> ApkParser: @@ -42,7 +43,8 @@ def firstLaunchConfig(package_name, client_email, client_password, country_code, "fields": {"USR_EMAIL": {"value": client_email}, "USR_PASSWORD": {"value": client_password}} } - )} + )}, + timeout=TIMEOUT_IN_S ) token = res.json()["accessToken"] @@ -54,7 +56,7 @@ def firstLaunchConfig(package_name, client_email, client_password, country_code, except BaseException: pass logger.error(msg) - raise Exception(msg) from ex + raise ConnectionError(msg) from ex try: res2 = requests.post( f"https://mw-{BRAND[package_name]['brand_code'].lower()}-m2c.mym.awsmpsa.com/api/v1/user", @@ -73,6 +75,7 @@ def firstLaunchConfig(package_name, client_email, client_password, country_code, "Version": APP_VERSION }, cert=("certs/public.pem", "certs/private.pem"), + timeout=TIMEOUT_IN_S ) res_dict = res2.json()["success"] @@ -84,7 +87,7 @@ def firstLaunchConfig(package_name, client_email, client_password, country_code, except BaseException: pass logger.error(msg) - raise Exception(msg) from ex + raise ConnectionError(msg) from ex # Psacc psacc = PSAClient(None, apk_parser.client_id, apk_parser.client_secret, None, customer_id, BRAND[package_name]["realm"], @@ -94,7 +97,7 @@ def firstLaunchConfig(package_name, client_email, client_password, country_code, res = psacc.get_vehicles() if len(res) == 0: - Exception("No vehicle in your account is compatible with this API, you vehicle is probably too old...") + raise ValueError("No vehicle in your account is compatible with this API, you vehicle is probably too old...") for vehicle in res_dict["vehicles"]: car = psacc.vehicles_list.get_car_by_vin(vehicle["vin"]) diff --git a/psa_car_controller/psa/setup/github.py b/psa_car_controller/psa/setup/github.py index a4f9fce7..86ad39fa 100644 --- a/psa_car_controller/psa/setup/github.py +++ b/psa_car_controller/psa/setup/github.py @@ -4,10 +4,12 @@ import requests logger = logging.getLogger(__name__) +TIMEOUT_IN_S = 10 def get_github_sha_from_file(user, repo, directory, filename): - res = requests.get("https://api.github.com/repos/{}/{}/git/trees/main:{}".format(user, repo, directory)).json() + res = requests.get("https://api.github.com/repos/{}/{}/git/trees/main:{}".format(user, repo, directory), + timeout=TIMEOUT_IN_S).json() try: file_info = next((file for file in res["tree"] if file['path'] == filename)) except KeyError as e: @@ -36,12 +38,14 @@ def github_file_need_to_be_downloaded(user, repo, directory, filename): def urlretrieve_from_github(user, repo, directory, filename, branch="main"): if github_file_need_to_be_downloaded(user, repo, directory, filename): with open(filename, 'wb') as f: - r = requests.get("https://github.com/{}/{}/raw/{}/{}{}".format(user, repo, branch, directory, filename), + url = "https://github.com/{}/{}/raw/{}/{}{}".format(user, repo, branch, directory, filename) + r = requests.get(url, headers={ "Accept": "application/vnd.github.VERSION.raw" - }, - stream=True - ) + }, + stream=True, + timeout=TIMEOUT_IN_S + ) r.raise_for_status() for chunk in r.iter_content(1024): diff --git a/psa_car_controller/psacc/application/abrp.py b/psa_car_controller/psacc/application/abrp.py index f6ed7ad2..3b5e7cd8 100644 --- a/psa_car_controller/psacc/application/abrp.py +++ b/psa_car_controller/psacc/application/abrp.py @@ -8,6 +8,7 @@ from psa_car_controller.psacc.model.car import Car logger = logging.getLogger(__name__) +TIMEOUT_IN_S = 10 class Abrp: @@ -47,7 +48,7 @@ def call(self, car: Car, ext_temp: float = None): tlm["ext_temp"] = ext_temp params = {"tlm": json.dumps(tlm), "token": self.token, "api_key": self.api_key} response = requests.request("POST", self.url, params=params, proxies=self.proxies, - verify=self.proxies is None) + verify=self.proxies is None, timeout=TIMEOUT_IN_S) logger.debug(response.text) try: return json.loads(response.text)["status"] == "ok" diff --git a/psa_car_controller/psacc/application/battery_charge_curve.py b/psa_car_controller/psacc/application/battery_charge_curve.py index 4fdef896..72ee5f02 100644 --- a/psa_car_controller/psacc/application/battery_charge_curve.py +++ b/psa_car_controller/psacc/application/battery_charge_curve.py @@ -7,6 +7,9 @@ from psa_car_controller.psacc.model.charge import Charge from psa_car_controller.psacc.repository.db import Database +DEFAULT_KM_BY_KW = 5.3 +MINIMUM_AUTONOMY_FOR_GOOD_RESULT = 20 + class BatteryChargeCurve: def __init__(self, level, speed): @@ -23,7 +26,10 @@ def dto_to_battery_curve(car: Car, charge: Charge, battery_curves_dto: List[Batt battery_curves = [] if len(battery_curves_dto) > 0 and battery_curves_dto[-1].level > 0 and battery_curves_dto[-1].autonomy > 0: battery_capacity = battery_curves_dto[-1].level * car.battery_power / 100 - km_by_kw = 0.8 * battery_curves_dto[-1].autonomy / battery_capacity + if battery_curves_dto[-1].autonomy > MINIMUM_AUTONOMY_FOR_GOOD_RESULT: + km_by_kw = 0.8 * battery_curves_dto[-1].autonomy / battery_capacity + else: + km_by_kw = DEFAULT_KM_BY_KW start = 0 speeds = [] @@ -51,7 +57,7 @@ def speed_in_kw_from_km(battery_curve_dto): start = end speeds = [] battery_curves.append(BatteryChargeCurve(charge.end_level, 0)) - elif charge.end_level and charge.start_level: + elif charge.end_level and charge.start_level is not None: speed = car.get_charge_speed(charge.end_level - charge.start_level, (stop_at - start_date).total_seconds()) battery_curves.append(BatteryChargeCurve(charge.start_level, speed)) battery_curves.append(BatteryChargeCurve(charge.end_level, speed)) diff --git a/psa_car_controller/psacc/application/car_controller.py b/psa_car_controller/psacc/application/car_controller.py index c09df1ca..5ab478f5 100644 --- a/psa_car_controller/psacc/application/car_controller.py +++ b/psa_car_controller/psacc/application/car_controller.py @@ -1,6 +1,7 @@ import argparse import atexit import logging +import socket import threading from os import environ, path @@ -65,10 +66,14 @@ def start_remote_control(self): self.chc = ChargeControls.load_config(self.myp, name=self.args.charge_control) self.chc.init() self.myp.start_refresh_thread() + except socket.timeout: + logger.error("Can't connect to mqtt broker your are not connected to internet or PSA MQTT server is " + "down !") except ConfigException: logger.error("start_remote_control failed redo otp config") def load_app(self) -> bool: + # pylint: disable=too-many-branches my_logger(handler_level=int(self.args.debug)) if self.args.config: self.config_name = self.args.config @@ -99,7 +104,10 @@ def load_app(self) -> bool: self.is_good = True else: self.is_good = False - logger.error("Please reconnect by going to config web page") + if self.args.web_conf: + logger.error("Please reconnect by going to config web page") + else: + logger.error("Connection need to be updated, Please redo authentication process.") if self.args.refresh: self.myp.info_refresh_rate = self.args.refresh * 60 if self.is_good: diff --git a/psa_car_controller/psacc/application/charge_control.py b/psa_car_controller/psacc/application/charge_control.py index 6083c412..d20a513f 100644 --- a/psa_car_controller/psacc/application/charge_control.py +++ b/psa_car_controller/psacc/application/charge_control.py @@ -101,7 +101,7 @@ def process(self): if next_in_second < self.psacc.info_refresh_rate: periodicity = next_in_second thread = threading.Timer(periodicity, self.process) - thread.setDaemon(True) + thread.daemon = True thread.start() elif status == STOPPED and has_threshold and hit_threshold and self.__is_approaching_scheduled_time(now): logger.info("Approaching scheduled charging time, but should not charge. Postponing charge hour!") diff --git a/psa_car_controller/psacc/application/ecomix.py b/psa_car_controller/psacc/application/ecomix.py index 6ed92837..15da2eab 100644 --- a/psa_car_controller/psacc/application/ecomix.py +++ b/psa_car_controller/psacc/application/ecomix.py @@ -12,7 +12,7 @@ CO2_SIGNAL_REQ_INTERVAL = 600 CO2_SIGNAL_URL = "https://api.co2signal.com" - +TIMEOUT_IN_S = 10 logger = logging.getLogger(__name__) @@ -31,7 +31,8 @@ def get_data_france(start, end) -> Union[float, None]: headers={ "Origin": "https://www.rte-france.com", "Referer": "https://www.rte-france.com/eco2mix/les-emissions-de-co2-par-kwh-produit-en-france", - } + }, + timeout=TIMEOUT_IN_S ) except RequestException: logger.exception("get_data_france: ") @@ -71,7 +72,8 @@ def get_data_from_co2_signal(latitude, longitude, country_code_default): return False res = requests.get(CO2_SIGNAL_URL + "/v1/latest", headers={"auth-token": Ecomix.co2_signal_key}, - params={"countryCode": country_code}) + params={"countryCode": country_code}, + timeout=TIMEOUT_IN_S) data = res.json() value = data["data"]["carbonIntensity"] assert isinstance(value, numbers.Number) diff --git a/psa_car_controller/psacc/application/psa_client.py b/psa_car_controller/psacc/application/psa_client.py index a3f1a6f9..7356517f 100644 --- a/psa_car_controller/psacc/application/psa_client.py +++ b/psa_car_controller/psacc/application/psa_client.py @@ -86,7 +86,7 @@ def api(self) -> VehiclesApi: def set_proxies(self, proxies): if proxies is None: - proxies = dict(http='', https='') + proxies = {"http": '', "https": ''} self.api_config.proxy = None else: self.api_config.proxy = proxies['http'] @@ -117,7 +117,7 @@ def __refresh_vehicle_info(self): if self.refresh_thread and self.refresh_thread.is_alive(): logger.debug("refresh_vehicle_info: precedent task still alive") self.refresh_thread = threading.Timer(self.info_refresh_rate, self.__refresh_vehicle_info) - self.refresh_thread.setDaemon(True) + self.refresh_thread.daemon = True self.refresh_thread.start() try: logger.debug("refresh_vehicle_info") @@ -162,7 +162,7 @@ def save_config(self, name=None, force=False): def load_config(name="config.json"): with open(name, "r", encoding="utf-8") as f: config_str = f.read() - config = dict(**json.loads(config_str)) + config = {**json.loads(config_str)} if "country_code" not in config: config["country_code"] = input("What is your country code ? (ex: FR, GB, DE, ES...)\n") for new_el in ["abrp", "co2_signal_api"]: diff --git a/psa_car_controller/psacc/resources/car_models.yml b/psa_car_controller/psacc/resources/car_models.yml index 26e4f837..bfc98da7 100644 --- a/psa_car_controller/psacc/resources/car_models.yml +++ b/psa_car_controller/psacc/resources/car_models.yml @@ -96,13 +96,6 @@ abrp_name: opel:comboe:22:50:peugeot reg: VR3EZZKXZ.* max_elec_consumption: 70 -- !ElecModel - name: e-Berlingo - battery_power: 50 - fuel_capacity: 0 - abrp_name: citroen:berlingoe:11:100:peugeot - reg: VR7EZZKXZP.* - max_elec_consumption: 100 - !ElecModel name: ION battery_power: 14.5 @@ -380,12 +373,3 @@ abrp_name: opel:comboe:22:50 reg: W0VEZZKXZN.* max_elec_consumption: 45 -- !CarModel - name: C5X hybrid - battery_power: 12.4 - fuel_capacity: 40 - abrp_name: - reg: VR7NDDGYPN.* - max_elec_consumption: 70 - max_fuel_consumption: 30 - diff --git a/psa_car_controller/psacc/utils/utils.py b/psa_car_controller/psacc/utils/utils.py index 3fdce387..2729511a 100644 --- a/psa_car_controller/psacc/utils/utils.py +++ b/psa_car_controller/psacc/utils/utils.py @@ -4,6 +4,7 @@ import requests logger = logging.getLogger(__name__) +TIMEOUT_IN_S = 10 def get_temp(latitude: str, longitude: str, api_key: str) -> float: @@ -13,7 +14,8 @@ def get_temp(latitude: str, longitude: str, api_key: str) -> float: params={"lat": latitude, "lon": longitude, "exclude": "minutely,hourly,daily,alerts", "appid": api_key, - "units": "metric"}) + "units": "metric"}, + timeout=TIMEOUT_IN_S) temp = weather_rep.json()["current"]["temp"] logger.debug("Temperature :%fc", temp) return temp diff --git a/psa_car_controller/web/app.py b/psa_car_controller/web/app.py index 9a846547..977933f4 100644 --- a/psa_car_controller/web/app.py +++ b/psa_car_controller/web/app.py @@ -1,5 +1,6 @@ import locale import logging +import sys import dash_bootstrap_components as dbc from flask import Flask @@ -14,7 +15,10 @@ from werkzeug import DispatcherMiddleware from psa_car_controller.common.mylogger import file_handler -import importlib +if sys.version_info >= (3, 8): + import importlib +else: + import importlib_metadata as importlib # pylint: disable=invalid-name app = None diff --git a/psa_car_controller/web/view/control.py b/psa_car_controller/web/view/control.py index 5a4b2c3a..3c55a10c 100644 --- a/psa_car_controller/web/view/control.py +++ b/psa_car_controller/web/view/control.py @@ -33,7 +33,7 @@ def get_control_tabs(config): myp: PSAClient = config.myp el = [] buttons_row = [] - if config.remote_control: + if config.remote_control and car.status is not None: try: preconditionning_state = car.status.preconditionning.air_conditioning.status != "Disabled" charging_state = car.status.get_energy('Electric').charging.status == "InProgress" diff --git a/pyproject.toml b/pyproject.toml index 2abcf3ea..e47f5e4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ include = [ ] [tool.poetry.dependencies] -python = ">=3.7.0, <4.0.0" +python = ">=3.7.2, <4.0.0" paho-mqtt = ">=1.5.0" dash = ">=2.9.0, <3.0.0" dash-daq = "^0.5.0" @@ -36,13 +36,21 @@ six = ">=1.10" python-dateutil = ">=2.5.3" urllib3 = ">=1.15.1 <2.0.0" importlib-metadata = {version = ">=1.7.0", python = "<3.8"} +pandas = "^1.1.5" +numpy = [{version = ">=1.24.0", python = ">=3.11"}, + {version = "<1.26.0", python = "<3.9"}, + {version = "<1.22.0", python = "<3.8"}] +scipy = [{version = ">=1.9.2", python = ">=3.11"}, + {version = "<1.11.0", python = "<3.8"}, + {version = "<1.8.0", python = "<3.8"}] [tool.poetry.dev-dependencies] -prospector = ">=1.3.0" +prospector = "1.10.3" pre-commit = "^2.17.0" coverage = "^6.3.2" deepdiff = "^5.7.0" greenery = "^3.3.5" +autopep8 = "2.0.4" [build-system] requires = ["poetry-core>=1.0.0"]