diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e91048b..7bb92d79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,20 +2,37 @@ # See https://pre-commit.com/hooks.html for more hooks default_language_version: python: python3.9 + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer + - id: check-json + - id: check-toml - id: check-yaml - id: check-added-large-files + - id: debug-statements + - id: end-of-file-fixer + - id: no-commit-to-branch + args: ['--branch', 'main', '--branch', 'master'] + - id: requirements-txt-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black rev: '22.1.0' hooks: - id: black + - repo: https://github.com/pycqa/flake8 rev: '4.0.1' hooks: - id: flake8 additional_dependencies: [ flake8-docstrings ] + + - repo: https://github.com/asottile/pyupgrade + rev: v2.31.0 + hooks: + - id: pyupgrade + args: [ + '--py37-plus' + ] diff --git a/pyproject.toml b/pyproject.toml index 7c0811e9..c90b2f9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,7 @@ requires = [ "setuptools>=45", "wheel>=0.37.0", - "setuptools_scm>=6.x", - "pytest>=6,<8", + "setuptools_scm>=6.0", ] build-backend = "setuptools.build_meta" diff --git a/requirements-test.txt b/requirements-test.txt index 2a545fef..78bbcd62 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,11 +1,11 @@ -r requirements.txt -pytest>=7.0.0 -setuptools -pytest-asyncio +black flake8 flake8-docstrings +freezegun>=1.0.0 pre-commit +pytest>=7.0.0 +pytest-asyncio pytest-cov pytest-subtests -black -freezegun>=1.0.0 +setuptools diff --git a/requirements.txt b/requirements.txt index 4ac69efd..bfbe4fa3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -lxml -beautifulsoup4 aiohttp +beautifulsoup4 +lxml pyjwt diff --git a/tests/conftest.py b/tests/conftest.py index ce9c654c..4302db80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +"""Configure tests.""" import sys pytest_plugins = ["pytest_cov"] diff --git a/tests/dummy_test.py b/tests/dummy_test.py index 078a92c1..adb2a1ec 100644 --- a/tests/dummy_test.py +++ b/tests/dummy_test.py @@ -1,15 +1,14 @@ +"""Dummy tests. Might be removed once there are proper ones.""" import pytest from aiohttp import ClientSession -# we need to change os path to be able to import volkswagecarnet from volkswagencarnet import vw_connection @pytest.mark.asyncio async def test_volkswagencarnet(): + """Dummy test to ensure logged in status is false by default.""" async with ClientSession() as session: connection = vw_connection.Connection(session, "test@example.com", "test_password") # if await connection._login(): - if not connection.logged_in: - return True - pytest.fail("Something happend we should have got a False from vw.logged_in") + assert connection.logged_in is False diff --git a/tests/fixtures/connection.py b/tests/fixtures/connection.py index 3582f6c2..24920eb5 100644 --- a/tests/fixtures/connection.py +++ b/tests/fixtures/connection.py @@ -1,3 +1,4 @@ +"""Session and connection related test fixtures.""" import os from pathlib import Path @@ -13,7 +14,7 @@ @pytest_asyncio.fixture async def session(): - """Client session that can be used in tests""" + """Client session that can be used in tests.""" jar = CookieJar() jar.load(os.path.join(resource_path, "dummy_cookies.pickle")) sess = ClientSession(headers={"Connection": "keep-alive"}, cookie_jar=jar) @@ -23,5 +24,5 @@ async def session(): @pytest.fixture def connection(session): - """Real connection for integration tests""" + """Real connection for integration tests.""" return Connection(session=session, username="", password="", country="DE", interval=999, fulldebug=True) diff --git a/tests/integration_test.py b/tests/integration_test.py index 52bed146..24f82332 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1,3 +1,12 @@ +""" +Integration tests. + +These tests use actual credentials, and should thus be used with care. +Credentials have to be specified in credentials.py. +""" +import logging +from unittest import skip + import pytest from aiohttp import ClientSession @@ -7,7 +16,6 @@ from credentials import username, password, spin, vin except ImportError: username = password = spin = vin = None - pass @pytest.mark.skipif( @@ -15,9 +23,22 @@ ) @pytest.mark.asyncio async def test_successful_login(): + """Test that login succeeds.""" async with ClientSession() as session: connection = vw_connection.Connection(session, username, password) await connection.doLogin() - if connection.logged_in: - return True - pytest.fail("Login failed") + assert connection.logged_in is True + + +@pytest.mark.skipif( + username is None or password is None, reason="Username or password is not set. Check credentials.py.sample" +) +@pytest.mark.asyncio +@skip("Not yet implemented") +async def test_spin_action(): + """ + Test something that uses s-pin. + + Not yet implemented... + """ + logging.getLogger().debug(f"using vin: {vin} and s-pin: {spin}") diff --git a/tests/vw_connection_test.py b/tests/vw_connection_test.py index a93ded6f..10bd136b 100644 --- a/tests/vw_connection_test.py +++ b/tests/vw_connection_test.py @@ -1,13 +1,15 @@ """Tests for main connection class.""" import sys -import unittest + +from volkswagencarnet import vw_connection if sys.version_info >= (3, 8): # This won't work on python versions less than 3.8 from unittest import IsolatedAsyncioTestCase else: + from unittest import TestCase - class IsolatedAsyncioTestCase(unittest.TestCase): + class IsolatedAsyncioTestCase(TestCase): """Dummy class to use instead (tests might need to skipped separately also).""" pass @@ -18,10 +20,6 @@ class IsolatedAsyncioTestCase(unittest.TestCase): import pytest -import volkswagencarnet.vw_connection -from volkswagencarnet.vw_connection import Connection -from volkswagencarnet.vw_vehicle import Vehicle - @pytest.mark.skipif(condition=sys.version_info < (3, 8), reason="Test incompatible with Python < 3.8") def test_clear_cookies(connection): @@ -63,13 +61,13 @@ async def update(self): @property def vehicles(self): """Return the vehicles.""" - vehicle1 = Vehicle(None, "vin1") - vehicle2 = Vehicle(None, "vin2") + vehicle1 = vw_connection.Vehicle(None, "vin1") + vehicle2 = vw_connection.Vehicle(None, "vin2") return [vehicle1, vehicle2] @pytest.mark.asyncio - @patch.object(volkswagencarnet.vw_connection.logging, "basicConfig") - @patch("volkswagencarnet.vw_connection.Connection", spec_set=Connection, new=FailingLoginConnection) + @patch.object(vw_connection.logging, "basicConfig") + @patch("volkswagencarnet.vw_connection.Connection", spec_set=vw_connection.Connection, new=FailingLoginConnection) @pytest.mark.skipif(condition=sys.version_info < (3, 8), reason="Test incompatible with Python < 3.8") async def test_main_argv(self, logger_config): """Test verbosity flags.""" @@ -86,27 +84,27 @@ async def test_main_argv(self, logger_config): for c in cases: args = ["dummy"] args.extend(c[1]) - with patch.object(volkswagencarnet.vw_connection.sys, "argv", args), self.subTest(msg=c[0]): - await volkswagencarnet.vw_connection.main() + with patch.object(vw_connection.sys, "argv", args), self.subTest(msg=c[0]): + await vw_connection.main() logger_config.assert_called_with(level=c[2]) logger_config.reset() @pytest.mark.asyncio @patch("sys.stdout", new_callable=StringIO) - @patch("volkswagencarnet.vw_connection.Connection", spec_set=Connection, new=FailingLoginConnection) + @patch("volkswagencarnet.vw_connection.Connection", spec_set=vw_connection.Connection, new=FailingLoginConnection) @pytest.mark.skipif(condition=sys.version_info < (3, 8), reason="Test incompatible with Python < 3.8") async def test_main_output_failed(self, stdout: StringIO): """Verify empty stdout on failed login.""" - await volkswagencarnet.vw_connection.main() + await vw_connection.main() assert stdout.getvalue() == "" @pytest.mark.asyncio @patch("sys.stdout", new_callable=StringIO) - @patch("volkswagencarnet.vw_connection.Connection", spec_set=Connection, new=TwoVehiclesConnection) + @patch("volkswagencarnet.vw_connection.Connection", spec_set=vw_connection.Connection, new=TwoVehiclesConnection) @pytest.mark.skipif(condition=sys.version_info < (3, 8), reason="Test incompatible with Python < 3.8") async def test_main_output_two_vehicles(self, stdout: StringIO): """Get console output for two vehicles.""" - await volkswagencarnet.vw_connection.main() + await vw_connection.main() assert ( stdout.getvalue() == """Vehicle id: vin1 diff --git a/tests/vw_exceptions_test.py b/tests/vw_exceptions_test.py new file mode 100644 index 00000000..0cfcfaea --- /dev/null +++ b/tests/vw_exceptions_test.py @@ -0,0 +1,13 @@ +"""Misc tests for exception and their handling.""" +from unittest import TestCase + +from volkswagencarnet.vw_exceptions import AuthenticationException + + +class ExceptionTests(TestCase): + """Unit tests for exceptions.""" + + def test_auth_exception(self): + """Test that message matches. Dummy test.""" + ex = AuthenticationException("foo failed") + self.assertEqual("foo failed", ex.__str__()) diff --git a/tests/vw_utilities_test.py b/tests/vw_utilities_test.py index 2a377c0e..d4a7bf09 100644 --- a/tests/vw_utilities_test.py +++ b/tests/vw_utilities_test.py @@ -1,13 +1,12 @@ -import unittest from datetime import datetime, timezone, timedelta from json import JSONDecodeError -from unittest import mock +from unittest import TestCase, mock from unittest.mock import DEFAULT from volkswagencarnet.vw_utilities import camel2slug, is_valid_path, obj_parser, json_loads, read_config -class UtilitiesTest(unittest.TestCase): +class UtilitiesTest(TestCase): def test_camel_to_slug(self): data = {"foo": "foo", "fooBar": "foo_bar", "XYZ": "x_y_z", "B4R": "b4_r"} # Should this actually be "b_4_r"? =) for v in data: diff --git a/tests/vw_vehicle_test.py b/tests/vw_vehicle_test.py index 333b7d14..d5569d35 100644 --- a/tests/vw_vehicle_test.py +++ b/tests/vw_vehicle_test.py @@ -1,17 +1,20 @@ +"""Vehicle class tests.""" import sys -import unittest from datetime import datetime -# This won't work on python versions less than 3.8 if sys.version_info >= (3, 8): + # This won't work on python versions less than 3.8 from unittest import IsolatedAsyncioTestCase else: + from unittest import TestCase + + class IsolatedAsyncioTestCase(TestCase): + """Python 3.7 compatibility dummy class.""" - class IsolatedAsyncioTestCase(unittest.TestCase): pass -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from aiohttp import ClientSession from freezegun import freeze_time @@ -20,8 +23,11 @@ class IsolatedAsyncioTestCase(unittest.TestCase): class VehicleTest(IsolatedAsyncioTestCase): + """Test Vehicle methods.""" + @freeze_time("2022-02-14 03:04:05") async def test_init(self): + """Test that init does what it should.""" async with ClientSession() as conn: target_date = datetime.fromisoformat("2022-02-14 03:04:05") url = "https://foo.bar" @@ -63,9 +69,11 @@ async def test_init(self): ) def test_discover(self): + """Test the discovery process.""" pass async def test_update_deactivated(self): + """Test that calling update on a deactivated Vehicle does nothing.""" vehicle = MagicMock(spec=Vehicle, name="MockDeactivatedVehicle") vehicle.update = lambda: Vehicle.update(vehicle) vehicle._discovered = True @@ -78,6 +86,7 @@ async def test_update_deactivated(self): self.assertEqual(0, vehicle.method_calls.__len__(), f"xpected none, got {vehicle.method_calls}") async def test_update(self): + """Test that update calls the wanted methods and nothing else.""" vehicle = MagicMock(spec=Vehicle, name="MockUpdateVehicle") vehicle.update = lambda: Vehicle.update(vehicle) @@ -98,3 +107,15 @@ async def test_update(self): self.assertEqual( 8, vehicle.method_calls.__len__(), f"Wrong number of methods called. Expected 8, got {vehicle.method_calls}" ) + + async def test_json(self): + """Test that update calls the wanted methods and nothing else.""" + vehicle = Vehicle(conn=None, url="dummy34") + + vehicle._discovered = True + dtstring = "2022-02-22T02:22:20+02:00" + d = datetime.fromisoformat(dtstring) + + with patch.dict(vehicle.attrs, {"a string": "yay", "some date": d}): + res = f"{vehicle.json}" + self.assertEqual('{\n "a string": "yay",\n "some date": "2022-02-22T02:22:20+02:00"\n}', res) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index 61d1d654..47bbccc4 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """Communicate with We Connect services.""" -import base64 import re import secrets import sys @@ -17,13 +15,13 @@ from datetime import timedelta, datetime from urllib.parse import urljoin, parse_qs, urlparse from json import dumps as to_json -import aiohttp from bs4 import BeautifulSoup -from base64 import b64encode +from base64 import b64encode, urlsafe_b64encode -from aiohttp import ClientSession, ClientTimeout +from aiohttp import ClientSession, ClientTimeout, client_exceptions from aiohttp.hdrs import METH_GET, METH_POST +from volkswagencarnet.vw_exceptions import AuthenticationException from .vw_utilities import json_loads, read_config from .vw_vehicle import Vehicle @@ -136,6 +134,11 @@ async def _login(self, client="Legacy"): # Helper functions def getNonce(): + """ + Get a random nonce. + + :return: + """ ts = "%d" % (time.time()) sha256 = hashlib.sha256() sha256.update(ts.encode()) @@ -143,13 +146,13 @@ def getNonce(): return b64encode(sha256.digest()).decode("utf-8")[:-1] def base64URLEncode(s): - return base64.urlsafe_b64encode(s).rstrip(b"=") - - def extract_csrf(req): - return re.compile('').search(req).group(1) + """ + Encode string as Base 64 in a URL safe way, stripping trailing '='. - def extract_guest_language_id(req): - return req.split("_")[1].lower() + :param s: + :return: + """ + return urlsafe_b64encode(s).rstrip(b"=") # Login starts here try: @@ -227,12 +230,10 @@ def extract_guest_language_id(req): try: response_data = await req.text() response_soup = BeautifulSoup(response_data, "html.parser") - mailform = dict( - [ - (t["name"], t["value"]) - for t in response_soup.find("form", id="emailPasswordForm").find_all("input", type="hidden") - ] - ) + mailform = { + t["name"]: t["value"] + for t in response_soup.find("form", id="emailPasswordForm").find_all("input", type="hidden") + } mailform["email"] = self._session_auth_username pe_url = auth_issuer + response_soup.find("form", id="emailPasswordForm").get("action") except Exception as e: @@ -316,10 +317,9 @@ def extract_guest_language_id(req): _LOGGER.warning("Login failed, invalid password") else: _LOGGER.warning(f"Login failed: {error}") - raise error + raise AuthenticationException(error) if "code" in ref: _LOGGER.debug("Got code: %s" % ref) - pass else: _LOGGER.debug("Exception occurred while logging in.") raise e @@ -484,7 +484,7 @@ async def _request(self, method, url, **kwargs): _LOGGER.debug(f"Not success status code [{response.status}] response: {response}") if "X-RateLimit-Remaining" in response.headers: res["rate_limit_remaining"] = response.headers.get("X-RateLimit-Remaining", "") - except: + except Exception: res = {} _LOGGER.debug(f"Something went wrong [{response.status}] response: {response}") return res @@ -500,7 +500,7 @@ async def get(self, url, vin=""): try: response = await self._request(METH_GET, self._make_url(url, vin)) return response - except aiohttp.client_exceptions.ClientResponseError as error: + except client_exceptions.ClientResponseError as error: if error.status == 401: _LOGGER.warning(f'Received "unauthorized" error while fetching data: {error}') self._session_logged_in = False @@ -558,7 +558,7 @@ async def update(self): await asyncio.gather(*updatelist) return True - except (IOError, OSError, LookupError, Exception) as error: + except (OSError, LookupError, Exception) as error: _LOGGER.warning(f"Could not update information: {error}") return False @@ -673,13 +673,11 @@ async def getVehicleStatusData(self, vin): ): data = { "StoredVehicleDataResponse": response.get("StoredVehicleDataResponse", {}), - "StoredVehicleDataResponseParsed": dict( - [ - (e["id"], e if "value" in e else "") - for f in [s["field"] for s in response["StoredVehicleDataResponse"]["vehicleData"]["data"]] - for e in f - ] - ), + "StoredVehicleDataResponseParsed": { + e["id"]: e if "value" in e else "" + for f in [s["field"] for s in response["StoredVehicleDataResponse"]["vehicleData"]["data"]] + for e in f + }, } return data elif response.get("status_code", {}): @@ -927,7 +925,7 @@ async def dataCall(self, query, vin="", **data): response = await self.post(query, vin=vin, **data) _LOGGER.debug(f"Data call returned: {response}") return response - except aiohttp.client_exceptions.ClientResponseError as error: + except client_exceptions.ClientResponseError as error: if error.status == 401: _LOGGER.error("Unauthorized") self._session_logged_in = False @@ -1250,6 +1248,7 @@ def vehicles(self): def logged_in(self): """ Return cached logged in state. + Not actually checking anything. """ return self._session_logged_in @@ -1273,7 +1272,7 @@ async def validate_login(self): return False return True - except (IOError, OSError) as error: + except OSError as error: _LOGGER.warning("Could not validate login: %s", error) return False diff --git a/volkswagencarnet/vw_dashboard.py b/volkswagencarnet/vw_dashboard.py index 0ca400fb..2a7344e6 100644 --- a/volkswagencarnet/vw_dashboard.py +++ b/volkswagencarnet/vw_dashboard.py @@ -4,6 +4,8 @@ import logging from .vw_utilities import camel2slug +CLIMA_DEFAULT_DURATION = 30 + _LOGGER = logging.getLogger(__name__) @@ -42,7 +44,7 @@ def vehicle_name(self): @property def full_name(self): - return "%s %s" % (self.vehicle_name, self.name) + return f"{self.vehicle_name} {self.name}" @property def is_mutable(self): @@ -104,9 +106,9 @@ def configurate(self, miles=False, scandinavian_miles=False, **config): self.unit = "kWh/100 mil" # Init placeholder for parking heater duration - config.get("parkingheater", 30) + config.get("parkingheater", CLIMA_DEFAULT_DURATION) if "pheater_duration" == self.attr: - setValue = config.get("climatisation_duration", 30) + setValue = config.get("climatisation_duration", CLIMA_DEFAULT_DURATION) self.vehicle.pheater_duration = setValue @property @@ -215,23 +217,29 @@ def assumed_state(self): class Climate(Instrument): def __init__(self, attr, name, icon): super().__init__(component="climate", attr=attr, name=name, icon=icon) + self.spin = "" + self.duration = CLIMA_DEFAULT_DURATION @property def hvac_mode(self): - pass + raise NotImplementedError @property def target_temperature(self): - pass + raise NotImplementedError - def set_temperature(self, **kwargs): - pass + def set_temperature(self, temperature: float, **kwargs): + raise NotImplementedError def set_hvac_mode(self, hvac_mode): - pass + raise NotImplementedError class ElectricClimatisationClimate(Climate): + @property + def is_mutable(self): + return True + def __init__(self): super().__init__(attr="electric_climatisation", name="Electric Climatisation", icon="mdi:radiator") @@ -243,7 +251,7 @@ def hvac_mode(self): def target_temperature(self): return self.vehicle.climatisation_target_temperature - async def set_temperature(self, temperature): + async def set_temperature(self, temperature: float, **kwargs): await self.vehicle.climatisation_target(temperature) async def set_hvac_mode(self, hvac_mode): @@ -254,12 +262,16 @@ async def set_hvac_mode(self, hvac_mode): class CombustionClimatisationClimate(Climate): + @property + def is_mutable(self): + return True + def __init__(self): super().__init__(attr="pheater_heating", name="Parking Heater Climatisation", icon="mdi:radiator") def configurate(self, **config): self.spin = config.get("spin", "") - self.duration = config.get("combustionengineheatingduration", 30) + self.duration = config.get("combustionengineheatingduration", CLIMA_DEFAULT_DURATION) @property def hvac_mode(self): @@ -269,7 +281,7 @@ def hvac_mode(self): def target_temperature(self): return self.vehicle.climatisation_target_temperature - async def set_temperature(self, temperature): + async def set_temperature(self, temperature: float, **kwargs): await self.vehicle.setClimatisationTargetTemperature(temperature) async def set_hvac_mode(self, hvac_mode): @@ -310,6 +322,7 @@ def str_state(self): class DoorLock(Instrument): def __init__(self): super().__init__(component="lock", attr="door_locked", name="Door locked") + self.spin = "" def configurate(self, **config): self.spin = config.get("spin", "") @@ -441,6 +454,7 @@ def attributes(self): class AuxiliaryClimatisation(Switch): def __init__(self): super().__init__(attr="auxiliary_climatisation", name="Auxiliary Climatisation", icon="mdi:radiator") + self.spin = "" def configurate(self, **config): self.spin = config.get("spin", "") @@ -575,10 +589,12 @@ def attributes(self): class PHeaterVentilation(Switch): def __init__(self): super().__init__(attr="pheater_ventilation", name="Parking Heater Ventilation", icon="mdi:radiator") + self.spin = "" + self.duration = CLIMA_DEFAULT_DURATION def configurate(self, **config): self.spin = config.get("spin", "") - self.duration = config.get("combustionengineclimatisationduration", 30) + self.duration = config.get("combustionengineclimatisationduration", CLIMA_DEFAULT_DURATION) @property def state(self): diff --git a/volkswagencarnet/vw_exceptions.py b/volkswagencarnet/vw_exceptions.py new file mode 100644 index 00000000..3ffc67ce --- /dev/null +++ b/volkswagencarnet/vw_exceptions.py @@ -0,0 +1,7 @@ +"""Backend specific exceptions.""" + + +class AuthenticationException(Exception): + """Login fails for whatever reason.""" + + pass diff --git a/volkswagencarnet/vw_utilities.py b/volkswagencarnet/vw_utilities.py index 03cef730..916dccc0 100644 --- a/volkswagencarnet/vw_utilities.py +++ b/volkswagencarnet/vw_utilities.py @@ -1,3 +1,4 @@ +"""Common utility functions.""" import json import logging import re @@ -26,12 +27,13 @@ def read_config() -> dict: _LOGGER.debug("checking for config file %s", config) with open(config) as config: return dict(x.split(": ") for x in config.read().strip().splitlines() if not x.startswith("#")) - except (IOError, OSError): + except OSError: continue return {} def json_loads(s) -> Any: + """Load JSON from string and parse timestamps.""" return json.loads(s, object_hook=obj_parser) @@ -41,12 +43,15 @@ def obj_parser(obj: dict) -> dict: try: obj[key] = datetime.strptime(val, "%Y-%m-%dT%H:%M:%S%z") except (TypeError, ValueError): - pass + """The value was not a date.""" return obj def find_path(src, path) -> Any: - """Simple navigation of a hierarchical dict structure using XPATH-like syntax. + """ + Return data at path in source. + + Simple navigation of a hierarchical dict structure using XPATH-like syntax. >>> find_path(dict(a=1), 'a') 1 @@ -83,6 +88,8 @@ def find_path(src, path) -> Any: def is_valid_path(src, path): """ + Check if path exists in source. + >>> is_valid_path(dict(a=1), 'a') True diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 2d0fb50e..fe18a353 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """Vehicle class for We Connect.""" import asyncio import logging from collections import OrderedDict from datetime import datetime, timedelta, timezone from json import dumps as to_json +from typing import Optional from .vw_utilities import find_path, is_valid_path @@ -28,7 +28,10 @@ class Vehicle: + """Vehicle contains the state of sensors and methods for interacting with the car.""" + def __init__(self, conn, url): + """Initialize the Vehicle with default values.""" self._connection = conn self._url = url self._homeregion = "https://msg.volkswagen.de" @@ -103,7 +106,6 @@ async def discover(self): self._services[service_name].update(data) except Exception as error: _LOGGER.warning(f'Encountered exception: "{error}" while parsing service item: {service}') - pass else: _LOGGER.warning(f"Could not determine available API endpoints for {self.vin}") _LOGGER.debug(f"API endpoints: {self._services}") @@ -188,7 +190,7 @@ async def get_position(self): if new_time > old_time: _LOGGER.debug("Detected new parking time") self.requests_remaining = 15 - except: + except Exception: pass self._states.update(data) else: @@ -251,7 +253,7 @@ async def wait_for_request(self, section, request, retry_count=36): # Data set functions # Charging (BATTERYCHARGE) async def set_charger_current(self, value): - """Set charger current""" + """Set charger current.""" if self.is_charging_supported: if 1 <= int(value) <= 255: data = {"action": {"settings": {"maxChargeCurrent": int(value)}, "type": "setSettings"}} @@ -550,12 +552,29 @@ async def set_refresh(self): # Vehicle info @property def attrs(self): + """ + Return all attributes. + + :return: + """ return self._states - def has_attr(self, attr): + def has_attr(self, attr) -> bool: + """ + Return true if attribute exists. + + :param attr: + :return: + """ return is_valid_path(self.attrs, attr) def get_attr(self, attr): + """ + Return a specific attribute. + + :param attr: + :return: + """ return find_path(self.attrs, attr) async def expired(self, service): @@ -576,100 +595,130 @@ async def expired(self, service): return True else: return False - except: + except Exception: _LOGGER.debug(f"Exception. Could not determine end of access for service {service}, assuming it is valid") return False def dashboard(self, **config): + """ + Return dashboard with specified configuraion. + + :param config: + :return: + """ # Classic python notation from .vw_dashboard import Dashboard return Dashboard(self, **config) @property - def vin(self): + def vin(self) -> str: + """ + Vehicle identification number. + + :return: + """ return self._url @property - def unique_id(self): + def unique_id(self) -> str: + """ + Return unique id for the vehicle (vin). + + :return: + """ return self.vin # Information from vehicle states # # Car information @property - def nickname(self): + def nickname(self) -> Optional[str]: + """ + Return nickname of the vehicle. + + :return: + """ return self.attrs.get("carData", {}).get("nickname", None) @property - def is_nickname_supported(self): - if self.attrs.get("carData", {}).get("nickname", False): - return True + def is_nickname_supported(self) -> bool: + """ + Return true if naming the vehicle is supported. + + :return: + """ + return self.attrs.get("carData", {}).get("nickname", False) is not False @property - def deactivated(self): + def deactivated(self) -> Optional[bool]: + """ + Return true if service is deactivated. + + :return: + """ return self.attrs.get("carData", {}).get("deactivated", None) @property - def is_deactivated_supported(self): - if self.attrs.get("carData", {}).get("deactivated", False): - return True + def is_deactivated_supported(self) -> bool: + """ + Return true if service deactivation status is supported. + + :return: + """ + return self.attrs.get("carData", {}).get("deactivated", False) is True @property - def model(self): - """Return model""" + def model(self) -> Optional[str]: + """Return model.""" return self.attrs.get("carportData", {}).get("modelName", None) @property - def is_model_supported(self): + def is_model_supported(self) -> bool: """Return true if model is supported.""" - if self.attrs.get("carportData", {}).get("modelName", False): - return True + return self.attrs.get("carportData", {}).get("modelName", False) is not False @property - def model_year(self): - """Return model year""" + def model_year(self) -> Optional[bool]: + """Return model year.""" return self.attrs.get("carportData", {}).get("modelYear", None) @property - def is_model_year_supported(self): + def is_model_year_supported(self) -> bool: """Return true if model year is supported.""" - if self.attrs.get("carportData", {}).get("modelYear", False): - return True + return self.attrs.get("carportData", {}).get("modelYear", False) is not False @property - def model_image(self): + def model_image(self) -> str: # Not implemented - """Return model image""" + """Return vehicle model image.""" return self.attrs.get("imageUrl") @property - def is_model_image_supported(self): + def is_model_image_supported(self) -> bool: + """ + Return true if vehicle model image is supported. + + :return: + """ # Not implemented - if self.attrs.get("imageUrl", False): - return True + return self.attrs.get("imageUrl", False) is not False # Lights @property - def parking_light(self): - """Return true if parking light is on""" + def parking_light(self) -> bool: + """Return true if parking light is on.""" response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301010001"].get("value", 0)) - if response != 2: - return True - else: - return False + return response != 2 @property - def is_parking_light_supported(self): - """Return true if parking light is supported""" + def is_parking_light_supported(self) -> bool: + """Return true if parking light is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): - if "0x0301010001" in self.attrs.get("StoredVehicleDataResponseParsed"): - return True - else: - return False + return "0x0301010001" in self.attrs.get("StoredVehicleDataResponseParsed") # Connection status @property - def last_connected(self): + def last_connected(self) -> str: """Return when vehicle was last connected to connect servers.""" last_connected_utc = ( self.attrs.get("StoredVehicleDataResponse") @@ -682,7 +731,7 @@ def last_connected(self): return last_connected.strftime("%Y-%m-%d %H:%M:%S") @property - def is_last_connected_supported(self): + def is_last_connected_supported(self) -> bool: """Return when vehicle was last connected to connect servers.""" if next( iter( @@ -693,44 +742,50 @@ def is_last_connected_supported(self): None, ).get("tsCarSentUtc", []): return True + return False # Service information @property - def distance(self): + def distance(self) -> Optional[int]: """Return vehicle odometer.""" value = self.attrs.get("StoredVehicleDataResponseParsed")["0x0101010002"].get("value", 0) if value: return int(value) + return None @property - def is_distance_supported(self): - """Return true if odometer is supported""" + def is_distance_supported(self) -> bool: + """Return true if odometer is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): - if "0x0101010002" in self.attrs.get("StoredVehicleDataResponseParsed"): - return True - else: - return False + return "0x0101010002" in self.attrs.get("StoredVehicleDataResponseParsed") @property def service_inspection(self): - """Return time left for service inspection""" + """Return time left for service inspection.""" return -int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0203010004"].get("value")) @property - def is_service_inspection_supported(self): + def is_service_inspection_supported(self) -> bool: + """ + Return true if days to service inspection is supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): - if "0x0203010004" in self.attrs.get("StoredVehicleDataResponseParsed"): - return True - else: - return False + return "0x0203010004" in self.attrs.get("StoredVehicleDataResponseParsed") @property def service_inspection_distance(self): - """Return time left for service inspection""" + """Return time left for service inspection.""" return -int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0203010003"].get("value", 0)) @property def is_service_inspection_distance_supported(self): + """ + Return true if distance to oil inspection is supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0203010003" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -739,11 +794,16 @@ def is_service_inspection_distance_supported(self): @property def oil_inspection(self): - """Return time left for service inspection""" + """Return time left for service inspection.""" return -int(self.attrs.get("StoredVehicleDataResponseParsed", {}).get("0x0203010002", {}).get("value", 0)) @property def is_oil_inspection_supported(self): + """ + Return true if days to oil inspection is supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0203010002" in self.attrs.get("StoredVehicleDataResponseParsed"): if self.attrs.get("StoredVehicleDataResponseParsed").get("0x0203010002").get("value", None) is not None: @@ -752,11 +812,16 @@ def is_oil_inspection_supported(self): @property def oil_inspection_distance(self): - """Return time left for service inspection""" + """Return time left for service inspection.""" return -int(self.attrs.get("StoredVehicleDataResponseParsed", {}).get("0x0203010001", {}).get("value", 0)) @property def is_oil_inspection_distance_supported(self): + """ + Return true if oil inspection distance is supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0203010001" in self.attrs.get("StoredVehicleDataResponseParsed"): if self.attrs.get("StoredVehicleDataResponseParsed").get("0x0203010001").get("value", None) is not None: @@ -781,7 +846,7 @@ def is_adblue_level_supported(self): # Charger related states for EV and PHEV @property def charging(self): - """Return battery level""" + """Return battery level.""" cstate = ( self.attrs.get("charger", {}) .get("status", {}) @@ -793,7 +858,7 @@ def charging(self): @property def is_charging_supported(self): - """Return true if charging is supported""" + """Return true if charging is supported.""" if self.attrs.get("charger", False): if "status" in self.attrs.get("charger", {}): if "chargingStatusData" in self.attrs.get("charger")["status"]: @@ -803,14 +868,14 @@ def is_charging_supported(self): @property def battery_level(self): - """Return battery level""" + """Return battery level.""" return int( self.attrs.get("charger").get("status").get("batteryStatusData").get("stateOfCharge").get("content", 0) ) @property def is_battery_level_supported(self): - """Return true if battery level is supported""" + """Return true if battery level is supported.""" if self.attrs.get("charger", False): if "status" in self.attrs.get("charger"): if "batteryStatusData" in self.attrs.get("charger")["status"]: @@ -833,7 +898,7 @@ def charge_max_ampere(self): @property def is_charge_max_ampere_supported(self): - """Return true if Charger Max Ampere is supported""" + """Return true if Charger Max Ampere is supported.""" if self.attrs.get("charger", False): if "settings" in self.attrs.get("charger", {}): if "maxChargeCurrent" in self.attrs.get("charger", {})["settings"]: @@ -843,7 +908,7 @@ def is_charge_max_ampere_supported(self): @property def charging_cable_locked(self): - """Return plug locked state""" + """Return plug locked state.""" response = self.attrs.get("charger")["status"]["plugStatusData"]["lockState"].get("content", 0) if response == "locked": return True @@ -852,7 +917,7 @@ def charging_cable_locked(self): @property def is_charging_cable_locked_supported(self): - """Return true if plug locked state is supported""" + """Return true if plug locked state is supported.""" if self.attrs.get("charger", False): if "status" in self.attrs.get("charger", {}): if "plugStatusData" in self.attrs.get("charger").get("status", {}): @@ -862,7 +927,7 @@ def is_charging_cable_locked_supported(self): @property def charging_cable_connected(self): - """Return plug locked state""" + """Return plug locked state.""" response = self.attrs.get("charger")["status"]["plugStatusData"]["plugState"].get("content", 0) if response == "connected": return False @@ -871,7 +936,7 @@ def charging_cable_connected(self): @property def is_charging_cable_connected_supported(self): - """Return true if charging cable connected is supported""" + """Return true if charging cable connected is supported.""" if self.attrs.get("charger", False): if "status" in self.attrs.get("charger", {}): if "plugStatusData" in self.attrs.get("charger").get("status", {}): @@ -881,7 +946,7 @@ def is_charging_cable_connected_supported(self): @property def charging_time_left(self): - """Return minutes to charing complete""" + """Return minutes to charging complete.""" if self.external_power: minutes = ( self.attrs.get("charger", {}) @@ -903,7 +968,7 @@ def charging_time_left(self): @property def is_charging_time_left_supported(self): - """Return true if charging is supported""" + """Return true if charging is supported.""" return self.is_charging_supported @property @@ -966,7 +1031,7 @@ def position(self): lng = int(pos_obj.get("Position").get("carCoordinate").get("longitude")) / 1000000 parking_time = pos_obj.get("parkingTimeUTC") output = {"lat": lat, "lng": lng, "timestamp": parking_time} - except: + except Exception: output = { "lat": "?", "lng": "?", @@ -1009,6 +1074,11 @@ def is_parking_time_supported(self): # Vehicle fuel level and range @property def electric_range(self): + """ + Return electric range. + + :return: + """ value = -1 if ( PRIMARY_RANGE in self.attrs.get("StoredVehicleDataResponseParsed") @@ -1027,6 +1097,11 @@ def electric_range(self): @property def is_electric_range_supported(self): + """ + Return true if electric range is supported. + + :return: + """ supported = False if self.attrs.get("StoredVehicleDataResponseParsed", False): if ( @@ -1046,6 +1121,11 @@ def is_electric_range_supported(self): @property def combustion_range(self): + """ + Return combustion engine range. + + :return: + """ value = -1 if ( PRIMARY_RANGE in self.attrs.get("StoredVehicleDataResponseParsed") @@ -1064,6 +1144,11 @@ def combustion_range(self): @property def is_combustion_range_supported(self): + """ + Return true if combustion range is supported, i.e. false for EVs. + + :return: + """ supported = False if self.attrs.get("StoredVehicleDataResponseParsed", False): if ( @@ -1083,6 +1168,11 @@ def is_combustion_range_supported(self): @property def combined_range(self): + """ + Return combined range. + + :return: + """ value = -1 if COMBINED_RANGE in self.attrs.get("StoredVehicleDataResponseParsed"): value = self.attrs.get("StoredVehicleDataResponseParsed")[COMBINED_RANGE].get("value", NO_VALUE) @@ -1090,13 +1180,23 @@ def combined_range(self): @property def is_combined_range_supported(self): + """ + Return true if combined range is supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if COMBINED_RANGE in self.attrs.get("StoredVehicleDataResponseParsed"): return self.is_electric_range_supported and self.is_combustion_range_supported return False @property - def fuel_level(self): + def fuel_level(self) -> int: + """ + Return fuel level. + + :return: + """ value = -1 if FUEL_LEVEL in self.attrs.get("StoredVehicleDataResponseParsed"): if "value" in self.attrs.get("StoredVehicleDataResponseParsed")[FUEL_LEVEL]: @@ -1105,6 +1205,11 @@ def fuel_level(self): @property def is_fuel_level_supported(self): + """ + Return true if fuel level reporting is supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if FUEL_LEVEL in self.attrs.get("StoredVehicleDataResponseParsed"): return self.is_combustion_range_supported @@ -1156,7 +1261,7 @@ def outside_temperature(self): @property def is_outside_temperature_supported(self): - """Return true if outside temp is supported""" + """Return true if outside temp is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301020001" in self.attrs.get("StoredVehicleDataResponseParsed"): if "value" in self.attrs.get("StoredVehicleDataResponseParsed")["0x0301020001"]: @@ -1275,6 +1380,11 @@ def is_window_heater_supported(self): # Parking heater, "legacy" auxiliary climatisation @property def pheater_duration(self): + """ + Return heating duration for legacy aux heater. + + :return: + """ return self._climate_duration @pheater_duration.setter @@ -1286,6 +1396,11 @@ def pheater_duration(self, value): @property def is_pheater_duration_supported(self): + """ + Return true if legacy aux heater is supported. + + :return: + """ return self.is_pheater_heating_supported @property @@ -1329,6 +1444,11 @@ def is_pheater_status_supported(self): # Windows @property def windows_closed(self): + """ + Return true if all windows are closed. + + :return: + """ return ( self.window_closed_left_front and self.window_closed_left_back @@ -1338,7 +1458,7 @@ def windows_closed(self): @property def is_windows_closed_supported(self): - """Return true if window state is supported""" + """Return true if window state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301050001" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1347,6 +1467,11 @@ def is_windows_closed_supported(self): @property def window_closed_left_front(self): + """ + Return left front window closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301050001"].get("value", 0)) if response == 3: return True @@ -1355,7 +1480,7 @@ def window_closed_left_front(self): @property def is_window_closed_left_front_supported(self): - """Return true if window state is supported""" + """Return true if window state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301050001" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1364,6 +1489,11 @@ def is_window_closed_left_front_supported(self): @property def window_closed_right_front(self): + """ + Return right front window closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301050005"].get("value", 0)) if response == 3: return True @@ -1372,7 +1502,7 @@ def window_closed_right_front(self): @property def is_window_closed_right_front_supported(self): - """Return true if window state is supported""" + """Return true if window state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301050005" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1381,6 +1511,11 @@ def is_window_closed_right_front_supported(self): @property def window_closed_left_back(self): + """ + Return left back window closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301050003"].get("value", 0)) if response == 3: return True @@ -1389,7 +1524,7 @@ def window_closed_left_back(self): @property def is_window_closed_left_back_supported(self): - """Return true if window state is supported""" + """Return true if window state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301050003" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1398,6 +1533,11 @@ def is_window_closed_left_back_supported(self): @property def window_closed_right_back(self): + """ + Return right back window closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301050007"].get("value", 0)) if response == 3: return True @@ -1406,7 +1546,7 @@ def window_closed_right_back(self): @property def is_window_closed_right_back_supported(self): - """Return true if window state is supported""" + """Return true if window state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301050007" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1415,6 +1555,11 @@ def is_window_closed_right_back_supported(self): @property def sunroof_closed(self): + """ + Return sunroof closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x030105000B"].get("value", 0)) if response == 3: return True @@ -1423,7 +1568,7 @@ def sunroof_closed(self): @property def is_sunroof_closed_supported(self): - """Return true if sunroof state is supported""" + """Return true if sunroof state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x030105000B" in self.attrs.get("StoredVehicleDataResponseParsed"): if int(self.attrs.get("StoredVehicleDataResponseParsed")["0x030105000B"].get("value", 0)) == 0: @@ -1436,6 +1581,11 @@ def is_sunroof_closed_supported(self): # Locks @property def door_locked(self): + """ + Return true if all doors are locked. + + :return: + """ # LEFT FRONT response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301040001"].get("value", 0)) if response != 2: @@ -1457,6 +1607,11 @@ def door_locked(self): @property def is_door_locked_supported(self): + """ + Return true if supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301040001" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1465,6 +1620,11 @@ def is_door_locked_supported(self): @property def trunk_locked(self): + """ + Return trunk locked state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x030104000D"].get("value", 0)) if response == 2: return True @@ -1473,6 +1633,11 @@ def trunk_locked(self): @property def is_trunk_locked_supported(self): + """ + Return true if supported. + + :return: + """ if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x030104000D" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1482,7 +1647,7 @@ def is_trunk_locked_supported(self): # Doors, hood and trunk @property def hood_closed(self): - """Return true if hood is closed""" + """Return true if hood is closed.""" response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301040011"].get("value", 0)) if response == 3: return True @@ -1491,7 +1656,7 @@ def hood_closed(self): @property def is_hood_closed_supported(self): - """Return true if hood state is supported""" + """Return true if hood state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301040011" in self.attrs.get("StoredVehicleDataResponseParsed", {}): if int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301040011"].get("value", 0)) == 0: @@ -1503,6 +1668,11 @@ def is_hood_closed_supported(self): @property def door_closed_left_front(self): + """ + Return left front door closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301040002"].get("value", 0)) if response == 3: return True @@ -1511,7 +1681,7 @@ def door_closed_left_front(self): @property def is_door_closed_left_front_supported(self): - """Return true if window state is supported""" + """Return true if supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301040002" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1520,6 +1690,11 @@ def is_door_closed_left_front_supported(self): @property def door_closed_right_front(self): + """ + Return right front door closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301040008"].get("value", 0)) if response == 3: return True @@ -1528,7 +1703,7 @@ def door_closed_right_front(self): @property def is_door_closed_right_front_supported(self): - """Return true if window state is supported""" + """Return true if supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301040008" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1537,6 +1712,11 @@ def is_door_closed_right_front_supported(self): @property def door_closed_left_back(self): + """ + Return left back door closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x0301040005"].get("value", 0)) if response == 3: return True @@ -1545,7 +1725,7 @@ def door_closed_left_back(self): @property def is_door_closed_left_back_supported(self): - """Return true if window state is supported""" + """Return true if supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x0301040005" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1554,6 +1734,11 @@ def is_door_closed_left_back_supported(self): @property def door_closed_right_back(self): + """ + Return right back door closed state. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x030104000B"].get("value", 0)) if response == 3: return True @@ -1562,7 +1747,7 @@ def door_closed_right_back(self): @property def is_door_closed_right_back_supported(self): - """Return true if window state is supported""" + """Return true if supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x030104000B" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1571,6 +1756,11 @@ def is_door_closed_right_back_supported(self): @property def trunk_closed(self): + """ + Return state of trunk closed. + + :return: + """ response = int(self.attrs.get("StoredVehicleDataResponseParsed")["0x030104000E"].get("value", 0)) if response == 3: return True @@ -1579,7 +1769,7 @@ def trunk_closed(self): @property def is_trunk_closed_supported(self): - """Return true if window state is supported""" + """Return true if trunk closed state is supported.""" if self.attrs.get("StoredVehicleDataResponseParsed", False): if "0x030104000E" in self.attrs.get("StoredVehicleDataResponseParsed"): return True @@ -1590,30 +1780,60 @@ def is_trunk_closed_supported(self): # Not yet implemented @property def schedule1(self): + """ + Return schedule #1. + + :return: + """ return False @property def is_schedule1_suppored(self): + """ + Return true if supported. + + :return: + """ if self.attrs.get("timers", {}).get("timersAndProfiles", {}).get("timerList", {}).get("timer", False): return True return False @property def schedule2(self): + """ + Return schedule #2. + + :return: + """ return False @property def is_schedule2_suppored(self): + """ + Return true if supported. + + :return: + """ if self.attrs.get("timers", {}).get("timersAndProfiles", {}).get("timerList", {}).get("timer", False): return True return False @property def schedule3(self): + """ + Return schedule #3. + + :return: + """ return False @property def is_schedule3_suppored(self): + """ + Return true if supported. + + :return: + """ if self.attrs.get("timers", {}).get("timersAndProfiles", {}).get("timerList", {}).get("timer", False): return True return False @@ -1621,114 +1841,212 @@ def is_schedule3_suppored(self): # Trip data @property def trip_last_entry(self): + """ + Return last trip data entry. + + :return: + """ return self.attrs.get("tripstatistics", {}) @property def trip_last_average_speed(self): + """ + Return last trip average speed. + + :return: + """ return self.trip_last_entry.get("averageSpeed") @property def is_trip_last_average_speed_supported(self): + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry if response and type(response.get("averageSpeed", None)) in (float, int): return True @property def trip_last_average_electric_engine_consumption(self): + """ + Return last trip average electric consumption. + + :return: + """ value = self.trip_last_entry.get("averageElectricEngineConsumption") return float(value / 10) @property def is_trip_last_average_electric_engine_consumption_supported(self): + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry if response and type(response.get("averageElectricEngineConsumption", None)) in (float, int): return True @property def trip_last_average_fuel_consumption(self): + """ + Return last trip average fuel consumption. + + :return: + """ return int(self.trip_last_entry.get("averageFuelConsumption")) / 10 @property def is_trip_last_average_fuel_consumption_supported(self): + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry if response and type(response.get("averageFuelConsumption", None)) in (float, int): return True @property def trip_last_average_auxillary_consumption(self): + """ + Return last trip average auxiliary consumption. + + :return: + """ return self.trip_last_entry.get("averageAuxiliaryConsumption") @property - def is_trip_last_average_auxillary_consumption_supported(self): + def is_trip_last_average_auxillary_consumption_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry - if response and type(response.get("averageAuxiliaryConsumption", None)) in (float, int): - return True + return response and type(response.get("averageAuxiliaryConsumption", None)) in (float, int) @property def trip_last_average_aux_consumer_consumption(self): + """ + Return last trip average auxiliary consumer consumption. + + :return: + """ value = self.trip_last_entry.get("averageAuxConsumerConsumption") return float(value / 10) @property - def is_trip_last_average_aux_consumer_consumption_supported(self): + def is_trip_last_average_aux_consumer_consumption_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry - if response and type(response.get("averageAuxConsumerConsumption", None)) in (float, int): - return True + return response and type(response.get("averageAuxConsumerConsumption", None)) in (float, int) @property def trip_last_duration(self): + """ + Return last trip duration in minutes(?). + + :return: + """ return self.trip_last_entry.get("traveltime") @property - def is_trip_last_duration_supported(self): + def is_trip_last_duration_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry - if response and type(response.get("traveltime", None)) in (float, int): - return True + return response and type(response.get("traveltime", None)) in (float, int) @property def trip_last_length(self): + """ + Return last trip length. + + :return: + """ return self.trip_last_entry.get("mileage") @property - def is_trip_last_length_supported(self): + def is_trip_last_length_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry - if response and type(response.get("mileage", None)) in (float, int): - return True + return response and type(response.get("mileage", None)) in (float, int) @property def trip_last_recuperation(self): + """ + Return last trip recuperation. + + :return: + """ # Not implemented return self.trip_last_entry.get("recuperation") @property - def is_trip_last_recuperation_supported(self): + def is_trip_last_recuperation_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ # Not implemented response = self.trip_last_entry - if response and type(response.get("recuperation", None)) in (float, int): - return True + return response and type(response.get("recuperation", None)) in (float, int) @property def trip_last_average_recuperation(self): + """ + Return last trip total recuperation. + + :return: + """ value = self.trip_last_entry.get("averageRecuperation") return float(value / 10) @property - def is_trip_last_average_recuperation_supported(self): + def is_trip_last_average_recuperation_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ response = self.trip_last_entry - if response and type(response.get("averageRecuperation", None)) in (float, int): - return True + return response and type(response.get("averageRecuperation", None)) in (float, int) @property def trip_last_total_electric_consumption(self): + """ + Return last trip total electric consumption. + + :return: + """ # Not implemented return self.trip_last_entry.get("totalElectricConsumption") @property - def is_trip_last_total_electric_consumption_supported(self): + def is_trip_last_total_electric_consumption_supported(self) -> bool: + """ + Return true if supported. + + :return: + """ # Not implemented response = self.trip_last_entry - if response and type(response.get("totalElectricConsumption", None)) in (float, int): - return True + return response and type(response.get("totalElectricConsumption", None)) in (float, int) # Status of set data requests @property @@ -1759,13 +2077,13 @@ def lock_action_status(self): # Requests data @property def refresh_data(self): - """Get state of data refresh""" + """Get state of data refresh.""" if self._requests.get("refresh", {}).get("id", False): return True @property def is_refresh_data_supported(self): - """Data refresh is always supported.""" + """Return true, as data refresh is always supported.""" return True @property @@ -1775,7 +2093,7 @@ def request_in_progress(self): for section in self._requests: if self._requests[section].get("id", False): return True - except: + except Exception: pass return False @@ -1812,16 +2130,33 @@ def requests_remaining(self, value): @property def is_requests_remaining_supported(self): + """ + Return true if requests remaining is supperted. + + :return: + """ return True if self._requests.get("remaining", False) else False # Helper functions # def __str__(self): + """Return the vin.""" return self.vin @property def json(self): + """ + Return vehicle data in JSON format. + + :return: + """ + def serialize(obj): - if isinstance(obj, datetime): - return obj.isoformat() + """ + Convert datetime instances back to JSON compatible format. + + :param obj: + :return: + """ + return obj.isoformat() if isinstance(obj, datetime) else obj return to_json(OrderedDict(sorted(self.attrs.items())), indent=4, default=serialize)