diff --git a/FAQ.md b/FAQ.md index fdb856c..f59b6ca 100644 --- a/FAQ.md +++ b/FAQ.md @@ -3,8 +3,10 @@ ### 1. Can't get car model The car model need to be known by the app. To find it, the app use the first 10 character of your VIN. If the car isn't in the list we need to add it, to do that you need to edit the file here: -### todo +Go to [car_models.yml](https://github.com/flobz/psa_car_controller/blob/develop/psa_car_controller/psacc/resources/car_models.yml) +and click on edit then copy cut and already existent model in the list and edit all properties that are incorrect for your model. +Finally, click on propose change. ### 2. Error during activation {'newversion': '2.0.0', 'newversionurl': 'http://m.inwebo.com/', 'err': 'NOK:FORBIDDEN'} Your psa account is locked because you makes 20 sms activation. To unlock do this : diff --git a/README.md b/README.md index ccdce4d..2132e91 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ You can modify a price manually in the dashboard. It can be useful if you use pu ## FAQ If you have a problem, or a question please check if the answer isn't in the [FAQ](FAQ.md). + +## Contribute If you need information to contribute or edit this program go [here](docs/Develop.md). ## Donation diff --git a/psa_car_controller/__main__.py b/psa_car_controller/__main__.py index fe926ee..150dc49 100755 --- a/psa_car_controller/__main__.py +++ b/psa_car_controller/__main__.py @@ -9,13 +9,14 @@ DIR = os.path.dirname(os.path.realpath(__file__)) if sys.version_info < (3, 7): raise RuntimeError("This application requires Python 3.7+") - # pylint: disable=wrong-import-position from psa_car_controller.psacc.application.car_controller import PSACarController from psa_car_controller import web + # noqa: MC0001 -if __name__ == "__main__": + +def main(): app = PSACarController() app.load_app() args = app.args @@ -23,3 +24,7 @@ if __name__ == "__main__": args=["My car info", args.base_path, logger.level < 20, args.listen, int(args.port)], daemon=True) t1.start() t1.join() + + +if __name__ == "__main__": + main() diff --git a/psa_car_controller/psa/otp/otp.py b/psa_car_controller/psa/otp/otp.py index 3e9ef02..1803c9c 100644 --- a/psa_car_controller/psa/otp/otp.py +++ b/psa_car_controller/psa/otp/otp.py @@ -305,7 +305,7 @@ def save_otp(obj, filename="otp.bin"): class RenameUnpickler(pickle.Unpickler): def find_class(self, module, name): - renamed_module = "psa_car_controller." + module.lower() + renamed_module = "psa_car_controller.psa." + module.lower() return super().find_class(renamed_module, name) diff --git a/psa_car_controller/psacc/model/car.py b/psa_car_controller/psacc/model/car.py index 6c5cca4..3a3fc6e 100644 --- a/psa_car_controller/psacc/model/car.py +++ b/psa_car_controller/psacc/model/car.py @@ -70,7 +70,7 @@ class Car: raise ValueError("ABRP model is not set") @property - def status(self): + def status(self) -> CarStatus: return self._status @status.setter @@ -78,7 +78,7 @@ class Car: self._status = value if self._status is not None and self.status.__class__ != CarStatus: self._status.__class__ = CarStatus - self._status.correct() + self._status.correct(self.is_electric()) def get_charge_speed(self, diff_elvel, duration_in_sec) -> float: duration_in_hour = duration_in_sec / 3600 diff --git a/psa_car_controller/psacc/model/car_status.py b/psa_car_controller/psacc/model/car_status.py index a399750..4901346 100644 --- a/psa_car_controller/psacc/model/car_status.py +++ b/psa_car_controller/psacc/model/car_status.py @@ -19,9 +19,9 @@ class CarStatus(Status): service=None, timed_odometer=None): # noqa: E501 super().__init__(embedded, links, battery, doors_state, energy, environment, ignition, kinetic, last_position, preconditionning, privacy, safety, service, timed_odometer) - self.correct() + self.correct(False) - def correct(self): + def correct(self, electric_car): try: if len(self.last_position.geometry.coordinates) < 2: raise AttributeError() @@ -39,6 +39,8 @@ class CarStatus(Status): if self.timed_odometer is None: self.timed_odometer = VehicleOdometer() + if electric_car: + self.get_energy("Fuel").level = None def is_moving(self): try: diff --git a/psa_car_controller/psacc/repository/db.py b/psa_car_controller/psacc/repository/db.py index 2b2eb84..0236bab 100644 --- a/psa_car_controller/psacc/repository/db.py +++ b/psa_car_controller/psacc/repository/db.py @@ -84,15 +84,20 @@ class Database: @staticmethod def init_db(conn): - conn.execute("""CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY, - VIN TEXT, longitude REAL, - latitude REAL, - mileage REAL, - level INTEGER, - level_fuel INTEGER, - moving BOOLEAN, - temperature INTEGER, - altitude INTEGER);""") + new_db = True + try: + conn.execute("""CREATE TABLE position (Timestamp DATETIME PRIMARY KEY, + VIN TEXT, longitude REAL, + latitude REAL, + mileage REAL, + level INTEGER, + level_fuel INTEGER, + moving BOOLEAN, + temperature INTEGER, + altitude INTEGER);""") + except sqlite3.OperationalError: + new_db = False + logger.debug("Database already exist") make_backup = False conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME,VIN TEXT, " "start_level INTEGER, end_level INTEGER, co2 INTEGER, kw INTEGER);") @@ -108,7 +113,7 @@ class Database: make_backup = True except sqlite3.OperationalError: pass - if make_backup: + if not new_db and make_backup: Database.backup(conn) conn.execute("DROP TRIGGER IF EXISTS update_trigger") Database.clean_battery(conn) @@ -237,7 +242,7 @@ class Database: conn = Database.get_db() if conn.execute("SELECT Timestamp from position where Timestamp=?", (date,)).fetchone() is None: temp = get_temp(latitude, longitude, weather_api) - if level_fuel == 0: # fix fuel level not provided when car is off + if level_fuel and level_fuel == 0: # fix fuel level not provided when car is off try: level_fuel = conn.execute( "SELECT level_fuel FROM position WHERE level_fuel>0 AND VIN=? ORDER BY Timestamp DESC " diff --git a/pyproject.toml b/pyproject.toml index 5b92c6b..88042ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,11 @@ version = "3.0.0" description = "This is a python program to control a psa car with connected_car v4 api." authors = ["Florian Bezannier "] license = "GPL-3.0" +homepage = "https://github.com/flobz/psa_car_controller" +repository = "https://github.com/flobz/psa_car_controller" +include = [ + "LICENSE", +] [tool.poetry.dependencies] python = "^3.7.0" @@ -37,12 +42,13 @@ prospector = ">=1.3.0" pre-commit = "^2.17.0" coverage = "^6.3.2" deepdiff = "^5.7.0" -selenium = "^4.1.3" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" +[tool.poetry.scripts] +psa-car-controller = 'psa_car_controller.__main__:main' [tool.autopep8] max_line_length = 120 diff --git a/tests/data/car_status.py b/tests/data/car_status.py new file mode 100644 index 0000000..bcdca40 --- /dev/null +++ b/tests/data/car_status.py @@ -0,0 +1,39 @@ +FUEL_CAR_STATUS = { + 'lastPosition': {'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [9.65457, 49.96119, 21]}, + 'properties': {'updatedAt': '2021-03-29T05:16:10Z', 'heading': 126, + 'type': 'Estimated'}}, 'preconditionning': { + 'airConditioning': {'updatedAt': '2021-04-01T16:17:01Z', 'status': 'Disabled', 'programs': [ + {'enabled': False, 'slot': 1, 'recurrence': 'Daily', 'start': 'PT21H40M', + 'occurence': {'day': ['Sat']}}]}}, + 'energy': [{'updatedAt': '2021-02-23T22:29:03Z', 'type': 'Fuel', 'level': 0}, + {'updatedAt': '2021-04-01T16:17:01Z', 'type': 'Electric', 'level': 70, 'autonomy': 192, + 'charging': {'plugged': True, 'status': 'InProgress', 'remainingTime': 'PT0S', + 'chargingRate': 20, 'chargingMode': 'Slow', 'nextDelayedTime': 'PT21H30M'}}], + 'createdAt': '2021-04-01T16:17:01Z', + 'battery': {'voltage': 99, 'current': 0, 'createdAt': '2021-04-01T16:17:01Z'}, + 'kinetic': {'createdAt': '2021-03-29T05:16:10Z', 'moving': False}, + 'privacy': {'createdAt': '2021-04-01T16:17:01Z', 'state': 'None'}, + 'service': {'type': 'Electric', 'updatedAt': '2021-02-23T21:10:29Z'}, + '_links': {'self': { + 'href': 'https://api.groupe-psa.com/connectedcar/v4/user/vehicles/myid/status'}, + 'vehicles': { + 'href': 'https://api.groupe-psa.com/connectedcar/v4/user/vehicles/myid'}}, + 'timed.odometer': {'createdAt': None, 'mileage': 1107.1}, 'updatedAt': '2021-04-01T16:17:01Z'} + +ELECTRIC_CAR_STATUS = { + "lastPosition": {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-1.59008, 47.274, 30]}, + "properties": {"updatedAt": "2021-03-29T06:22:51Z", "type": "Aquire", "signalQuality": 9}}, + "preconditionning": {"airConditioning": {"updatedAt": "2022-03-26T10:52:11Z", "status": "Disabled"}}, + "energy": [{"updatedAt": "2021-09-14T20:39:06Z", "type": "Fuel", "level": 0}, + {"updatedAt": "2022-03-26T11:02:54Z", "type": "Electric", "level": 59, "autonomy": 122, + "charging": {"plugged": False, "status": "Disconnected", "remainingTime": "PT0S", "chargingRate": 0, + "chargingMode": "No", "nextDelayedTime": "PT22H31M"}}], + "createdAt": "2022-03-26T11:02:54Z", + "battery": {"voltage": 83.5, "current": 0, "createdAt": "2022-03-26T10:52:11Z"}, + "kinetic": {"createdAt": "2021-03-29T06:22:51Z", "moving": True}, + "privacy": {"createdAt": "2022-03-26T11:02:53Z", "state": "None"}, + "service": {"type": "Electric", "updatedAt": "2022-03-26T11:02:54Z"}, "_links": {"self": { + "href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/aa/status"}, + "vehicles": { + "href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/aa"}}, + "timed.odometer": {"createdAt": None, "mileage": 3196.5}, "updatedAt": "2022-03-26T11:02:54Z"} diff --git a/tests/test_unit.py b/tests/test_unit.py index 52884a0..303e6a1 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -3,9 +3,10 @@ import json import os import unittest from datetime import datetime, timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import reverse_geocode +from dateutil.tz import tzutc from pytz import UTC from psa_car_controller import psa @@ -28,6 +29,7 @@ from psa_car_controller.psacc.repository.config_repository import ConfigReposito from psa_car_controller.psacc.repository.db import Database from psa_car_controller.psacc.repository.trips import Trips from psa_car_controller.psacc.utils.utils import get_temp +from tests.data.car_status import FUEL_CAR_STATUS, ELECTRIC_CAR_STATUS from tests.utils import DATA_DIR, record_position, latitude, longitude, date0, date1, date2, date3, record_charging, \ vehicule_list, get_new_test_db, get_date, date4 from psa_car_controller.web.figures import get_figures, get_battery_curve_fig, get_altitude_fig @@ -131,39 +133,36 @@ class TestUnit(unittest.TestCase): self.assertEqual(charge.price, res.price) def test_sdk(self): - res = { - 'lastPosition': {'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [9.65457, 49.96119, 21]}, - 'properties': {'updatedAt': '2021-03-29T05:16:10Z', 'heading': 126, - 'type': 'Estimated'}}, 'preconditionning': { - 'airConditioning': {'updatedAt': '2021-04-01T16:17:01Z', 'status': 'Disabled', 'programs': [ - {'enabled': False, 'slot': 1, 'recurrence': 'Daily', 'start': 'PT21H40M', - 'occurence': {'day': ['Sat']}}]}}, - 'energy': [{'updatedAt': '2021-02-23T22:29:03Z', 'type': 'Fuel', 'level': 0}, - {'updatedAt': '2021-04-01T16:17:01Z', 'type': 'Electric', 'level': 70, 'autonomy': 192, - 'charging': {'plugged': True, 'status': 'InProgress', 'remainingTime': 'PT0S', - 'chargingRate': 20, 'chargingMode': 'Slow', 'nextDelayedTime': 'PT21H30M'}}], - 'createdAt': '2021-04-01T16:17:01Z', - 'battery': {'voltage': 99, 'current': 0, 'createdAt': '2021-04-01T16:17:01Z'}, - 'kinetic': {'createdAt': '2021-03-29T05:16:10Z', 'moving': False}, - 'privacy': {'createdAt': '2021-04-01T16:17:01Z', 'state': 'None'}, - 'service': {'type': 'Electric', 'updatedAt': '2021-02-23T21:10:29Z'}, '_links': {'self': { - 'href': 'https://api.groupe-psa.com/connectedcar/v4/user/vehicles/myid/status'}, - 'vehicles': { - 'href': 'https://api.groupe-psa.com/connectedcar/v4/user/vehicles/myid'}}, - 'timed.odometer': {'createdAt': None, 'mileage': 1107.1}, 'updatedAt': '2021-04-01T16:17:01Z'} api = ApiClient() - status: psa.connected_car_api.models.status.Status = api._ApiClient__deserialize(res, "Status") + status: psa.connected_car_api.models.status.Status = api._ApiClient__deserialize(FUEL_CAR_STATUS, "Status") geocode_res = reverse_geocode.search([(status.last_position.geometry.coordinates[:2])[::-1]])[0] - assert geocode_res["country_code"] == "DE" get_new_test_db() car = Car("XX", "vid", "Peugeot") car.status = status myp = PSAClient.load_config(DATA_DIR + "config.json") myp.record_info(car) + assert geocode_res["country_code"] == "DE" assert "features" in json.loads(Database.get_recorded_position()) # electric should be first assert car.status.energy[0].type == 'Electric' + @patch("psa_car_controller.psacc.repository.db.Database.record_position") + def test_electric_record_info(self, mock_db): + api = ApiClient() + status: psa.connected_car_api.models.status.Status = api._ApiClient__deserialize(ELECTRIC_CAR_STATUS, "Status") + get_new_test_db() + car = self.vehicule_list[0] + car.status = status + myp = PSAClient.load_config(DATA_DIR + "config.json") + myp.record_info(car) + db_record_position_arg = mock_db.call_args_list[0][0] + expected_result = (None, 'VR3UHZKX', 3196.5, 47.274, -1.59008, 30, + datetime(2022, 3, 26, 11, 2, 54, tzinfo=tzutc()), + 59.0, + None, + True) + self.assertEqual(db_record_position_arg, expected_result) + def test_record_position_charging(self): get_new_test_db() config_repository.CONFIG_FILENAME = DATA_DIR + "config.ini"