From d864b0fea51a8a0376ba97c24e9159f5df5aad11 Mon Sep 17 00:00:00 2001 From: Brett Date: Sat, 14 Dec 2024 19:02:26 +1000 Subject: [PATCH] Progress --- custom_components/teslemetry/__init__.py | 2 +- custom_components/teslemetry/climate.py | 8 +- custom_components/teslemetry/cover.py | 154 ++++++++++++++++------- custom_components/teslemetry/entity.py | 22 +++- 4 files changed, 132 insertions(+), 54 deletions(-) diff --git a/custom_components/teslemetry/__init__.py b/custom_components/teslemetry/__init__.py index b938e30..d9ea91d 100644 --- a/custom_components/teslemetry/__init__.py +++ b/custom_components/teslemetry/__init__.py @@ -38,7 +38,7 @@ PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.BUTTON, - #Platform.COVER, + Platform.COVER, Platform.CLIMATE, #Platform.DEVICE_TRACKER, #Platform.LOCK, diff --git a/custom_components/teslemetry/climate.py b/custom_components/teslemetry/climate.py index 2809706..28d1958 100644 --- a/custom_components/teslemetry/climate.py +++ b/custom_components/teslemetry/climate.py @@ -38,7 +38,7 @@ async def async_setup_entry( TeslemetryPollingClimateEntity( vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes ) - if True or vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" + if vehicle.api.pre2021 or vehicle.firmware < "2030.44.25" # Insufficent streaming data for climate else TeslemetryStreamingClimateEntity( vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes ) @@ -357,7 +357,7 @@ class TeslemetryPollingCabinOverheatProtectionEntity(TeslemetryVehicleEntity, Te def __init__( self, data: TeslemetryVehicleData, - scopes: [Scope], + scopes: list[Scope], ) -> None: """Initialize the climate.""" @@ -400,7 +400,7 @@ class TeslemetryStreamingCabinOverheatProtectionEntity(TeslemetryVehicleComplexS def __init__( self, data: TeslemetryVehicleData, - scopes: [Scope], + scopes: list[Scope], ) -> None: """Initialize the climate.""" @@ -418,7 +418,7 @@ def __init__( self._attr_supported_features = ( ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF ) - if self.get("vehicle_config_cop_user_set_temp_supported"): + if data.coordinator.data.get("vehicle_config_cop_user_set_temp_supported"): self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE # Scopes diff --git a/custom_components/teslemetry/cover.py b/custom_components/teslemetry/cover.py index 4728087..8e818a8 100644 --- a/custom_components/teslemetry/cover.py +++ b/custom_components/teslemetry/cover.py @@ -2,11 +2,9 @@ from __future__ import annotations +from itertools import chain from typing import Any -from tesla_fleet_api.const import WindowCommand, Trunk, Scope, SunRoofCommand -from teslemetry_stream import Signal - from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, @@ -16,10 +14,16 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from tesla_fleet_api.const import Scope, SunRoofCommand, Trunk, WindowCommand +from teslemetry_stream import Signal -from .entity import TeslemetryVehicleEntity, TeslemetryVehicleStreamEntity, TeslemetryVehicleComplexStreamEntity -from .models import TeslemetryVehicleData +from .entity import ( + TeslemetryVehicleComplexStreamEntity, + TeslemetryVehicleEntity, + TeslemetryVehicleStreamEntity, +) from .helpers import auto_type +from .models import TeslemetryVehicleData CLOSED = 0 OPEN = 1 @@ -29,18 +33,38 @@ async def async_setup_entry( ) -> None: """Set up the Teslemetry sensor platform from a config entry.""" - async_add_entities( - klass(vehicle, entry.runtime_data.scopes) - for vehicle in entry.runtime_data.vehicles - for (klass) in ( - TeslemetryPollingWindowEntity if vehicle.pre2021 else TeslemetryStreamingWindowEntity, - TeslemetryChargePortEntity, - TeslemetryFrontTrunkEntity, - TeslemetryRearTrunkEntity, - TeslemetrySunroofEntity, + chain( + ( + TeslemetryPollingWindowEntity(vehicle, entry.runtime_data.scopes) + if vehicle.api.pre2021 or vehicle.firmware < "2024.26" + else TeslemetryStreamingWindowEntity(vehicle, entry.runtime_data.scopes) + for vehicle in entry.runtime_data.vehicles + ), + ( + TeslemetryPollingChargePortEntity(vehicle, entry.runtime_data.scopes) + if vehicle.api.pre2021 or vehicle.firmware < "2024.26" + else TeslemetryStreamingChargePortEntity(vehicle, entry.runtime_data.scopes) + for vehicle in entry.runtime_data.vehicles + ), + ( + TeslemetryPollingFrontTrunkEntity(vehicle, entry.runtime_data.scopes) + if vehicle.api.pre2021 or vehicle.firmware < "2024.26" + else TeslemetryStreamingFrontTrunkEntity(vehicle, entry.runtime_data.scopes) + for vehicle in entry.runtime_data.vehicles + ), + ( + TeslemetryPollingRearTrunkEntity(vehicle, entry.runtime_data.scopes) + if vehicle.api.pre2021 or vehicle.firmware < "2024.26" + else TeslemetryStreamingRearTrunkEntity(vehicle, entry.runtime_data.scopes) + for vehicle in entry.runtime_data.vehicles + ), + ( + TeslemetrySunroofEntity(vehicle, entry.runtime_data.scopes) + for vehicle in entry.runtime_data.vehicles + if vehicle.coordinator.data.get("vehicle_config_sun_roof_installed") + ) ) - ) @@ -131,9 +155,9 @@ def _async_data_from_stream(self, data) -> None: if value := data.get(Signal.RP_WINDOW): self.rp = value == "WindowStateOpen" - if True in (fd, fp, rd, rp): + if True in (self.fd, self.fp, self.rd, self.rp): self._attr_is_closed = False - elif None in (fd, fp, rd, rp): + elif None in (self.fd, self.fp, self.rd, self.rp): self._attr_is_closed = None else: self._attr_is_closed = True @@ -160,8 +184,9 @@ async def async_close_cover(self, **kwargs: Any) -> None: self._attr_is_closed = True self.async_write_ha_state() -class TeslemetryPollingChargePortLatch(TeslemetryVehicleEntity, TeslemetryChargePortEntity): +class TeslemetryPollingChargePortEntity(TeslemetryVehicleEntity, TeslemetryChargePortEntity): """Polling cover entity for the charge port.""" + def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: """Initialize the sensor.""" self.scoped = any( @@ -176,11 +201,11 @@ def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: "charge_state_charge_port_door_open", ) - def _async_value_from_stream(self, value) -> None: - """Update the value of the entity.""" - self._attr_is_closed = not auto_type(value) + def _async_update_attrs(self) -> None: + """Update the entity attributes.""" + self._attr_is_closed = self.exactly(False) -class TeslemetryStreamingChargePortLatch(TeslemetryVehicleStreamEntity, TeslemetryChargePortEntity): +class TeslemetryStreamingChargePortEntity(TeslemetryVehicleStreamEntity, TeslemetryChargePortEntity): """Streaming cover entity for the charge port.""" def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: @@ -200,26 +225,15 @@ def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: def _async_value_from_stream(self, value) -> None: """Update the value of the entity.""" - self._attr_is_closed = not auto_type(value) + self._attr_is_closed = value == "ChargePortLatchDisengaged" -class TeslemetryFrontTrunkEntity(TeslemetryVehicleEntity, CoverRestoreEntity): +class TeslemetryFrontTrunkEntity(CoverEntity): """Cover entity for the front trunk.""" _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN - def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: - """Initialize the sensor.""" - self.scoped = Scope.VEHICLE_CMDS in scopes - if not self.scoped: - self._attr_supported_features = CoverEntityFeature(0) - super().__init__(vehicle, "vehicle_state_ft") - - def _async_update_attrs(self) -> None: - """Update the entity attributes.""" - self._attr_is_closed = self.exactly(CLOSED) - async def async_open_cover(self, **kwargs: Any) -> None: """Open front trunk.""" self.raise_for_scope(Scope.VEHICLE_CMDS) @@ -230,24 +244,44 @@ async def async_open_cover(self, **kwargs: Any) -> None: # In the future this could be extended to add aftermarket close support through a option flow - -class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverRestoreEntity): - """Cover entity for the rear trunk.""" - - _attr_device_class = CoverDeviceClass.DOOR - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE +class TeslemetryPollingFrontTrunkEntity(TeslemetryVehicleEntity, TeslemetryFrontTrunkEntity): + """Polling cover entity for the front trunk.""" def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: """Initialize the sensor.""" self.scoped = Scope.VEHICLE_CMDS in scopes if not self.scoped: self._attr_supported_features = CoverEntityFeature(0) - super().__init__(vehicle, "vehicle_state_rt") + super().__init__(vehicle, "vehicle_state_ft") def _async_update_attrs(self) -> None: """Update the entity attributes.""" self._attr_is_closed = self.exactly(CLOSED) +class TeslemetryStreamingFrontTrunkEntity(TeslemetryVehicleStreamEntity, TeslemetryFrontTrunkEntity): + """Streaming cover entity for the front trunk.""" + + def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: + """Initialize the sensor.""" + self.scoped = Scope.VEHICLE_CMDS in scopes + if not self.scoped: + self._attr_supported_features = CoverEntityFeature(0) + super().__init__(vehicle, "vehicle_state_ft", Signal.DOOR_STATE) + + def _async_value_from_stream(self, value) -> None: + """Update the entity attributes.""" + value = value.get("TrunkFront") + if value is None: + self._attr_is_closed = None + else: + self._attr_is_closed = not value + +class TeslemetryRearTrunkEntity(CoverEntity): + """Cover entity for the rear trunk.""" + + _attr_device_class = CoverDeviceClass.DOOR + _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" if self.is_closed is not False: @@ -266,7 +300,41 @@ async def async_close_cover(self, **kwargs: Any) -> None: self._attr_is_closed = True self.async_write_ha_state() -class TeslemetrySunroofEntity(TeslemetryVehicleEntity, CoverRestoreEntity): + +class TeslemetryPollingRearTrunkEntity(TeslemetryVehicleEntity, TeslemetryRearTrunkEntity): + """Polling Cover entity for the rear trunk.""" + + def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: + """Initialize the sensor.""" + self.scoped = Scope.VEHICLE_CMDS in scopes + if not self.scoped: + self._attr_supported_features = CoverEntityFeature(0) + super().__init__(vehicle, "vehicle_state_rt") + + def _async_update_attrs(self) -> None: + """Update the entity attributes.""" + self._attr_is_closed = self.exactly(CLOSED) + + +class TeslemetryStreamingRearTrunkEntity(TeslemetryVehicleStreamEntity, TeslemetryRearTrunkEntity): + """Polling Cover entity for the rear trunk.""" + + def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scope]) -> None: + """Initialize the sensor.""" + self.scoped = Scope.VEHICLE_CMDS in scopes + if not self.scoped: + self._attr_supported_features = CoverEntityFeature(0) + super().__init__(vehicle, "vehicle_state_rt", Signal.DOOR_STATE) + + def _async_value_from_stream(self, value) -> None: + """Update the entity attributes.""" + value = value.get("TrunkRear") + if value is None: + self._attr_is_closed = None + else: + self._attr_is_closed = not value + +class TeslemetrySunroofEntity(TeslemetryVehicleEntity, TeslemetryWindowEntity): """Cover entity for the sunroof.""" _attr_device_class = CoverDeviceClass.WINDOW diff --git a/custom_components/teslemetry/entity.py b/custom_components/teslemetry/entity.py index 4198afb..fcb7e8f 100644 --- a/custom_components/teslemetry/entity.py +++ b/custom_components/teslemetry/entity.py @@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import DOMAIN, LOGGER from .coordinator import ( TeslemetryEnergySiteInfoCoordinator, @@ -72,7 +72,12 @@ async def async_added_to_hass(self) -> None: def _handle_stream_update(self, data: dict[str, Any]) -> None: """Handle updated data from the stream.""" - self._async_value_from_stream(data["data"][self.streaming_key]) + + try: + self._async_value_from_stream(data["data"][self.streaming_key]) + except Exception as e: + LOGGER.error("Error updating %s: %s", self._attr_translation_key, e) + LOGGER.debug(data) self.async_write_ha_state() def _async_value_from_stream(self, value: Any) -> None: @@ -114,9 +119,14 @@ async def async_added_to_hass(self) -> None: def _handle_stream_update(self, data: dict[str, Any]) -> None: """Handle updated data from the stream.""" - data = {key: data["data"][key] for key in self.streaming_keys if key in data["data"]} - self._async_data_from_stream(data["data"]) - self.async_write_ha_state() + #data = {key: data["data"][key] for key in self.streaming_keys if key in data["data"]} + if any(key in data["data"] for key in self.streaming_keys): + try: + self._async_data_from_stream(data["data"]) + except Exception as e: + LOGGER.error("Error updating %s: %s", self._attr_translation_key, e) + LOGGER.debug(data) + self.async_write_ha_state() def _async_data_from_stream(self, data: Any) -> None: """Update the entity with the latest value from the stream.""" @@ -168,7 +178,7 @@ def get_number(self, key: str, default: float) -> float: return default def exactly(self, value: Any, key: str | None = None) -> bool | None: - """Return if a key exactly matches the valug but retain None.""" + """Return if a key exactly matches the value but retain None.""" key = key or self.key if value is None: return self.get(key, False) is None