From ef42308df537b75c3f0e70e85ea28fba2c29ffae Mon Sep 17 00:00:00 2001 From: DurgNomis-drol Date: Fri, 28 Jan 2022 20:22:28 +0100 Subject: [PATCH 1/6] Rewrite to use models instead --- mytoyota/client.py | 8 +- mytoyota/const.py | 21 +- mytoyota/hvac.py | 52 ---- mytoyota/location.py | 24 -- mytoyota/models/dashboard.py | 84 ++++++ mytoyota/models/data.py | 11 + mytoyota/models/hvac.py | 91 ++++++ mytoyota/models/location.py | 22 ++ mytoyota/models/sensors.py | 154 ++++++++++ mytoyota/models/vehicle.py | 123 ++++++++ mytoyota/sensors.py | 184 ------------ mytoyota/status.py | 147 ---------- mytoyota/vehicle.py | 173 ----------- .../vehicle_JTMW1234565432109_odometer.json | 4 - ...cle_JTMW1234565432109_odometer_legacy.json | 11 + .../vehicle_JTMW1234565432109_status.json | 8 + ...hicle_JTMW1234565432109_status_legacy.json | 35 +++ tests/test_energy.py | 231 --------------- tests/test_hvac.py | 63 ++-- tests/test_odometer.py | 45 --- tests/test_parking_location.py | 24 +- tests/test_sensors.py | 272 +++++------------- tests/test_utils.py | 4 +- tests/test_vehicle.py | 163 +++++++++-- 24 files changed, 771 insertions(+), 1183 deletions(-) delete mode 100644 mytoyota/hvac.py delete mode 100644 mytoyota/location.py create mode 100644 mytoyota/models/dashboard.py create mode 100644 mytoyota/models/data.py create mode 100644 mytoyota/models/hvac.py create mode 100644 mytoyota/models/location.py create mode 100644 mytoyota/models/sensors.py create mode 100644 mytoyota/models/vehicle.py delete mode 100644 mytoyota/sensors.py delete mode 100644 mytoyota/status.py delete mode 100644 mytoyota/vehicle.py create mode 100644 tests/data/vehicle_JTMW1234565432109_odometer_legacy.json delete mode 100644 tests/test_energy.py delete mode 100644 tests/test_odometer.py diff --git a/mytoyota/client.py b/mytoyota/client.py index f2953474..e3e23f55 100644 --- a/mytoyota/client.py +++ b/mytoyota/client.py @@ -35,10 +35,10 @@ ToyotaLocaleNotValid, ToyotaRegionNotSupported, ) +from .models.vehicle import Vehicle from .statistics import Statistics from .utils.locale import is_valid_locale from .utils.logs import censor, censor_vin -from .vehicle import Vehicle _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -256,16 +256,14 @@ async def get_vehicle_status(self, vehicle: dict) -> Vehicle: _LOGGER.debug("Presenting information as an object...") - car = Vehicle( + return Vehicle( vehicle_info=vehicle, connected_services=data[0], odometer=data[1], status=data[2], - remote_control=data[3], + status_legacy=data[3], ) - return car - async def get_vehicle_status_json(self, vehicle: dict) -> str: """Returns vehicle status as json string. diff --git a/mytoyota/const.py b/mytoyota/const.py index 59497577..3a7f6e01 100644 --- a/mytoyota/const.py +++ b/mytoyota/const.py @@ -28,15 +28,7 @@ TOKEN = "token" UUID = "uuid" CUSTOMERPROFILE = "customerProfile" -FUEL = "fuel" -MILEAGE = "mileage" -TYPE = "type" -VALUE = "value" -UNIT = "unit" -VEHICLE_INFO = "VehicleInfo" ACQUISITIONDATE = "AcquisitionDatetime" -CHARGE_INFO = "ChargeInfo" -HVAC = "RemoteHvacInfo" BUCKET = "bucket" DAYOFYEAR = "dayOfYear" @@ -45,6 +37,7 @@ DATA = "data" SUMMARY = "summary" HISTOGRAM = "histogram" +UNIT = "unit" DAY = "day" WEEK = "week" @@ -52,18 +45,6 @@ MONTH = "month" YEAR = "year" -HOOD = "hood" -DOORS = "doors" -WINDOWS = "windows" -LIGHTS = "lamps" -KEY = "key" -CLOSED = "closed" -LOCKED = "locked" -WARNING = "warning" -STATE = "state" -OFF = "off" -INCAR = "inCar" - METRIC = "metric" IMPERIAL = "imperial" IMPERIAL_LITERS = "imperial_liters" diff --git a/mytoyota/hvac.py b/mytoyota/hvac.py deleted file mode 100644 index 861e7ef7..00000000 --- a/mytoyota/hvac.py +++ /dev/null @@ -1,52 +0,0 @@ -"""HVAC representation for mytoyota""" - - -class Hvac: - """Representation of the HVAC system in the car""" - - legacy: bool = False - - def __init__(self, hvac: dict, legacy: bool = False) -> None: - # Support legacy method. Toyota seems to be changing their api for newer - # cars, though not a lot seems to use the method yet. - # This option enables support for older cars. - if legacy: - self._set_legacy(hvac) - else: - self._set(hvac) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return hvac as dict.""" - return vars(self) - - def _set(self, hvac: dict) -> None: - """Set attributes""" - current_temp = hvac.get("currentTemperatureIndication", {}) - self.current_temperature = current_temp.get("value", None) - target_temp = hvac.get("targetTemperature", {}) - self.target_temperature = target_temp.get("value", None) - self.started_at = hvac.get("startedAt", None) - self.status = hvac.get("status", None) - self.type = hvac.get("type", None) - self.duration = hvac.get("duration", None) - - self.options = hvac.get("options", None) - self.command_id = hvac.get("commandId", None) - - self.last_updated = current_temp.get("timestamp", None) - - def _set_legacy(self, hvac: dict) -> None: - """Set attributes using legacy data""" - self.legacy = True - - self.current_temperature = hvac.get("InsideTemperature", None) - self.target_temperature = hvac.get("SettingTemperature", None) - self.blower_on = hvac.get("BlowerStatus", None) - - self.front_defogger_on = hvac.get("FrontDefoggerStatus", False) - self.rear_defogger_on = hvac.get("RearDefoggerStatus", False) - - self.last_updated = hvac.get("LatestAcStartTime", None) diff --git a/mytoyota/location.py b/mytoyota/location.py deleted file mode 100644 index 511f4c8e..00000000 --- a/mytoyota/location.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Location representation for mytoyota""" -import logging - -_LOGGER: logging.Logger = logging.getLogger(__package__) - - -class ParkingLocation: - """ParkingLocation representation""" - - latitude: float = 0.0 - longitude: float = 0.0 - timestamp: int = 0 - - def __init__(self, parking: dict) -> None: - self.latitude = float(parking.get("lat", 0.0)) - self.longitude = float(parking.get("lon", 0.0)) - self.timestamp = int(parking.get("timestamp", 0)) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return parking location as dict.""" - return vars(self) diff --git a/mytoyota/models/dashboard.py b/mytoyota/models/dashboard.py new file mode 100644 index 00000000..4da32f2f --- /dev/null +++ b/mytoyota/models/dashboard.py @@ -0,0 +1,84 @@ +"""Models for vehicle sensors.""" +from __future__ import annotations +from typing import Any + +from mytoyota.utils.conversions import convert_to_miles + + +class Dashboard: + """Instrumentation data model.""" + + _chargeinfo: dict[str, Any] + _energy: dict[str, Any] + + def __init__( + self, + vehicle, + ) -> None: + self._vehicle = vehicle + + vehicle_info = vehicle._status_legacy.get("VehicleInfo", {}) + self._chargeinfo = vehicle_info.get("ChargeInfo", {}) + self._energy = vehicle._status.get("energy", [])[0] if vehicle._status.get("energy") else {} + + @property + def legacy(self) -> bool: + if "Fuel" in self._vehicle._odometer: + return True + return False + + @property + def is_metric(self) -> bool: + return self._vehicle._odometer.get("mileage_unit") == "km" + + @property + def odometer(self) -> int | None: + return self._vehicle._odometer.get("mileage") + + @property + def fuel_level(self) -> float | None: + if self.legacy: + return self._vehicle._odometer.get("Fuel") + return self._energy.get("level") + + @property + def range(self) -> float | None: + range = ( + self._chargeinfo.get("GasolineTravelableDistance") + if self.legacy + else self._energy.get("remainingRange", None) + ) + return convert_to_miles(range) if not self.is_metric else range + + @property + def battery_level(self) -> float | None: + if self.legacy: + return self._chargeinfo.get("ChargeRemainingAmount") + return None + + @property + def battery_range(self) -> float | None: + if self.legacy: + range = self._chargeinfo.get("EvDistanceInKm") + return convert_to_miles(range) if not self.is_metric else range + return None + + @property + def battery_range_with_aircon(self) -> float | None: + if self.legacy: + range = self._chargeinfo.get("EvDistanceWithAirCoInKm") + return convert_to_miles(range) if not self.is_metric else range + return None + + @property + def charging_status(self) -> str | None: + if self.legacy: + return self._chargeinfo.get("ChargingStatus") + return None + + @property + def remaining_charge_time(self) -> int | None: + if self.legacy: + return self._chargeinfo.get("RemainingChargeTime") + return None + \ No newline at end of file diff --git a/mytoyota/models/data.py b/mytoyota/models/data.py new file mode 100644 index 00000000..d9aefb8a --- /dev/null +++ b/mytoyota/models/data.py @@ -0,0 +1,11 @@ +"""Base data model.""" +from typing import Any, Dict + + +class VehicleData: + def __init__(self, data: dict) -> None: + self._data = data or {} + + @property + def raw_json(self) -> Dict[str, Any]: + return self._data \ No newline at end of file diff --git a/mytoyota/models/hvac.py b/mytoyota/models/hvac.py new file mode 100644 index 00000000..d4f0f814 --- /dev/null +++ b/mytoyota/models/hvac.py @@ -0,0 +1,91 @@ +"""Models for vehicle sensors.""" +from __future__ import annotations +from typing import Any + +from mytoyota.models.data import VehicleData + +def get_attr_in_dict(data: dict[str, float], attr: str) -> float | None: + return data.get(attr) + + +class Hvac(VehicleData): + """HVAC data model.""" + + def __init__(self, data: dict[str, Any], legacy: bool = False) -> None: + # Support legacy method. Toyota seems to be changing their api for newer + # cars, though not a lot seems to use the method yet. + # This option enables support for older cars. + super().__init__(data) + self.legacy = legacy + + @property + def current_temperature(self) -> float | None: + if self.legacy: + return self._data.get("InsideTemperature") + return get_attr_in_dict(self._data.get("currentTemperatureIndication", {}), "value") + + @property + def target_temperature(self) -> float | None: + if self.legacy: + return self._data.get("SettingTemperature") + return get_attr_in_dict(self._data.get("targetTemperature", {}), "value") + + @property + def started_at(self) -> str | None: + if self.legacy: + return None + return self._data.get("startedAt") + + @property + def status(self) -> str | None: + if self.legacy: + return None + return self._data.get("status") + + @property + def type(self) -> str | None: + if self.legacy: + return None + return self._data.get("type") + + @property + def duration(self) -> str | None: + if self.legacy: + return None + return self._data.get("duration") + + @property + def options(self) -> dict | list | None: + if self.legacy: + return None + return self._data.get("options") + + @property + def command_id(self) -> str | int | None: + if self.legacy: + return None + return self._data.get("commandId") + + @property + def front_defogger_is_on(self) -> bool | None: + if self.legacy: + return self._data.get("FrontDefoggerStatus") == 1 + return None + + @property + def rear_defogger_is_on(self) -> bool | None: + if self.legacy: + return self._data.get("RearDefoggerStatus") == 1 + return None + + @property + def blower_on(self) -> int | None: + if self.legacy: + return self._data.get("BlowerStatus") + return None + + @property + def last_updated(self) -> str | None: + if self.legacy: + return None + return get_attr_in_dict(self._data.get("currentTemperatureIndication", {}), "timestamp") \ No newline at end of file diff --git a/mytoyota/models/location.py b/mytoyota/models/location.py new file mode 100644 index 00000000..eaeb4ff7 --- /dev/null +++ b/mytoyota/models/location.py @@ -0,0 +1,22 @@ +"""Models for vehicle location.""" +from __future__ import annotations + + +from sqlite3 import Timestamp +from mytoyota.models.data import VehicleData + + +class ParkingLocation(VehicleData): + """Parking Location data model.""" + + @property + def latitude(self) -> float: + return float(self._data.get("lat", 0.0)) + + @property + def longitude(self) -> float: + return float(self._data.get("lon", 0.0)) + + @property + def timestamp(self) -> int | None: + return self._data.get("timestamp") diff --git a/mytoyota/models/sensors.py b/mytoyota/models/sensors.py new file mode 100644 index 00000000..50f50dd4 --- /dev/null +++ b/mytoyota/models/sensors.py @@ -0,0 +1,154 @@ +"""Models for vehicle sensors.""" +from __future__ import annotations +from mytoyota.models.data import VehicleData + + +class Door(VehicleData): + """Door/hood data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def closed(self) -> bool | None: + return self._data.get("closed") + + @property + def locked(self) -> bool | None: + return self._data.get("locked") + +class Doors(VehicleData): + """Trunk/doors/hood data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def driver_seat(self) -> Door: + return Door(self._data.get("driverSeatDoor")) + + @property + def passenger_seat(self) -> Door: + return Door(self._data.get("passengerSeatDoor")) + + @property + def leftrear_seat(self) -> Door: + return Door(self._data.get("rearLeftSeatDoor")) + + @property + def rightrear_seat(self) -> Door: + return Door(self._data.get("rearRightSeatDoor")) + + @property + def trunk(self) -> Door: + return Door(self._data.get("backDoor")) + +class Window(VehicleData): + """Window data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def state(self) -> str | None: + return self._data.get("state") + +class Windows(VehicleData): + """Windows data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def driver_seat(self) -> Window: + return Window(self._data.get("driverSeatWindow")) + + @property + def passenger_seat(self) -> Window: + return Window(self._data.get("passengerSeatWindow")) + + @property + def leftrear_seat(self) -> Window: + return Window(self._data.get("rearLeftSeatWindow")) + + @property + def rightrear_seat(self) -> Window: + return Window(self._data.get("rearRightSeatWindow")) + +class Light(VehicleData): + """Vehicle light data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def off(self) -> bool | None: + return self._data.get("off") + +class Lights(VehicleData): + """Vehicle lights data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def headlights(self) -> Light: + return Light(self._data.get("headLamp")) + + @property + def taillights(self) -> Light: + return Light(self._data.get("tailLamp")) + + @property + def hazardlights(self) -> Light: + return Light(self._data.get("hazardLamp")) + +class Key(VehicleData): + """Keyfob data model.""" + + @property + def warning(self) -> bool | None: + return self._data.get("warning") + + @property + def in_car(self) -> bool | None: + return self._data.get("inCar") + + +class Sensors(VehicleData): + """Vehicle sensors data model.""" + + @property + def overallstatus(self) -> str | None: + return self._data.get("overallStatus") + + @property + def last_updated(self) -> str | None: + return self._data.get("timestamp") + + @property + def lights(self) -> Lights: + return Lights(self._data.get("lamps")) + + @property + def hood(self) -> Door: + return Door(self._data.get("hood")) + + @property + def doors(self) -> Doors: + return Doors(self._data.get("doors")) + + @property + def windows(self) -> Windows: + return Windows(self._data.get("windows")) + + @property + def key(self) -> Key: + return Key(self._data.get("key")) + diff --git a/mytoyota/models/vehicle.py b/mytoyota/models/vehicle.py new file mode 100644 index 00000000..3a184761 --- /dev/null +++ b/mytoyota/models/vehicle.py @@ -0,0 +1,123 @@ +"""Vehicle model.""" +from __future__ import annotations +import logging +from typing import Any +from mytoyota.models.hvac import Hvac +from mytoyota.models.dashboard import Dashboard +from mytoyota.models.location import ParkingLocation +from mytoyota.models.sensors import Sensors +from mytoyota.utils.formatters import format_odometer + +from mytoyota.utils.logs import censor_vin + + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +class Vehicle: + """Vehicle data representation.""" + + def __init__( + self, + vehicle_info: dict[str, Any] = {}, + connected_services: dict[str, Any] = {}, + odometer: list[Any] = [], + status: dict[str, Any] = {}, + status_legacy: dict[str, Any] = {}, + ) -> None: + + self._connected_services = connected_services + self._vehicle_info = vehicle_info + self._odometer = format_odometer(odometer) + self._status = status + self._status_legacy = status_legacy + + @property + def id(self) -> int | None: + return self._vehicle_info.get("id") + + @property + def vin(self) -> str | None: + return self._vehicle_info.get("vin") + + @property + def alias(self) -> str | None: + return self._vehicle_info.get("alias") + + @property + def hybrid(self) -> bool | None: + return self._vehicle_info.get("hybrid") + + @property + def fueltype(self) -> str: + if "energy" in self._status and self._status["energy"]: + return self._status["energy"][0].get("type", "Unknown").capitalize() + + fueltype = self._vehicle_info.get("fuel", "Unknown") + return "Petrol" if fueltype == "1.0P" else fueltype + + + @property + def details(self) -> dict[str, Any] | None: + """Formats vehicle info into a dict.""" + d: dict[str, Any] = {} + for i in sorted(self._vehicle_info): + if i in ("vin", "alias", "id", "hybrid"): + continue + d[i] = self._vehicle_info[i] + return d if d else None + + @property + def is_connected_services_enabled(self) -> bool: + """Checks if the user has enabled connected services.""" + # Check if vin is not None. Toyota's servers is a bit flacky and can + # return garbage from connected_services endpoint, this is just to + # make sure that we don't throw a error message. + if self.vin: + if ( + "connectedService" in self._connected_services + and "status" in self._connected_services["connectedService"] + ): + if self._connected_services["connectedService"]["status"] == "ACTIVE": + return True + + _LOGGER.error( + "Please setup Connected Services if you want live data from the car. (%s)", + censor_vin(self.vin), + ) + return False + _LOGGER.error( + "Your vehicle does not support Connected services (%s). You can find out if your " + "vehicle is compatible by checking the manual that comes with your car.", + censor_vin(self.vin), + ) + return False + + @property + def parkinglocation(self) -> ParkingLocation | None: + if self.is_connected_services_enabled and "event" in self._status: + return ParkingLocation(self._status.get("event")) + return None + + @property + def sensors(self) -> Sensors | None: + if self.is_connected_services_enabled and "protectionState" in self._status: + return Sensors(self._status.get("protectionState")) + return None + + @property + def hvac(self) -> Hvac | None: + if self.is_connected_services_enabled: + rci = self._status_legacy.get("VehicleInfo", {}) + if self._status and "climate" in self._status: + return Hvac(self._status.get("climate")) + + elif "RemoteHvacInfo" in rci: + return Hvac(rci.get("RemoteHvacInfo"), True) + return None + + @property + def dashboard(self) -> Dashboard | None: + if self.is_connected_services_enabled: + return Dashboard(self) + return None + \ No newline at end of file diff --git a/mytoyota/sensors.py b/mytoyota/sensors.py deleted file mode 100644 index a08af557..00000000 --- a/mytoyota/sensors.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Sensor representation for mytoyota""" -from typing import Optional - -from mytoyota.const import CLOSED, INCAR, LOCKED, OFF, STATE, WARNING - - -class Hood: - """Representation of the hood of the car""" - - def __init__(self, hood: dict) -> None: - self.warning: Optional[bool] = hood.get(WARNING, None) - self.closed: Optional[bool] = hood.get(CLOSED, None) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return vars(self) - - -class Door: - """Representation of a door""" - - def __init__(self, door: dict) -> None: - self.warning: Optional[bool] = door.get(WARNING, None) - self.closed: Optional[bool] = door.get(CLOSED, None) - self.locked: Optional[bool] = door.get(LOCKED, None) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - WARNING: self.warning, - CLOSED: self.closed, - LOCKED: self.locked, - } - - -class Doors: - """Represent all car doors""" - - driverseat: Door - passengerseat: Door - rightrearseat: Door - leftrearseat: Door - trunk: Door - warning: bool = False - - def __init__(self, doors: dict): - self.warning = doors.get(WARNING, None) - - self.driverseat = Door(doors.get("driverSeatDoor", {})) - self.passengerseat = Door(doors.get("passengerSeatDoor", {})) - self.rightrearseat = Door(doors.get("rearRightSeatDoor", {})) - self.leftrearseat = Door(doors.get("rearLeftSeatDoor", {})) - self.trunk = Door(doors.get("backDoor", {})) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - WARNING: self.warning, - "driverseat": self.driverseat.as_dict(), - "passengerseat": self.passengerseat.as_dict(), - "rightrearseat": self.rightrearseat.as_dict(), - "leftrearseat": self.rightrearseat.as_dict(), - "trunk": self.trunk.as_dict(), - } - - -class Window: - """Representation of a window""" - - def __init__(self, window) -> None: - self.warning: Optional[bool] = window.get(WARNING, None) - self.state: Optional[str] = window.get(STATE, None) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - WARNING: self.warning, - STATE: self.state, - } - - -class Windows: - """Represent all car windows""" - - driverseat: Window - passengerseat: Window - rightrearseat: Window - leftrearseat: Window - - warning: bool = False - - def __init__(self, windows: dict) -> None: - self.warning = windows.get(WARNING, None) - - self.driverseat = Window(windows.get("driverSeatWindow", {})) - self.passengerseat = Window(windows.get("passengerSeatWindow", {})) - self.rightrearseat = Window(windows.get("rearRightSeatWindow", {})) - self.leftrearseat = Window(windows.get("rearLeftSeatWindow", {})) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - WARNING: self.warning, - "driverseat": self.driverseat.as_dict(), - "passengerseat": self.passengerseat.as_dict(), - "rightrearseat": self.rightrearseat.as_dict(), - "leftrearseat": self.rightrearseat.as_dict(), - } - - -class Light: - """Representation of the lights""" - - def __init__(self, light: dict) -> None: - self.warning: Optional[bool] = light.get(WARNING, None) - self.off: Optional[bool] = light.get(OFF, None) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - WARNING: self.warning, - OFF: self.off, - } - - -class Lights: - """Represent all car windows""" - - front: Light - back: Light - hazard: Light - warning: bool = False - - def __init__(self, lights: dict) -> None: - self.warning = lights.get(WARNING, None) - - self.front = Light(lights.get("headLamp", {})) - self.back = Light(lights.get("tailLamp", {})) - self.hazard = Light(lights.get("hazardLamp", {})) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - "warning": self.warning, - "front": self.front.as_dict(), - "back": self.back.as_dict(), - "hazard": self.back.as_dict(), - } - - -class Key: - """Representation of the ignition""" - - def __init__(self, key: dict) -> None: - self.warning: Optional[bool] = key.get(WARNING, None) - self.in_car: Optional[bool] = key.get(INCAR, None) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return vars(self) diff --git a/mytoyota/status.py b/mytoyota/status.py deleted file mode 100644 index a85c53ce..00000000 --- a/mytoyota/status.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Vehicle status representation for mytoyota""" -import logging -from typing import Optional, Union - -from mytoyota.const import DOORS, HOOD, KEY, LIGHTS, WINDOWS -from mytoyota.sensors import Doors, Hood, Key, Lights, Windows -from mytoyota.utils.conversions import convert_to_miles - -_LOGGER: logging.Logger = logging.getLogger(__package__) - - -class Odometer: - """Odometer representation""" - - mileage: int = None - unit: str = None - - def __init__(self, data: dict) -> None: - self.mileage = data.get("mileage", None) - self.unit = data.get("mileage_unit", None) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return odometer as dict.""" - return vars(self) - - -class Sensors: - """Vehicle sensors representation""" - - lights: Optional[Lights] = None - hood: Optional[Hood] = None - doors: Optional[Doors] = None - windows: Optional[Windows] = None - key: Optional[Key] = None - - overallstatus: str = None - last_updated: str = None - - def __init__(self, status: dict): - _LOGGER.debug("Raw sensor data: %s", str(status)) - - self.overallstatus = status.get("overallStatus", None) - self.last_updated = status.get("timestamp", None) - - self.lights = Lights(status[LIGHTS]) if LIGHTS in status else None - self.hood = Hood(status[HOOD]) if HOOD in status else None - self.doors = Doors(status[DOORS]) if DOORS in status else None - self.windows = Windows(status[WINDOWS]) if WINDOWS in status else None - self.key = Key(status[KEY]) if KEY in status else None - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return as dict.""" - return { - "overallstatus": self.overallstatus, - "lights": self.lights.as_dict() if self.lights is not None else None, - "hood": self.hood.as_dict() if self.hood is not None else None, - "doors": self.doors.as_dict() if self.doors is not None else None, - "windows": self.windows.as_dict() if self.windows is not None else None, - "key": self.key.as_dict() if self.key is not None else None, - "last_updated": self.last_updated, - } - - -class Energy: - """Represents fuel level, battery capacity and range""" - - level: Optional[int] = None - range: Optional[int] = None - range_with_aircon: Optional[int] = None - type: str = None - last_updated: Optional[str] = None - legacy: bool = False - - chargeinfo: dict = None - - _is_imperial: bool = False - - def __init__( - self, data: Union[list, dict] = None, unit: str = "km", legacy: bool = False - ): - - if unit == "mi": - self._is_imperial = True - - # Support for old endpoint for fuel level. Some cars still uses this. - if legacy: - self._set_legacy(data) - else: - self._set(data) - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return odometer as dict.""" - return vars(self) - - def _set(self, data: list) -> None: - """Set attributes""" - - range_in_km = data[0].get("remainingRange", None) - - self.level = data[0].get("level", None) - self.range = ( - convert_to_miles(range_in_km) - if range_in_km is not None and self._is_imperial - else range_in_km - ) - self.type = data[0].get("type", "Unknown").capitalize() - self.last_updated = data[0].get("timestamp", None) - - def _set_legacy(self, data: dict) -> None: - """Set attributes using legacy data""" - self.legacy = True - - self.level = data.get("Fuel", None) - - def set_battery_attributes(self, data: dict) -> None: - """Set charge info from legacy endpoint""" - - range_in_km = data.get("EvDistanceInKm", None) - range_in_km_with_aircon = data.get("EvDistanceWithAirCoInKm", None) - - self.range = ( - convert_to_miles(range_in_km) - if range_in_km is not None and self._is_imperial - else range_in_km - ) - self.range_with_aircon = ( - convert_to_miles(range_in_km_with_aircon) - if range_in_km_with_aircon is not None and self._is_imperial - else range_in_km_with_aircon - ) - - self.chargeinfo = { - "status": data.get("ChargingStatus", None), - "remaining_time": data.get("RemainingChargeTime", None), - "remaining_amount": data.get("ChargeRemainingAmount", None), - "start_time": data.get("ChargeStartTime", None), - "end_time": data.get("ChargeEndTime", None), - } diff --git a/mytoyota/vehicle.py b/mytoyota/vehicle.py deleted file mode 100644 index c9d73c94..00000000 --- a/mytoyota/vehicle.py +++ /dev/null @@ -1,173 +0,0 @@ -"""Vehicle representation""" -import logging -from typing import Optional - -from mytoyota.hvac import Hvac -from mytoyota.location import ParkingLocation -from mytoyota.status import Energy, Odometer, Sensors -from mytoyota.utils.formatters import format_odometer -from mytoyota.utils.logs import censor_vin - -_LOGGER: logging.Logger = logging.getLogger(__package__) - - -class VehicleStatistics: - """Vehicle statistics representation""" - - daily: list = None - weekly: list = None - monthly: list = None - yearly: list = None - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return vehicle statistics as dict.""" - return vars(self) - - -class Vehicle: - """Vehicle representation""" - - is_connected: bool = False - details: Optional[dict] = None - odometer: Optional[Odometer] = None - energy: Optional[Energy] = None - hvac: Optional[Hvac] = None - parking: Optional[ParkingLocation] = None - sensors: Optional[Sensors] = None - statistics: VehicleStatistics = VehicleStatistics() - - def __init__( # pylint: disable=too-many-branches - self, - vehicle_info: dict, - connected_services: Optional[dict] = None, - odometer: Optional[list] = None, - status: Optional[dict] = None, - remote_control: Optional[dict] = None, - ) -> None: - - # If no vehicle information is provided, abort. - if not vehicle_info: - _LOGGER.error("No vehicle information provided!") - return - - # Vehicle information - self.id = vehicle_info.get("id", None) # pylint: disable=invalid-name - self.vin = vehicle_info.get("vin", None) - self.alias = vehicle_info.get("alias", None) - - # Format vehicle details. - self.details = self._format_details(vehicle_info) - - self.is_connected = ( - self._has_connected_services_enabled(connected_services) - if connected_services - else False - ) - - if self.is_connected and self.vin: - - remote_control_info = ( - remote_control.get("VehicleInfo") if remote_control is not None else {} - ) - - # Extract mileage and if the car reports in km or mi - self.odometer = ( - Odometer(format_odometer(odometer)) if odometer else Odometer({}) - ) - - # Extract fuel level/Energy capacity information from status. - if status and "energy" in status: - _LOGGER.debug("Using energy data: %s", str(status.get("energy"))) - self.energy = Energy(status.get("energy"), self.odometer.unit) - # Use legacy odometer to get fuel level. Older cars still uses this. - elif odometer: - _LOGGER.debug("Using legacy odometer data: %s", str(odometer)) - self.energy = Energy( - format_odometer(odometer), self.odometer.unit, True - ) - fueltype = self.details.get("fuel", "Unknown") - # PATCH: Toyota Aygo reports wrong type. - if fueltype == "1.0P": - fueltype = "Petrol" - self.energy.type = fueltype - # Add charge information if car supports it. - if remote_control_info and "ChargeInfo" in remote_control_info: - self.energy.set_battery_attributes( - remote_control_info.get("ChargeInfo", {}) - ) - - if status: - # Extract parking information from status. - self.parking = ParkingLocation(status.get("event", {})) - - # Extracts window, door, lock and other information from status. - self.sensors = Sensors(status.get("protectionState", {})) - - # Extract HVAC information from endpoint - if remote_control_info and "RemoteHvacInfo" in remote_control_info: - self.hvac = Hvac(remote_control_info.get("RemoteHvacInfo", {}), True) - elif status and "climate" in status: - self.hvac = Hvac(status.get("climate")) - else: - self.hvac = None - - def __str__(self) -> str: - return str(self.as_dict()) - - def as_dict(self) -> dict: - """Return vehicle as dict.""" - return { - "id": self.id, - "alias": self.alias, - "vin": self.vin, - "details": self.details, - "status": { - "energy": self.energy.as_dict(), - "hvac": self.hvac.as_dict() if self.hvac is not None else {}, - "odometer": self.odometer.as_dict(), - "parking": self.parking.as_dict(), - "vehicle": self.sensors.as_dict(), - }, - "servicesEnabled": { - "connectedServices": self.is_connected, - }, - "statistics": self.statistics.as_dict(), - } - - def _has_connected_services_enabled(self, json_dict: dict) -> bool: - """Checks if the user has enabled connected services.""" - # Check if vin is not None. Toyota's servers is a bit flacky and can - # return garbage from connected_services endpoint, this is just to - # make sure that we don't throw a error message. - if self.vin: - if ( - "connectedService" in json_dict - and "status" in json_dict["connectedService"] - ): - if json_dict["connectedService"]["status"] == "ACTIVE": - return True - - _LOGGER.error( - "Please setup Connected Services if you want live data from the car. (%s)", - censor_vin(self.vin), - ) - return False - _LOGGER.error( - "Your vehicle does not support Connected services (%s). You can find out if your " - "vehicle is compatible by checking the manual that comes with your car.", - censor_vin(self.vin), - ) - return False - - @staticmethod - def _format_details(raw: dict) -> dict: - """Formats vehicle info into a dict.""" - details: dict = {} - for item in sorted(raw): - if item in ("vin", "alias", "id"): - continue - details[item] = raw[item] - return details diff --git a/tests/data/vehicle_JTMW1234565432109_odometer.json b/tests/data/vehicle_JTMW1234565432109_odometer.json index a49b142c..8b80161e 100644 --- a/tests/data/vehicle_JTMW1234565432109_odometer.json +++ b/tests/data/vehicle_JTMW1234565432109_odometer.json @@ -3,9 +3,5 @@ "type": "mileage", "value": 1004, "unit": "km" - }, - { - "type": "Fuel", - "value": 47.84 } ] diff --git a/tests/data/vehicle_JTMW1234565432109_odometer_legacy.json b/tests/data/vehicle_JTMW1234565432109_odometer_legacy.json new file mode 100644 index 00000000..a49b142c --- /dev/null +++ b/tests/data/vehicle_JTMW1234565432109_odometer_legacy.json @@ -0,0 +1,11 @@ +[ + { + "type": "mileage", + "value": 1004, + "unit": "km" + }, + { + "type": "Fuel", + "value": 47.84 + } +] diff --git a/tests/data/vehicle_JTMW1234565432109_status.json b/tests/data/vehicle_JTMW1234565432109_status.json index 2c1e404c..639dc16e 100644 --- a/tests/data/vehicle_JTMW1234565432109_status.json +++ b/tests/data/vehicle_JTMW1234565432109_status.json @@ -5,6 +5,14 @@ "timestamp": "1634051916000" }, "tripStatus": "0", + "energy": [ + { + "timestamp": "2021-09-19T14:02:37Z", + "type": "HYDROGEN", + "level": 58.8, + "remainingRange": 295 + } + ], "protectionState": { "overallStatus": "OK", "timestamp": "2021-10-12T15:22:53Z", diff --git a/tests/data/vehicle_JTMW1234565432109_status_legacy.json b/tests/data/vehicle_JTMW1234565432109_status_legacy.json index 8ed4a174..a1388b0c 100644 --- a/tests/data/vehicle_JTMW1234565432109_status_legacy.json +++ b/tests/data/vehicle_JTMW1234565432109_status_legacy.json @@ -1,3 +1,38 @@ { + "VehicleInfo": { + "RemoteHvacInfo": { + "InsideTemperature": 21, + "RemoteHvacMode": 0, + "RemoteHvacProhibitionSignal": 1, + "Temperaturelevel": 28, + "BlowerStatus": 0, + "FrontDefoggerStatus": 0, + "RearDefoggerStatus": 0, + "LatestAcStartTime": "2022-01-12T07:33:12Z", + "TemperatureDisplayFlag": 1, + "SettingTemperature": 21.0 + }, + "ChargeInfo": { + "PlugStatus": 12, + "PlugInHistory": 33, + "RemainingChargeTime": 65535, + "EvTravelableDistance": 45.9, + "EvTravelableDistanceSubtractionRate": 10, + "ChargeRemainingAmount": 75, + "SettingChangeAcceptanceStatus": 0, + "ChargeWeek": 0, + "ChargeStartTime": "42:35", + "ChargeEndTime": "42:35", + "ConnectorStatus": 2, + "BatteryPowerSupplyPossibleTime": 16383, + "ChargingStatus": "none", + "EvDistanceWithAirCoInKm": 41.31, + "GasolineTravelableDistance": 128, + "ChargeType": 15, + "GasolineTravelableDistanceUnit": 1, + "EvDistanceInKm": 45.9 + }, + "AcquisitionDatetime": "2022-01-12T16:48:39Z" + }, "ReturnCode": "000000" } diff --git a/tests/test_energy.py b/tests/test_energy.py deleted file mode 100644 index 10c96013..00000000 --- a/tests/test_energy.py +++ /dev/null @@ -1,231 +0,0 @@ -"""pytest tests for mytoyota.status.Energy""" - -from mytoyota.status import Energy - -# pylint: disable=no-self-use - - -class TestEnergy: - """pytest functions to test Energy""" - - @staticmethod - def _create_example_data(): - """Create list with predefined data""" - return [ - { - "timestamp": "2021-09-19T14:02:37Z", - "type": "HYDROGEN", - "level": 58.8, - "remainingRange": 295, - } - ] - - @staticmethod - def _create_example_legacy_data(): - """Create dict with predefined data""" - return {"Fuel": 69} - - @staticmethod - def _create_example_battery_data(): - """Create dict with predefined data""" - return { - "BatteryPowerSupplyPossibleTime": 16383, - "ChargeEndTime": "00:00", - "ChargeRemainingAmount": 100, - "ChargeStartTime": "22:10", - "ChargeType": 1, - "ChargeWeek": 5, - "ChargingStatus": "chargeComplete", - "ConnectorStatus": 5, - "EvDistanceInKm": 79.9, - "EvDistanceWithAirCoInKm": 73.51, - "EvTravelableDistance": 79.9, - "EvTravelableDistanceSubtractionRate": 8, - "PlugInHistory": 33, - "PlugStatus": 45, - "RemainingChargeTime": 65535, - "SettingChangeAcceptanceStatus": 0, - } - - def test_energy_km(self): - """Test energy in unit km""" - energy = Energy(self._create_example_data()) - - assert energy.level == 58.8 - assert energy.range == 295 - assert energy.type == "Hydrogen" - assert energy.last_updated == "2021-09-19T14:02:37Z" - - def test_energy_mi(self): - """Test energy in unit mi""" - energy = Energy(self._create_example_data(), unit="mi") - - assert energy.legacy is False - - assert energy.level == 58.8 - assert energy.range == 183.3045 - assert energy.type == "Hydrogen" - assert energy.last_updated == "2021-09-19T14:02:37Z" - - def test_energy_no_data(self): - """Test energy with no initialization data""" - energy = Energy([{}]) - - assert energy.legacy is False - - assert energy.level is None - assert energy.range is None - assert energy.type == "Unknown" - assert energy.last_updated is None - - def test_energy_str(self): - """Test energy converted to a string""" - energy = Energy(self._create_example_data()) - - string = str(energy) - assert isinstance(string, str) - assert ( - string == "{'level': 58.8, 'range': 295, 'type': 'Hydrogen', " - "'last_updated': '2021-09-19T14:02:37Z'}" - ) - - def test_energy_dict(self): - """Test energy converted to a dict""" - energy = Energy(self._create_example_data()) - - dictionary = energy.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == { - "level": 58.8, - "range": 295, - "type": "Hydrogen", - "last_updated": "2021-09-19T14:02:37Z", - } - - def test_energy_legacy_km(self): - """Test legacy energy in unit km""" - energy = Energy(self._create_example_legacy_data(), legacy=True) - - assert energy.legacy is True - assert energy.level == 69 - - energy.type = "Petrol" - - assert energy.type == "Petrol" - assert energy.chargeinfo is None - - energy.set_battery_attributes(self._create_example_battery_data()) - - assert energy.range == 79.9 - assert energy.range_with_aircon == 73.51 - assert isinstance(energy.chargeinfo, dict) - assert energy.chargeinfo == { - "status": "chargeComplete", - "remaining_time": 65535, - "remaining_amount": 100, - "start_time": "22:10", - "end_time": "00:00", - } - - def test_energy_legacy_mi(self): - """Test legacy energy in unit mi""" - energy = Energy(self._create_example_legacy_data(), unit="mi", legacy=True) - - assert energy.legacy is True - assert energy.level == 69 - - energy.type = "Petrol" - - assert energy.type == "Petrol" - assert energy.chargeinfo is None - - energy.set_battery_attributes(self._create_example_battery_data()) - - assert energy.range == 49.6476 - assert energy.range_with_aircon == 45.677 - assert isinstance(energy.chargeinfo, dict) - assert energy.chargeinfo == { - "status": "chargeComplete", - "remaining_time": 65535, - "remaining_amount": 100, - "start_time": "22:10", - "end_time": "00:00", - } - - def test_energy_legacy_no_data(self): - """Test energy with no initialization data""" - energy = Energy({}, legacy=True) - - assert energy.legacy is True - assert energy.level is None - - assert energy.type is None - assert energy.chargeinfo is None - - energy.set_battery_attributes({}) - - assert energy.range is None - assert energy.range_with_aircon is None - assert isinstance(energy.chargeinfo, dict) - assert energy.chargeinfo == { - "status": None, - "remaining_time": None, - "remaining_amount": None, - "start_time": None, - "end_time": None, - } - - def test_energy_legacy_str(self): - """Test energy converted to a string""" - energy = Energy(self._create_example_legacy_data(), legacy=True) - - string = str(energy) - assert isinstance(string, str) - assert string == "{'legacy': True, 'level': 69}" - - energy.type = "Petrol" - - energy.set_battery_attributes(self._create_example_battery_data()) - - string_with_battery = str(energy) - assert isinstance(string_with_battery, str) - assert ( - string_with_battery - == "{'legacy': True, 'level': 69, 'type': 'Petrol', 'range': 79.9, " - "'range_with_aircon': 73.51, " - "'chargeinfo': {'status': 'chargeComplete', 'remaining_time': 65535, " - "'remaining_amount': 100, " - "'start_time': '22:10', 'end_time': '00:00'}}" - ) - - def test_energy_legacy_dict(self): - """Test energy converted to a dict""" - energy = Energy(self._create_example_legacy_data(), legacy=True) - - dictionary = energy.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == { - "level": 69, - "legacy": True, - } - - energy.type = "Petrol" - - energy.set_battery_attributes(self._create_example_battery_data()) - - dict_with_battery = energy.as_dict() - assert isinstance(dict_with_battery, dict) - assert dict_with_battery == { - "level": 69, - "legacy": True, - "type": "Petrol", - "range": 79.9, - "range_with_aircon": 73.51, - "chargeinfo": { - "status": "chargeComplete", - "remaining_time": 65535, - "remaining_amount": 100, - "start_time": "22:10", - "end_time": "00:00", - }, - } diff --git a/tests/test_hvac.py b/tests/test_hvac.py index 07d9c362..63d5fb09 100644 --- a/tests/test_hvac.py +++ b/tests/test_hvac.py @@ -1,6 +1,6 @@ -"""pytest tests for mytoyota.hvac.Hvac""" +"""pytest tests for mytoyota.models.hvac.Hvac""" -from mytoyota.hvac import Hvac +from mytoyota.models.hvac import Hvac # pylint: disable=no-self-use @@ -87,6 +87,10 @@ def test_hvac(self): assert hvac.last_updated == "2020-10-16T03:50:15Z" + assert hvac.front_defogger_is_on is None + assert hvac.rear_defogger_is_on is None + assert hvac.blower_on is None + def test_hvac_legacy(self): """Test legacy Hvac""" hvac = self._create_example_legacy_data() @@ -96,9 +100,16 @@ def test_hvac_legacy(self): assert hvac.current_temperature == 22 assert hvac.target_temperature == 21 assert hvac.blower_on == 0 - assert hvac.front_defogger_on == 0 - assert hvac.rear_defogger_on == 0 - assert hvac.last_updated == "2020-10-16T03:50:15Z" + assert hvac.front_defogger_is_on is False + assert hvac.rear_defogger_is_on is False + assert hvac.last_updated is None + + assert hvac.started_at is None + assert hvac.status is None + assert hvac.type is None + assert hvac.duration is None + assert hvac.options is None + assert hvac.command_id is None def test_hvac_no_data(self): """Test Hvac with no initialization data""" @@ -116,45 +127,3 @@ def test_hvac_no_data(self): assert hvac.options is None assert hvac.last_updated is None - - def test_hvac_str(self): - """Test Hvac converted to a string""" - hvac = self._create_example_data() - - string = str(hvac) - assert isinstance(string, str) - assert ( - string == "{'current_temperature': 22, 'target_temperature': 21, " - "'started_at': '', 'status': '', 'type': '', 'duration': 1, " - "'options': {'frontDefogger': '', 'frontDriverSeatHeater': '', " - "'frontPassengerSeatHeater': '', 'mirrorHeater': '', 'rearDefogger': '', " - "'rearDriverSeatHeater': '', 'rearPassengerSeatHeater': '', " - "'steeringHeater': ''}, 'command_id': '', 'last_updated': '2020-10-16T03:50:15Z'}" - ) - - def test_hvac_dict(self): - """Test Hvac converted to a dict""" - hvac = self._create_example_data() - - dictionary = hvac.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == { - "current_temperature": 22, - "target_temperature": 21, - "started_at": "", - "status": "", - "type": "", - "duration": 1, - "options": { - "frontDefogger": "", - "frontDriverSeatHeater": "", - "frontPassengerSeatHeater": "", - "mirrorHeater": "", - "rearDefogger": "", - "rearDriverSeatHeater": "", - "rearPassengerSeatHeater": "", - "steeringHeater": "", - }, - "command_id": "", - "last_updated": "2020-10-16T03:50:15Z", - } diff --git a/tests/test_odometer.py b/tests/test_odometer.py deleted file mode 100644 index 41a1b8f2..00000000 --- a/tests/test_odometer.py +++ /dev/null @@ -1,45 +0,0 @@ -"""pytest tests for mytoyota.status.Odometer""" - -from mytoyota.status import Odometer - -# pylint: disable=no-self-use - - -class TestOdometer: - """pytest functions to test Odometer""" - - def _create_example_odometer(self): - """Create an ParkingLocation with some predefined example data""" - return Odometer({"mileage": 765, "mileage_unit": "km"}) - - def test_odometer_km(self): - """Test a mileage specified in km""" - odo = self._create_example_odometer() - assert odo.mileage == 765 - assert odo.unit == "km" - - def test_odometer_miles(self): - """Test a mileage specified in miles""" - odo = Odometer({"mileage": 345, "mileage_unit": "miles"}) - assert odo.mileage == 345 - assert odo.unit == "miles" - - def test_odometer_no_data(self): - """Test Odometer with no initialization data""" - odo = Odometer({}) - assert odo.mileage is None - assert odo.unit is None - - def test_odometer_str(self): - """Test Odometer converted to a str""" - odo = self._create_example_odometer() - string = str(odo) - assert isinstance(string, (str)) - assert string == "{'mileage': 765, 'unit': 'km'}" - - def test_odometer_dict(self): - """Test Odometer converted to a dictionary""" - odo = self._create_example_odometer() - dictionary = odo.as_dict() - assert isinstance(dictionary, (dict)) - assert dictionary == {"mileage": 765, "unit": "km"} diff --git a/tests/test_parking_location.py b/tests/test_parking_location.py index ec51cb99..9b36e717 100644 --- a/tests/test_parking_location.py +++ b/tests/test_parking_location.py @@ -1,6 +1,6 @@ -"""pytest tests for mytoyota.location.ParkingLocation""" +"""pytest tests for mytoyota.models.location.ParkingLocation""" -from mytoyota.location import ParkingLocation # pylint: disable=import-error +from mytoyota.models.location import ParkingLocation # pylint: disable=import-error # pylint: disable=no-self-use @@ -24,22 +24,4 @@ def test_parking_location_no_data(self): location = ParkingLocation({}) assert location.latitude == 0.0 assert location.longitude == 0.0 - assert location.timestamp == 0 - - def test_parking_location_str(self): - """Test ParkingLocation converted to a str""" - location = self._create_example_parking_location() - string = str(location) - assert isinstance(string, (str)) - assert string == "{'latitude': 1.234, 'longitude': 5.678, 'timestamp': 987654}" - - def test_parking_location_dict(self): - """Test ParkingLocation converted to a dictionary""" - location = self._create_example_parking_location() - dictionary = location.as_dict() - assert isinstance(dictionary, (dict)) - assert dictionary == { - "timestamp": 987654, - "latitude": 1.234, - "longitude": 5.678, - } + assert location.timestamp is None diff --git a/tests/test_sensors.py b/tests/test_sensors.py index d087258b..0a9188f6 100644 --- a/tests/test_sensors.py +++ b/tests/test_sensors.py @@ -1,6 +1,17 @@ -"""pytest tests for mytoyota.sensors""" - -from mytoyota.sensors import Door, Doors, Hood, Key, Light, Lights, Window, Windows +"""pytest tests for mytoyota.models.sensors""" +import json +import os + +from mytoyota.models.sensors import ( + Door, + Doors, + Key, + Light, + Lights, + Sensors, + Window, + Windows, +) # pylint: disable=no-self-use @@ -8,35 +19,28 @@ class TestSensors: # pylint: disable=too-many-public-methods """pytest functions to test Sensors""" + @staticmethod + def _load_from_file(filename: str): + """Load a data structure from the specified JSON filename, and + return it.""" + with open(filename, encoding="UTF-8") as json_file: + return json.load(json_file) + def test_hood(self): """Test hood""" - hood = Hood({"warning": False, "closed": True}) + hood = Door({"warning": False, "closed": True}) assert hood.warning is False assert hood.closed is True + assert hood.locked is None def test_hood_no_data(self): """Test hood with no initialization data""" - hood = Hood({}) + hood = Door({}) assert hood.warning is None assert hood.closed is None - - def test_hood_str(self): - """Test hood converted to str""" - hood = Hood({"warning": False, "closed": True}) - - string = str(hood) - assert isinstance(string, str) - assert string == "{'warning': False, 'closed': True}" - - def test_hood_dict(self): - """Test hood converted to a dictionary""" - hood = Hood({"warning": False, "closed": True}) - - dictionary = hood.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == {"warning": False, "closed": True} + assert hood.locked is None @staticmethod def _create_example_door(): @@ -59,22 +63,6 @@ def test_door_no_data(self): assert door.closed is None assert door.locked is None - def test_door_str(self): - """Test door converted to str""" - door = self._create_example_door() - - string = str(door) - assert isinstance(string, str) - assert string == "{'warning': False, 'closed': True, 'locked': False}" - - def test_door_dict(self): - """Test door converted to a dictionary""" - door = self._create_example_door() - - dictionary = door.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == {"warning": False, "closed": True, "locked": False} - def test_doors(self): """Test Doors""" doors = { @@ -89,58 +77,23 @@ def test_doors(self): doors = Doors(doors) assert doors.warning is False - assert isinstance(doors.driverseat, Door) - assert isinstance(doors.passengerseat, Door) - assert isinstance(doors.rightrearseat, Door) - assert isinstance(doors.leftrearseat, Door) + assert isinstance(doors.driver_seat, Door) + assert isinstance(doors.passenger_seat, Door) + assert isinstance(doors.leftrear_seat, Door) + assert isinstance(doors.rightrear_seat, Door) assert isinstance(doors.trunk, Door) - string = str(doors) - assert isinstance(string, str) - assert ( - string == "{'warning': False, " - "'driverseat': {'warning': False, 'closed': True, 'locked': False}, " - "'passengerseat': {'warning': False, 'closed': True, 'locked': False}, " - "'rightrearseat': {'warning': False, 'closed': True, 'locked': False}, " - "'leftrearseat': {'warning': False, 'closed': True, 'locked': False}, " - "'trunk': {'warning': False, 'closed': True, " - "'locked': False}}" - ) - - dictionary = doors.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == { - "warning": False, - "driverseat": {"warning": False, "closed": True, "locked": False}, - "passengerseat": {"warning": False, "closed": True, "locked": False}, - "rightrearseat": {"warning": False, "closed": True, "locked": False}, - "leftrearseat": {"warning": False, "closed": True, "locked": False}, - "trunk": {"warning": False, "closed": True, "locked": False}, - } - def test_doors_no_data(self): """Test Windows with no initialization data""" doors = Doors({}) assert doors.warning is None - assert isinstance(doors.driverseat, Door) - assert isinstance(doors.passengerseat, Door) - assert isinstance(doors.rightrearseat, Door) - assert isinstance(doors.leftrearseat, Door) + assert isinstance(doors.driver_seat, Door) + assert isinstance(doors.passenger_seat, Door) + assert isinstance(doors.leftrear_seat, Door) + assert isinstance(doors.rightrear_seat, Door) assert isinstance(doors.trunk, Door) - dictionary = doors.as_dict() - - assert isinstance(dictionary, dict) - assert dictionary == { - "warning": None, - "driverseat": {"warning": None, "closed": None, "locked": None}, - "passengerseat": {"warning": None, "closed": None, "locked": None}, - "rightrearseat": {"warning": None, "closed": None, "locked": None}, - "leftrearseat": {"warning": None, "closed": None, "locked": None}, - "trunk": {"warning": None, "closed": None, "locked": None}, - } - @staticmethod def _create_example_window(): """Create a window with predefined data""" @@ -160,22 +113,6 @@ def test_window_no_data(self): assert window.warning is None assert window.state is None - def test_window_str(self): - """Test window converted to str""" - window = self._create_example_window() - - string = str(window) - assert isinstance(string, str) - assert string == "{'warning': False, 'state': 'close'}" - - def test_window_dict(self): - """Test window converted to a dictionary""" - window = self._create_example_window() - - dictionary = window.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == {"warning": False, "state": "close"} - def test_windows(self): """Test Windows""" windows = { @@ -189,51 +126,20 @@ def test_windows(self): windows = Windows(windows) assert windows.warning is False - assert isinstance(windows.driverseat, Window) - assert isinstance(windows.passengerseat, Window) - assert isinstance(windows.rightrearseat, Window) - assert isinstance(windows.leftrearseat, Window) - - string = str(windows) - assert isinstance(string, str) - assert ( - string - == "{'warning': False, 'driverseat': {'warning': False, 'state': 'close'}, " - "'passengerseat': {'warning': False, 'state': 'close'}, " - "'rightrearseat': {'warning': False, 'state': 'close'}, " - "'leftrearseat': {'warning': False, 'state': 'close'}}" - ) - - dictionary = windows.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == { - "warning": False, - "driverseat": {"warning": False, "state": "close"}, - "passengerseat": {"warning": False, "state": "close"}, - "rightrearseat": {"warning": False, "state": "close"}, - "leftrearseat": {"warning": False, "state": "close"}, - } + assert isinstance(windows.driver_seat, Window) + assert isinstance(windows.passenger_seat, Window) + assert isinstance(windows.rightrear_seat, Window) + assert isinstance(windows.leftrear_seat, Window) def test_windows_no_data(self): """Test Windows with no initialization data""" windows = Windows({}) assert windows.warning is None - assert isinstance(windows.driverseat, Window) - assert isinstance(windows.passengerseat, Window) - assert isinstance(windows.rightrearseat, Window) - assert isinstance(windows.leftrearseat, Window) - - dictionary = windows.as_dict() - - assert isinstance(dictionary, dict) - assert dictionary == { - "warning": None, - "driverseat": {"warning": None, "state": None}, - "passengerseat": {"warning": None, "state": None}, - "rightrearseat": {"warning": None, "state": None}, - "leftrearseat": {"warning": None, "state": None}, - } + assert isinstance(windows.driver_seat, Window) + assert isinstance(windows.passenger_seat, Window) + assert isinstance(windows.rightrear_seat, Window) + assert isinstance(windows.leftrear_seat, Window) @staticmethod def _create_example_light(): @@ -254,22 +160,6 @@ def test_light_no_data(self): assert light.warning is None assert light.off is None - def test_light_str(self): - """Test light converted to str""" - light = self._create_example_light() - - string = str(light) - assert isinstance(string, str) - assert string == "{'warning': False, 'off': True}" - - def test_light_dict(self): - """Test light converted to a dictionary""" - light = self._create_example_light() - - dictionary = light.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == {"warning": False, "off": True} - def test_lights(self): """Test ligts""" lights = { @@ -282,46 +172,18 @@ def test_lights(self): lights = Lights(lights) assert lights.warning is False - assert isinstance(lights.front, Light) - assert isinstance(lights.back, Light) - assert isinstance(lights.hazard, Light) - - string = str(lights) - assert isinstance(string, str) - assert ( - string == "{'warning': False, " - "'front': {'warning': False, 'off': True}, " - "'back': {'warning': False, 'off': True}, " - "'hazard': {'warning': False, 'off': True}}" - ) - - dictionary = lights.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == { - "warning": False, - "front": {"warning": False, "off": True}, - "back": {"warning": False, "off": True}, - "hazard": {"warning": False, "off": True}, - } + assert isinstance(lights.headlights, Light) + assert isinstance(lights.taillights, Light) + assert isinstance(lights.hazardlights, Light) def test_lights_no_data(self): """Test Lights with no initialization data""" lights = Lights({}) assert lights.warning is None - assert isinstance(lights.front, Light) - assert isinstance(lights.back, Light) - assert isinstance(lights.hazard, Light) - - dictionary = lights.as_dict() - - assert isinstance(dictionary, dict) - assert dictionary == { - "warning": None, - "front": {"warning": None, "off": None}, - "back": {"warning": None, "off": None}, - "hazard": {"warning": None, "off": None}, - } + assert isinstance(lights.headlights, Light) + assert isinstance(lights.taillights, Light) + assert isinstance(lights.hazardlights, Light) def test_key(self): """Test key""" @@ -337,18 +199,28 @@ def test_key_no_data(self): assert key.warning is None assert key.in_car is None - def test_key_str(self): - """Test key converted to str""" - key = Key({"warning": False, "inCar": True}) - - string = str(key) - assert isinstance(string, str) - assert string == "{'warning': False, 'in_car': True}" - - def test_key_dict(self): - """Test key converted to a dictionary""" - key = Key({"warning": False, "inCar": True}) - - dictionary = key.as_dict() - assert isinstance(dictionary, dict) - assert dictionary == {"warning": False, "in_car": True} + def test_sensors(self): + """Test sensors""" + data_files = os.path.join(os.path.curdir, "tests", "data") + fixture = self._load_from_file( + os.path.join(data_files, "vehicle_JTMW1234565432109_status.json") + ) + sensors = Sensors(fixture.get("protectionState")) + + assert sensors.overallstatus == "OK" + assert sensors.last_updated == "2021-10-12T15:22:53Z" + assert isinstance(sensors.doors, Doors) + assert sensors.doors.driver_seat.warning is False + assert sensors.doors.driver_seat.closed is True + assert sensors.doors.driver_seat.locked is True + assert isinstance(sensors.hood, Door) + assert sensors.hood.warning is False + assert sensors.hood.closed is True + assert sensors.hood.locked is None + assert isinstance(sensors.lights, Lights) + assert sensors.lights.headlights.warning is False + assert sensors.lights.headlights.off is True + assert isinstance(sensors.windows, Windows) + assert sensors.windows.passenger_seat.warning is False + assert sensors.windows.passenger_seat.state == "close" + assert isinstance(sensors.key, Key) diff --git a/tests/test_utils.py b/tests/test_utils.py index a6ae8315..da6a68ba 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ """pytest tests for mytoyota.utils""" - -import pytest # pylint: disable=import-error +# pylint: disable=import-error +import pytest from mytoyota.exceptions import ToyotaInvalidToken diff --git a/tests/test_vehicle.py b/tests/test_vehicle.py index c49595dd..5fb1f7eb 100644 --- a/tests/test_vehicle.py +++ b/tests/test_vehicle.py @@ -1,8 +1,12 @@ -"""pytest tests for mytoyota.vehicle.Vehicle""" +"""pytest tests for mytoyota.models.vehicle.Vehicle""" import json import os -from mytoyota.vehicle import Vehicle +from mytoyota.models.dashboard import Dashboard +from mytoyota.models.hvac import Hvac +from mytoyota.models.location import ParkingLocation +from mytoyota.models.sensors import Sensors +from mytoyota.models.vehicle import Vehicle # pylint: disable=no-self-use @@ -21,41 +25,47 @@ def test_vehicle_no_data(self): """Test vehicle with no initialization data""" vehicle = Vehicle({}) - assert hasattr(vehicle, "id") is False - assert hasattr(vehicle, "vin") is False - assert hasattr(vehicle, "alias") is False + assert vehicle.id is None + assert vehicle.vin is None + assert vehicle.alias is None + assert vehicle.hybrid is None + assert vehicle.fueltype == "Unknown" assert vehicle.details is None - assert vehicle.is_connected is False - assert vehicle.odometer is None - assert vehicle.energy is None + assert vehicle.is_connected_services_enabled is False + assert vehicle.dashboard is None + assert vehicle.sensors is None assert vehicle.hvac is None - assert vehicle.parking is None + assert vehicle.parkinglocation is None def test_vehicle_init_no_status(self): """Test vehicle initialization with no status""" data_files = os.path.join(os.path.curdir, "tests", "data") - fixtures = self._load_from_file(os.path.join(data_files, "vehicles.json")) + vehicle_fixtures = self._load_from_file( + os.path.join(data_files, "vehicles.json") + ) - for veh in fixtures: + for veh in vehicle_fixtures: vehicle = Vehicle(vehicle_info=veh) - assert vehicle.vin == veh.get("vin") - assert vehicle.is_connected is False - assert vehicle.odometer is None - assert vehicle.energy is None + assert vehicle.is_connected_services_enabled is False + assert vehicle.dashboard is None + assert vehicle.sensors is None assert vehicle.hvac is None - assert vehicle.parking is None + assert vehicle.parkinglocation is None + assert vehicle.sensors is None def test_vehicle_init(self): """Test vehicle initialization with connected services""" data_files = os.path.join(os.path.curdir, "tests", "data") - fixtures = self._load_from_file(os.path.join(data_files, "vehicles.json")) + vehicle_fixtures = self._load_from_file( + os.path.join(data_files, "vehicles.json") + ) - for veh in fixtures: + for veh in vehicle_fixtures: vehicle = Vehicle( vehicle_info=veh, connected_services={"connectedService": {"status": "ACTIVE"}}, @@ -63,20 +73,117 @@ def test_vehicle_init(self): assert vehicle.vin == veh.get("vin") assert vehicle.alias == veh.get("alias") + assert vehicle.id == veh.get("id") + assert vehicle.hybrid == veh.get("hybrid") + assert isinstance(vehicle.fueltype, str) + assert isinstance(vehicle.details, dict) print(vehicle.id) if vehicle.vin is None: - assert vehicle.is_connected is False - assert vehicle.odometer is None - assert vehicle.energy is None - assert vehicle.hvac is None - assert vehicle.parking is None + assert vehicle.is_connected_services_enabled is False + assert vehicle.dashboard is None assert vehicle.sensors is None - else: - assert vehicle.is_connected is True - assert vehicle.odometer is not None - assert vehicle.energy is None assert vehicle.hvac is None - assert vehicle.parking is None + assert vehicle.parkinglocation is None + else: + assert vehicle.is_connected_services_enabled is True + assert isinstance(vehicle.dashboard, Dashboard) assert vehicle.sensors is None + assert vehicle.hvac is None + assert vehicle.parkinglocation is None + assert isinstance(vehicle.dashboard, Dashboard) + + def test_vehicle_init_status(self): + """Test vehicle initialization with connected services with status""" + + data_files = os.path.join(os.path.curdir, "tests", "data") + + vehicle_fixtures = self._load_from_file( + os.path.join(data_files, "vehicles.json") + ) + odometer_fixture = self._load_from_file( + os.path.join(data_files, "vehicle_JTMW1234565432109_odometer.json") + ) + status_fixture = self._load_from_file( + os.path.join(data_files, "vehicle_JTMW1234565432109_status.json") + ) + + vehicle = Vehicle( + vehicle_info=vehicle_fixtures[0], + connected_services={"connectedService": {"status": "ACTIVE"}}, + odometer=odometer_fixture, + status=status_fixture, + ) + + assert vehicle.fueltype == status_fixture["energy"][0]["type"].capitalize() + assert isinstance(vehicle.parkinglocation, ParkingLocation) + assert isinstance(vehicle.sensors, Sensors) + assert vehicle.hvac is None + assert isinstance(vehicle.dashboard, Dashboard) + assert vehicle.dashboard.legacy is False + assert vehicle.dashboard.fuel_level == status_fixture["energy"][0]["level"] + assert vehicle.dashboard.is_metric is True + assert vehicle.dashboard.odometer == odometer_fixture[0]["value"] + assert vehicle.dashboard.range == status_fixture["energy"][0]["remainingRange"] + assert vehicle.dashboard.battery_level is None + assert vehicle.dashboard.battery_range is None + assert vehicle.dashboard.battery_range_with_aircon is None + assert vehicle.dashboard.charging_status is None + assert vehicle.dashboard.remaining_charge_time is None + + def test_vehicle_init_status_legacy(self): + """Test vehicle initialization with connected services with legacy status""" + + data_files = os.path.join(os.path.curdir, "tests", "data") + + vehicle_fixtures = self._load_from_file( + os.path.join(data_files, "vehicles.json") + ) + odometer_fixture = self._load_from_file( + os.path.join(data_files, "vehicle_JTMW1234565432109_odometer_legacy.json") + ) + status_fixture = self._load_from_file( + os.path.join(data_files, "vehicle_JTMW1234565432109_status_legacy.json") + ) + + vehicle = Vehicle( + vehicle_info=vehicle_fixtures[0], + connected_services={"connectedService": {"status": "ACTIVE"}}, + odometer=odometer_fixture, + status_legacy=status_fixture, + ) + + assert vehicle.fueltype == "Petrol" + assert vehicle.parkinglocation is None + assert vehicle.sensors is None + assert isinstance(vehicle.hvac, Hvac) + assert isinstance(vehicle.dashboard, Dashboard) + assert vehicle.dashboard.legacy is True + assert vehicle.dashboard.fuel_level == odometer_fixture[1]["value"] + assert vehicle.dashboard.is_metric is True + assert vehicle.dashboard.odometer == odometer_fixture[0]["value"] + assert ( + vehicle.dashboard.range + == status_fixture["VehicleInfo"]["ChargeInfo"]["GasolineTravelableDistance"] + ) + assert ( + vehicle.dashboard.battery_level + == status_fixture["VehicleInfo"]["ChargeInfo"]["ChargeRemainingAmount"] + ) + assert ( + vehicle.dashboard.battery_range + == status_fixture["VehicleInfo"]["ChargeInfo"]["EvDistanceInKm"] + ) + assert ( + vehicle.dashboard.battery_range_with_aircon + == status_fixture["VehicleInfo"]["ChargeInfo"]["EvDistanceWithAirCoInKm"] + ) + assert ( + vehicle.dashboard.charging_status + == status_fixture["VehicleInfo"]["ChargeInfo"]["ChargingStatus"] + ) + assert ( + vehicle.dashboard.remaining_charge_time + == status_fixture["VehicleInfo"]["ChargeInfo"]["RemainingChargeTime"] + ) From f5e8e2640be62d8f495ce7ae1693041e20b07d0d Mon Sep 17 00:00:00 2001 From: DurgNomis-drol Date: Fri, 28 Jan 2022 21:48:05 +0100 Subject: [PATCH 2/6] Add docstrings, fix linting and some other minor tweaks --- mytoyota/client.py | 23 ---------- mytoyota/models/dashboard.py | 69 +++++++++++++++++++----------- mytoyota/models/data.py | 7 ++- mytoyota/models/hvac.py | 45 +++++++++++++------ mytoyota/models/location.py | 9 ++-- mytoyota/models/sensors.py | 77 ++++++++++++++++++++++++--------- mytoyota/models/vehicle.py | 83 ++++++++++++++++++++---------------- tests/test_myt.py | 11 ----- tests/test_vehicle.py | 20 +++++---- 9 files changed, 201 insertions(+), 143 deletions(-) diff --git a/mytoyota/client.py b/mytoyota/client.py index e3e23f55..09d10e01 100644 --- a/mytoyota/client.py +++ b/mytoyota/client.py @@ -264,29 +264,6 @@ async def get_vehicle_status(self, vehicle: dict) -> Vehicle: status_legacy=data[3], ) - async def get_vehicle_status_json(self, vehicle: dict) -> str: - """Returns vehicle status as json string. - - Collects and formats different vehicle status endpoints into - a easy accessible vehicle object. - - Args: - vehicle (dict): dict for each vehicle returned in get_vehicles(). - - Returns: - Json string containing odometer information, parking information, fuel and more. - - Raises: - ToyotaLoginError: An error returned when updating token or invalid login information. - ToyotaInternalError: An error occurred when making a request. - ToyotaApiError: Toyota's API returned an error. - """ - vehicle = await self.get_vehicle_status(vehicle) - - _LOGGER.debug("Returning it as json") - json_string = json.dumps(vehicle.as_dict(), indent=3) - return json_string - async def get_driving_statistics( # pylint: disable=too-many-branches self, vin: str, interval: str = MONTH, from_date: str = None, unit: str = METRIC ) -> list: diff --git a/mytoyota/models/dashboard.py b/mytoyota/models/dashboard.py index 4da32f2f..363ad80c 100644 --- a/mytoyota/models/dashboard.py +++ b/mytoyota/models/dashboard.py @@ -1,5 +1,6 @@ """Models for vehicle sensors.""" from __future__ import annotations + from typing import Any from mytoyota.utils.conversions import convert_to_miles @@ -14,71 +15,89 @@ class Dashboard: def __init__( self, vehicle, - ) -> None: + ) -> None: + """Dashboard.""" self._vehicle = vehicle vehicle_info = vehicle._status_legacy.get("VehicleInfo", {}) self._chargeinfo = vehicle_info.get("ChargeInfo", {}) - self._energy = vehicle._status.get("energy", [])[0] if vehicle._status.get("energy") else {} + self._energy = ( + vehicle._status.get("energy", [])[0] + if vehicle._status.get("energy") + else {} + ) @property def legacy(self) -> bool: - if "Fuel" in self._vehicle._odometer: + """If the car uses the legacy endpoints.""" + if "Fuel" in self._vehicle.odometer: return True return False - + @property def is_metric(self) -> bool: - return self._vehicle._odometer.get("mileage_unit") == "km" - + """If the car is reporting data in metric.""" + return self._vehicle.odometer.get("mileage_unit") == "km" + @property def odometer(self) -> int | None: - return self._vehicle._odometer.get("mileage") - + """Shows the odometer distance.""" + return self._vehicle.odometer.get("mileage") + @property def fuel_level(self) -> float | None: + """Shows the fuellevel of the vehicle.""" if self.legacy: - return self._vehicle._odometer.get("Fuel") + return self._vehicle.odometer.get("Fuel") return self._energy.get("level") - + @property - def range(self) -> float | None: - range = ( - self._chargeinfo.get("GasolineTravelableDistance") - if self.legacy + def fuel_range(self) -> float | None: + """Shows the range if available.""" + fuel_range = ( + self._chargeinfo.get("GasolineTravelableDistance") + if self.legacy else self._energy.get("remainingRange", None) ) - return convert_to_miles(range) if not self.is_metric else range - + return convert_to_miles(fuel_range) if not self.is_metric else fuel_range + @property def battery_level(self) -> float | None: + """Shows the battery level if a hybrid.""" if self.legacy: return self._chargeinfo.get("ChargeRemainingAmount") return None - + @property def battery_range(self) -> float | None: + """Shows the battery range if a hybrid.""" if self.legacy: - range = self._chargeinfo.get("EvDistanceInKm") - return convert_to_miles(range) if not self.is_metric else range + battery_range = self._chargeinfo.get("EvDistanceInKm") + return ( + convert_to_miles(battery_range) if not self.is_metric else battery_range + ) return None - + @property def battery_range_with_aircon(self) -> float | None: + """Shows the battery range with aircon on, if a hybrid.""" if self.legacy: - range = self._chargeinfo.get("EvDistanceWithAirCoInKm") - return convert_to_miles(range) if not self.is_metric else range + battery_range = self._chargeinfo.get("EvDistanceWithAirCoInKm") + return ( + convert_to_miles(battery_range) if not self.is_metric else battery_range + ) return None - + @property def charging_status(self) -> str | None: + """Shows the charging status if a hybrid.""" if self.legacy: return self._chargeinfo.get("ChargingStatus") return None - + @property def remaining_charge_time(self) -> int | None: + """Shows the remaining time to a full charge, if a hybrid.""" if self.legacy: return self._chargeinfo.get("RemainingChargeTime") return None - \ No newline at end of file diff --git a/mytoyota/models/data.py b/mytoyota/models/data.py index d9aefb8a..3a517430 100644 --- a/mytoyota/models/data.py +++ b/mytoyota/models/data.py @@ -3,9 +3,12 @@ class VehicleData: + """Vehicle data base model.""" + def __init__(self, data: dict) -> None: self._data = data or {} - + @property def raw_json(self) -> Dict[str, Any]: - return self._data \ No newline at end of file + """Return the input data.""" + return self._data diff --git a/mytoyota/models/hvac.py b/mytoyota/models/hvac.py index d4f0f814..029cab00 100644 --- a/mytoyota/models/hvac.py +++ b/mytoyota/models/hvac.py @@ -1,10 +1,13 @@ """Models for vehicle sensors.""" from __future__ import annotations + from typing import Any from mytoyota.models.data import VehicleData + def get_attr_in_dict(data: dict[str, float], attr: str) -> float | None: + """Get a specific attribute from a dict""" return data.get(attr) @@ -17,75 +20,91 @@ def __init__(self, data: dict[str, Any], legacy: bool = False) -> None: # This option enables support for older cars. super().__init__(data) self.legacy = legacy - + @property def current_temperature(self) -> float | None: + """Current temperature.""" if self.legacy: return self._data.get("InsideTemperature") - return get_attr_in_dict(self._data.get("currentTemperatureIndication", {}), "value") - + return get_attr_in_dict( + self._data.get("currentTemperatureIndication", {}), "value" + ) + @property def target_temperature(self) -> float | None: + """Target temperature.""" if self.legacy: return self._data.get("SettingTemperature") return get_attr_in_dict(self._data.get("targetTemperature", {}), "value") - + @property def started_at(self) -> str | None: + """Hvac started at.""" if self.legacy: return None return self._data.get("startedAt") - + @property def status(self) -> str | None: + """Hvac status.""" if self.legacy: return None return self._data.get("status") - + @property def type(self) -> str | None: + """Hvac type.""" if self.legacy: return None return self._data.get("type") - + @property def duration(self) -> str | None: + """Hvac duration.""" if self.legacy: return None return self._data.get("duration") - + @property def options(self) -> dict | list | None: + """Hvac options.""" if self.legacy: return None return self._data.get("options") - + @property def command_id(self) -> str | int | None: + """Hvac command id.""" if self.legacy: return None return self._data.get("commandId") - + @property def front_defogger_is_on(self) -> bool | None: + """If the front defogger is on.""" if self.legacy: return self._data.get("FrontDefoggerStatus") == 1 return None - + @property def rear_defogger_is_on(self) -> bool | None: + """If the rear defogger is on.""" if self.legacy: return self._data.get("RearDefoggerStatus") == 1 return None @property def blower_on(self) -> int | None: + """Hvac blower setting.""" if self.legacy: return self._data.get("BlowerStatus") return None - + @property def last_updated(self) -> str | None: + """Hvac last updated.""" if self.legacy: return None - return get_attr_in_dict(self._data.get("currentTemperatureIndication", {}), "timestamp") \ No newline at end of file + return get_attr_in_dict( + self._data.get("currentTemperatureIndication", {}), "timestamp" + ) diff --git a/mytoyota/models/location.py b/mytoyota/models/location.py index eaeb4ff7..df9b0d05 100644 --- a/mytoyota/models/location.py +++ b/mytoyota/models/location.py @@ -1,8 +1,6 @@ """Models for vehicle location.""" from __future__ import annotations - -from sqlite3 import Timestamp from mytoyota.models.data import VehicleData @@ -11,12 +9,15 @@ class ParkingLocation(VehicleData): @property def latitude(self) -> float: + """Latitude.""" return float(self._data.get("lat", 0.0)) - + @property def longitude(self) -> float: + """Longitude.""" return float(self._data.get("lon", 0.0)) - + @property def timestamp(self) -> int | None: + """Timestamp.""" return self._data.get("timestamp") diff --git a/mytoyota/models/sensors.py b/mytoyota/models/sensors.py index 50f50dd4..e61cba87 100644 --- a/mytoyota/models/sensors.py +++ b/mytoyota/models/sensors.py @@ -1,5 +1,6 @@ """Models for vehicle sensors.""" from __future__ import annotations + from mytoyota.models.data import VehicleData @@ -8,116 +9,146 @@ class Door(VehicleData): @property def warning(self) -> bool | None: + """If warning exists for the door.""" return self._data.get("warning") - + @property def closed(self) -> bool | None: + """If the door is closed.""" return self._data.get("closed") - + @property def locked(self) -> bool | None: + """If the door is locked.""" return self._data.get("locked") + class Doors(VehicleData): """Trunk/doors/hood data model.""" @property def warning(self) -> bool | None: + """If warning exists for one of the doors.""" return self._data.get("warning") @property def driver_seat(self) -> Door: + """Driver seat door.""" return Door(self._data.get("driverSeatDoor")) - + @property def passenger_seat(self) -> Door: + """Passenger seat door.""" return Door(self._data.get("passengerSeatDoor")) - + @property def leftrear_seat(self) -> Door: + """Left rearseat door.""" return Door(self._data.get("rearLeftSeatDoor")) - + @property def rightrear_seat(self) -> Door: + """Right rearseat door.""" return Door(self._data.get("rearRightSeatDoor")) - + @property def trunk(self) -> Door: + """Trunk.""" return Door(self._data.get("backDoor")) + class Window(VehicleData): """Window data model.""" @property def warning(self) -> bool | None: + """If a warning exists for the window.""" return self._data.get("warning") - + @property def state(self) -> str | None: + """Window state.""" return self._data.get("state") + class Windows(VehicleData): """Windows data model.""" @property def warning(self) -> bool | None: + """If a warning exists for one of the windows.""" return self._data.get("warning") @property def driver_seat(self) -> Window: + """Driver seat window.""" return Window(self._data.get("driverSeatWindow")) - + @property def passenger_seat(self) -> Window: + """Passenger seat window.""" return Window(self._data.get("passengerSeatWindow")) - + @property def leftrear_seat(self) -> Window: + """Left rearseat window.""" return Window(self._data.get("rearLeftSeatWindow")) - + @property def rightrear_seat(self) -> Window: + """Right rearseat window.""" return Window(self._data.get("rearRightSeatWindow")) + class Light(VehicleData): """Vehicle light data model.""" @property def warning(self) -> bool | None: + """If a warning exists for the light.""" return self._data.get("warning") @property def off(self) -> bool | None: + """If the light is off.""" return self._data.get("off") + class Lights(VehicleData): """Vehicle lights data model.""" @property def warning(self) -> bool | None: + """If a warning exists for one of the lights.""" return self._data.get("warning") @property def headlights(self) -> Light: + """Headlights.""" return Light(self._data.get("headLamp")) - + @property def taillights(self) -> Light: + """Taillights.""" return Light(self._data.get("tailLamp")) - + @property def hazardlights(self) -> Light: + """Hazardlights.""" return Light(self._data.get("hazardLamp")) + class Key(VehicleData): """Keyfob data model.""" @property def warning(self) -> bool | None: + """If a warning exists for the key..""" return self._data.get("warning") - + @property def in_car(self) -> bool | None: + """If the key is in the car.""" return self._data.get("inCar") @@ -126,29 +157,35 @@ class Sensors(VehicleData): @property def overallstatus(self) -> str | None: + """If a warning exists for any of the sensors.""" return self._data.get("overallStatus") - + @property def last_updated(self) -> str | None: + """Last time data was recieved from the car.""" return self._data.get("timestamp") - + @property def lights(self) -> Lights: + """Lights.""" return Lights(self._data.get("lamps")) - + @property def hood(self) -> Door: + """Hood.""" return Door(self._data.get("hood")) - + @property def doors(self) -> Doors: + """Doors.""" return Doors(self._data.get("doors")) - + @property def windows(self) -> Windows: + """Windows.""" return Windows(self._data.get("windows")) - + @property def key(self) -> Key: + """Key.""" return Key(self._data.get("key")) - diff --git a/mytoyota/models/vehicle.py b/mytoyota/models/vehicle.py index 3a184761..d7f7c802 100644 --- a/mytoyota/models/vehicle.py +++ b/mytoyota/models/vehicle.py @@ -1,70 +1,77 @@ """Vehicle model.""" from __future__ import annotations + import logging from typing import Any -from mytoyota.models.hvac import Hvac + from mytoyota.models.dashboard import Dashboard +from mytoyota.models.hvac import Hvac from mytoyota.models.location import ParkingLocation from mytoyota.models.sensors import Sensors from mytoyota.utils.formatters import format_odometer - from mytoyota.utils.logs import censor_vin - _LOGGER: logging.Logger = logging.getLogger(__package__) + class Vehicle: """Vehicle data representation.""" def __init__( self, - vehicle_info: dict[str, Any] = {}, - connected_services: dict[str, Any] = {}, - odometer: list[Any] = [], - status: dict[str, Any] = {}, - status_legacy: dict[str, Any] = {}, - ) -> None: + vehicle_info: dict[str, Any], + connected_services: dict[str, Any], + odometer: list[Any] | None = None, + status: dict[str, Any] | None = None, + status_legacy: dict[str, Any] | None = None, + ) -> None: self._connected_services = connected_services self._vehicle_info = vehicle_info - self._odometer = format_odometer(odometer) - self._status = status - self._status_legacy = status_legacy - + + self.odometer = format_odometer(odometer) if odometer else {} + self._status = status if status else {} + self._status_legacy = status_legacy if status_legacy else {} + @property - def id(self) -> int | None: + def vehicle_id(self) -> int | None: + """Vehicle's id.""" return self._vehicle_info.get("id") @property def vin(self) -> str | None: + """Vehicle's vinnumber.""" return self._vehicle_info.get("vin") - + @property def alias(self) -> str | None: + """Vehicle's alias.""" return self._vehicle_info.get("alias") - + @property def hybrid(self) -> bool | None: + """If the vehicle is a hybrid.""" return self._vehicle_info.get("hybrid") @property def fueltype(self) -> str: - if "energy" in self._status and self._status["energy"]: - return self._status["energy"][0].get("type", "Unknown").capitalize() - + """Fuel type of the vehicle.""" + if self._status: + if "energy" in self._status and self._status["energy"]: + return self._status["energy"][0].get("type", "Unknown").capitalize() + fueltype = self._vehicle_info.get("fuel", "Unknown") return "Petrol" if fueltype == "1.0P" else fueltype - @property def details(self) -> dict[str, Any] | None: """Formats vehicle info into a dict.""" - d: dict[str, Any] = {} + det: dict[str, Any] = {} for i in sorted(self._vehicle_info): if i in ("vin", "alias", "id", "hybrid"): continue - d[i] = self._vehicle_info[i] - return d if d else None + det[i] = self._vehicle_info[i] + return det if det else None @property def is_connected_services_enabled(self) -> bool: @@ -72,7 +79,7 @@ def is_connected_services_enabled(self) -> bool: # Check if vin is not None. Toyota's servers is a bit flacky and can # return garbage from connected_services endpoint, this is just to # make sure that we don't throw a error message. - if self.vin: + if self.vin and self._connected_services: if ( "connectedService" in self._connected_services and "status" in self._connected_services["connectedService"] @@ -91,33 +98,37 @@ def is_connected_services_enabled(self) -> bool: censor_vin(self.vin), ) return False - + @property def parkinglocation(self) -> ParkingLocation | None: + """Last parking location.""" if self.is_connected_services_enabled and "event" in self._status: return ParkingLocation(self._status.get("event")) return None - + @property def sensors(self) -> Sensors | None: - if self.is_connected_services_enabled and "protectionState" in self._status: - return Sensors(self._status.get("protectionState")) + """Vehicle sensors.""" + if self.is_connected_services_enabled and self._status: + if "protectionState" in self._status: + return Sensors(self._status.get("protectionState")) return None - + @property def hvac(self) -> Hvac | None: + """Vehicle hvac.""" if self.is_connected_services_enabled: - rci = self._status_legacy.get("VehicleInfo", {}) if self._status and "climate" in self._status: return Hvac(self._status.get("climate")) - - elif "RemoteHvacInfo" in rci: - return Hvac(rci.get("RemoteHvacInfo"), True) + if self._status_legacy: + rci = self._status_legacy.get("VehicleInfo", {}) + if "RemoteHvacInfo" in rci: + return Hvac(rci.get("RemoteHvacInfo"), True) return None - + @property def dashboard(self) -> Dashboard | None: - if self.is_connected_services_enabled: + """Vehicle dashboard.""" + if self.is_connected_services_enabled and self.odometer: return Dashboard(self) return None - \ No newline at end of file diff --git a/tests/test_myt.py b/tests/test_myt.py index a63c12f2..453a959e 100644 --- a/tests/test_myt.py +++ b/tests/test_myt.py @@ -258,17 +258,6 @@ def test_get_vehicle_status(self): ) assert status is not None - def disabled___test_get_vehicle_status_json(self): - """Test the retrieval of the status of a vehicle""" - myt = self._create_offline_myt() - vehicle = self._lookup_vehicle(myt, 4444444) - assert vehicle is not None - # Retrieve the actual status of the vehicle - status_json = asyncio.get_event_loop().run_until_complete( - myt.get_vehicle_status_json(vehicle) - ) - assert json.loads(status_json) is not None - @pytest.mark.parametrize( "interval,unit", [ diff --git a/tests/test_vehicle.py b/tests/test_vehicle.py index 5fb1f7eb..f377444e 100644 --- a/tests/test_vehicle.py +++ b/tests/test_vehicle.py @@ -23,9 +23,9 @@ def _load_from_file(filename: str): def test_vehicle_no_data(self): """Test vehicle with no initialization data""" - vehicle = Vehicle({}) + vehicle = Vehicle({}, {}) - assert vehicle.id is None + assert vehicle.vehicle_id is None assert vehicle.vin is None assert vehicle.alias is None assert vehicle.hybrid is None @@ -47,7 +47,7 @@ def test_vehicle_init_no_status(self): ) for veh in vehicle_fixtures: - vehicle = Vehicle(vehicle_info=veh) + vehicle = Vehicle(vehicle_info=veh, connected_services={}) assert vehicle.is_connected_services_enabled is False assert vehicle.dashboard is None @@ -73,12 +73,12 @@ def test_vehicle_init(self): assert vehicle.vin == veh.get("vin") assert vehicle.alias == veh.get("alias") - assert vehicle.id == veh.get("id") + assert vehicle.vehicle_id == veh.get("id") assert vehicle.hybrid == veh.get("hybrid") assert isinstance(vehicle.fueltype, str) assert isinstance(vehicle.details, dict) - print(vehicle.id) + print(vehicle.vehicle_id) if vehicle.vin is None: assert vehicle.is_connected_services_enabled is False @@ -88,11 +88,10 @@ def test_vehicle_init(self): assert vehicle.parkinglocation is None else: assert vehicle.is_connected_services_enabled is True - assert isinstance(vehicle.dashboard, Dashboard) + assert vehicle.dashboard is None assert vehicle.sensors is None assert vehicle.hvac is None assert vehicle.parkinglocation is None - assert isinstance(vehicle.dashboard, Dashboard) def test_vehicle_init_status(self): """Test vehicle initialization with connected services with status""" @@ -125,7 +124,10 @@ def test_vehicle_init_status(self): assert vehicle.dashboard.fuel_level == status_fixture["energy"][0]["level"] assert vehicle.dashboard.is_metric is True assert vehicle.dashboard.odometer == odometer_fixture[0]["value"] - assert vehicle.dashboard.range == status_fixture["energy"][0]["remainingRange"] + assert ( + vehicle.dashboard.fuel_range + == status_fixture["energy"][0]["remainingRange"] + ) assert vehicle.dashboard.battery_level is None assert vehicle.dashboard.battery_range is None assert vehicle.dashboard.battery_range_with_aircon is None @@ -164,7 +166,7 @@ def test_vehicle_init_status_legacy(self): assert vehicle.dashboard.is_metric is True assert vehicle.dashboard.odometer == odometer_fixture[0]["value"] assert ( - vehicle.dashboard.range + vehicle.dashboard.fuel_range == status_fixture["VehicleInfo"]["ChargeInfo"]["GasolineTravelableDistance"] ) assert ( From 9bbac616421a88476e3d428988a59964fee54741 Mon Sep 17 00:00:00 2001 From: DurgNomis-drol Date: Sun, 30 Jan 2022 16:56:10 +0100 Subject: [PATCH 3/6] Add defaults for some properties --- mytoyota/models/vehicle.py | 6 +++--- tests/test_vehicle.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mytoyota/models/vehicle.py b/mytoyota/models/vehicle.py index d7f7c802..6699b1ea 100644 --- a/mytoyota/models/vehicle.py +++ b/mytoyota/models/vehicle.py @@ -46,12 +46,12 @@ def vin(self) -> str | None: @property def alias(self) -> str | None: """Vehicle's alias.""" - return self._vehicle_info.get("alias") + return self._vehicle_info.get("alias", "My vehicle") @property - def hybrid(self) -> bool | None: + def hybrid(self) -> bool: """If the vehicle is a hybrid.""" - return self._vehicle_info.get("hybrid") + return self._vehicle_info.get("hybrid", False) @property def fueltype(self) -> str: diff --git a/tests/test_vehicle.py b/tests/test_vehicle.py index f377444e..8a3fce95 100644 --- a/tests/test_vehicle.py +++ b/tests/test_vehicle.py @@ -27,8 +27,8 @@ def test_vehicle_no_data(self): assert vehicle.vehicle_id is None assert vehicle.vin is None - assert vehicle.alias is None - assert vehicle.hybrid is None + assert vehicle.alias == "My vehicle" + assert vehicle.hybrid is False assert vehicle.fueltype == "Unknown" assert vehicle.details is None assert vehicle.is_connected_services_enabled is False @@ -72,9 +72,9 @@ def test_vehicle_init(self): ) assert vehicle.vin == veh.get("vin") - assert vehicle.alias == veh.get("alias") + assert vehicle.alias == veh.get("alias", "My vehicle") assert vehicle.vehicle_id == veh.get("id") - assert vehicle.hybrid == veh.get("hybrid") + assert vehicle.hybrid == veh.get("hybrid", False) assert isinstance(vehicle.fueltype, str) assert isinstance(vehicle.details, dict) From 6764415aca6ec094b297b62f226f140b544683a9 Mon Sep 17 00:00:00 2001 From: DurgNomis-drol Date: Sun, 30 Jan 2022 18:15:51 +0100 Subject: [PATCH 4/6] Minor code improvements --- mytoyota/models/dashboard.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/mytoyota/models/dashboard.py b/mytoyota/models/dashboard.py index 363ad80c..c9b0f7aa 100644 --- a/mytoyota/models/dashboard.py +++ b/mytoyota/models/dashboard.py @@ -1,20 +1,20 @@ """Models for vehicle sensors.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from mytoyota.utils.conversions import convert_to_miles +if TYPE_CHECKING: + from mytoyota.models.vehicle import Vehicle + class Dashboard: """Instrumentation data model.""" - _chargeinfo: dict[str, Any] - _energy: dict[str, Any] - def __init__( self, - vehicle, + vehicle: Vehicle, ) -> None: """Dashboard.""" self._vehicle = vehicle @@ -22,9 +22,7 @@ def __init__( vehicle_info = vehicle._status_legacy.get("VehicleInfo", {}) self._chargeinfo = vehicle_info.get("ChargeInfo", {}) self._energy = ( - vehicle._status.get("energy", [])[0] - if vehicle._status.get("energy") - else {} + vehicle._status.get("energy", [])[0] if "energy" in vehicle._status else {} ) @property From 270b8c7008e0fadf16208462d0f1fcfa23fb40af Mon Sep 17 00:00:00 2001 From: DurgNomis-drol Date: Sun, 30 Jan 2022 18:21:14 +0100 Subject: [PATCH 5/6] Removed a type annotation --- mytoyota/models/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mytoyota/models/dashboard.py b/mytoyota/models/dashboard.py index c9b0f7aa..f34785c2 100644 --- a/mytoyota/models/dashboard.py +++ b/mytoyota/models/dashboard.py @@ -1,7 +1,7 @@ """Models for vehicle sensors.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from mytoyota.utils.conversions import convert_to_miles From bf9e8d4cd6c9875adaa80e58de0b81b7bfa012a2 Mon Sep 17 00:00:00 2001 From: John de Rooij Date: Sun, 30 Jan 2022 21:24:18 +0100 Subject: [PATCH 6/6] Updated example code in README.md to match actual implementation (#128) --- README.md | 41 ++++++----------------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 6c82700c..234943e9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ async def get_information(): # Returns live data from car/last time you used it as an object. vehicle = await client.get_vehicle_status(car) - # You can either get them all async (Recommended) or sync (Look further down). data = await asyncio.gather( *[ @@ -59,53 +58,25 @@ async def get_information(): ] ) - # You can deposit each result into premade object holder for statistics. This will make it easier to use in your code. - - vehicle.statistics.daily = data[0] - vehicle.statistics.weekly = data[1] - vehicle.statistics.monthly = data[2] - vehicle.statistics.yearly = data[3] - - # You can access odometer data like this: - mileage = vehicle.odometer.mileage + mileage = vehicle.dashboard.odometer # Or retrieve the energy level (electric or gasoline) - fuel = vehicle.energy.level + fuel = vehicle.dashboard.fuel_level + battery = vehicle.dashboard.batter_level # Or Parking information: - latitude = vehicle.parking.latitude - - - # Pretty print the object. This will provide you with all available information. - print(json.dumps(vehicle.as_dict(), indent=3)) + latitude = vehicle.parkinglocation.latitude - - # ------------------------------- - # All data is return in an object. - # ------------------------------- - - # Returns live data from car/last time you used it. - vehicle = await client.get_vehicle_status(car) - print(vehicle.as_dict()) - - # Stats returned in a dict + # Daily stats daily_stats = await client.get_driving_statistics(vehicle.vin, interval="day") - print(daily_stats.as_list()) - - # Stats returned in json. - weekly_stats = await client.get_driving_statistics(vehicle.vin, interval="isoweek") - print(weekly_stats.as_list()) # ISO 8601 week stats iso_weekly_stats = await client.get_driving_statistics(vehicle.vin, interval="isoweek") - print(iso_weekly_stats.as_list) # Monthly stats is returned by default monthly_stats = await client.get_driving_statistics(vehicle.vin) - print(monthly_stats.as_list()) - #Get year to date stats. + # Get year to date stats. yearly_stats = await client.get_driving_statistics(vehicle.vin, interval="year") - print(yearly_stats.as_list()) loop = asyncio.get_event_loop()