diff --git a/custom_components/teslemetry/__init__.py b/custom_components/teslemetry/__init__.py index 2c6eb5a..18340bf 100644 --- a/custom_components/teslemetry/__init__.py +++ b/custom_components/teslemetry/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for product in products: if "vin" in product and Scopes.VEHICLE_DEVICE_DATA in scopes: # Remove the protobuff 'cached_data' that we do not use to save memory - product.pop('cached_data', None) + product.pop("cached_data", None) vin = product["vin"] api = VehicleSpecific(teslemetry.vehicle, vin) coordinator = TeslemetryVehicleDataCoordinator(hass, api, product) diff --git a/custom_components/teslemetry/binary_sensor.py b/custom_components/teslemetry/binary_sensor.py index 9ef8c84..7e60b5f 100644 --- a/custom_components/teslemetry/binary_sensor.py +++ b/custom_components/teslemetry/binary_sensor.py @@ -1,6 +1,7 @@ """Binary Sensor platform for Teslemetry integration.""" from __future__ import annotations +from itertools import chain from collections.abc import Callable from dataclasses import dataclass @@ -162,21 +163,24 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - TeslemetryVehicleBinarySensorEntity(vehicle, description) - for vehicle in data.vehicles - for description in VEHICLE_DESCRIPTIONS - ) - - async_add_entities( - TeslemetryEnergyLiveBinarySensorEntity(energysite, description) - for energysite in data.energysites - for description in ENERGY_LIVE_DESCRIPTIONS - ) - - async_add_entities( - TeslemetryEnergyInfoBinarySensorEntity(energysite, description) - for energysite in data.energysites - for description in ENERGY_INFO_DESCRIPTIONS + chain( + # Vehicles + TeslemetryVehicleBinarySensorEntity(vehicle, description) + for vehicle in data.vehicles + for description in VEHICLE_DESCRIPTIONS + ), + ( + # Energy Site Live + TeslemetryEnergyLiveBinarySensorEntity(energysite, description) + for energysite in data.energysites + for description in ENERGY_LIVE_DESCRIPTIONS + ), + ( + # Energy Site Info + TeslemetryEnergyInfoBinarySensorEntity(energysite, description) + for energysite in data.energysites + for description in ENERGY_INFO_DESCRIPTIONS + ), ) diff --git a/custom_components/teslemetry/button.py b/custom_components/teslemetry/button.py index 74fc890..8b37c74 100644 --- a/custom_components/teslemetry/button.py +++ b/custom_components/teslemetry/button.py @@ -26,7 +26,7 @@ class TeslemetryButtonEntityDescription(ButtonEntityDescription): DESCRIPTIONS: tuple[TeslemetryButtonEntityDescription, ...] = ( - TeslemetryButtonEntityDescription(key="wake"), # Every button also runs wakeup + TeslemetryButtonEntityDescription(key="wake"), # Every button runs wakeup TeslemetryButtonEntityDescription( key="flash_lights", func=lambda api: api.flash_lights() ), @@ -68,11 +68,6 @@ def __init__( super().__init__(data, description.key) self.entity_description = description - @property - def avaliable(self) -> bool: - """Return if the cover is available.""" - return True - async def async_press(self) -> None: """Press the button.""" await self.wake_up_if_asleep() diff --git a/custom_components/teslemetry/coordinator.py b/custom_components/teslemetry/coordinator.py index 8141626..93d2461 100644 --- a/custom_components/teslemetry/coordinator.py +++ b/custom_components/teslemetry/coordinator.py @@ -16,17 +16,18 @@ ENERGY_INFO_INTERVAL = timedelta(seconds=300) ENDPOINTS = [ - VehicleDataEndpoints.CHARGE_STATE, - VehicleDataEndpoints.CLIMATE_STATE, - #VehicleDataEndpoints.CLOSURES_STATE, - VehicleDataEndpoints.DRIVE_STATE, - #VehicleDataEndpoints.GUI_SETTINGS, - VehicleDataEndpoints.LOCATION_DATA, - #VehicleDataEndpoints.VEHICLE_CONFIG, - VehicleDataEndpoints.VEHICLE_STATE, - ] + VehicleDataEndpoints.CHARGE_STATE, + VehicleDataEndpoints.CLIMATE_STATE, + # VehicleDataEndpoints.CLOSURES_STATE, + VehicleDataEndpoints.DRIVE_STATE, + # VehicleDataEndpoints.GUI_SETTINGS, + VehicleDataEndpoints.LOCATION_DATA, + # VehicleDataEndpoints.VEHICLE_CONFIG, + VehicleDataEndpoints.VEHICLE_STATE, +] # + def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]: """Flatten the data structure.""" result = {} @@ -68,7 +69,7 @@ async def _async_update_data(self) -> dict[str, Any]: except TeslaFleetError as e: raise UpdateFailed(e.message) from e - return {**self.data , **flatten(data["response"])} + return {**self.data, **flatten(data["response"])} class TeslemetryEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]): diff --git a/custom_components/teslemetry/cover.py b/custom_components/teslemetry/cover.py index da27a17..1142499 100644 --- a/custom_components/teslemetry/cover.py +++ b/custom_components/teslemetry/cover.py @@ -53,20 +53,19 @@ def __init__(self, data: TeslemetryVehicleData, scoped) -> None: if not scoped: self._attr_supported_features = CoverEntityFeature(0) - @property - def avaliable(self) -> bool: - """Return if the cover is available.""" - return True - @property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" - return ( - self.get("vehicle_state_fd_window") == TeslemetryCoverStates.CLOSED - and self.get("vehicle_state_fp_window") == TeslemetryCoverStates.CLOSED - and self.get("vehicle_state_rd_window") == TeslemetryCoverStates.CLOSED - and self.get("vehicle_state_rp_window") == TeslemetryCoverStates.CLOSED - ) + fd = self.get("vehicle_state_fd_window") + fp = self.get("vehicle_state_fp_window") + rd = self.get("vehicle_state_rd_window") + rp = self.get("vehicle_state_rp_window") + + if fd or fp or rd or rp == TeslemetryCoverStates.OPEN: + return False + if fd and fp and rd and rp == TeslemetryCoverStates.CLOSED: + return True + return None async def async_open_cover(self, **kwargs: Any) -> None: """Vent windows.""" @@ -106,11 +105,6 @@ def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: if not scoped: self._attr_supported_features = CoverEntityFeature(0) - @property - def avaliable(self) -> bool: - """Return if the cover is available.""" - return True - @property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" @@ -145,15 +139,15 @@ def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: if not scoped: self._attr_supported_features = CoverEntityFeature(0) - @property - def avaliable(self) -> bool: - """Return if the cover is available.""" - return True - @property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" - return self.get() == TeslemetryCoverStates.CLOSED + value = self.get() + if value == TeslemetryCoverStates.CLOSED: + return True + if value == TeslemetryCoverStates.OPEN: + return False + return None async def async_open_cover(self, **kwargs: Any) -> None: """Open front trunk.""" @@ -176,15 +170,15 @@ def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: if not scoped: self._attr_supported_features = CoverEntityFeature(0) - @property - def avaliable(self) -> bool: - """Return if the cover is available.""" - return True - @property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" - return self.get() == TeslemetryCoverStates.CLOSED + value = self.get() + if value == TeslemetryCoverStates.CLOSED: + return True + if value == TeslemetryCoverStates.OPEN: + return False + return None async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" diff --git a/custom_components/teslemetry/device_tracker.py b/custom_components/teslemetry/device_tracker.py index 45ef612..9298e04 100644 --- a/custom_components/teslemetry/device_tracker.py +++ b/custom_components/teslemetry/device_tracker.py @@ -87,3 +87,8 @@ def longitude(self) -> float | None: def latitude(self) -> float | None: """Return the latitude of the device tracker.""" return self.get("drive_state_active_route_latitude") + + @property + def location_name(self) -> str | None: + """Return the location of the device tracker.""" + return self.get("drive_state_active_route_destination") diff --git a/custom_components/teslemetry/entity.py b/custom_components/teslemetry/entity.py index 3265868..1095d9a 100644 --- a/custom_components/teslemetry/entity.py +++ b/custom_components/teslemetry/entity.py @@ -35,7 +35,7 @@ def __init__( | TeslemetryEnergySiteLiveCoordinator | TeslemetryEnergySiteInfoCoordinator, api: VehicleSpecific | EnergySpecific, - key:str + key: str, ) -> None: """Initialize common aspects of a Teslemetry entity.""" super().__init__(coordinator) @@ -43,13 +43,6 @@ def __init__( self.key = key self._attr_translation_key = key - @property - def available(self) -> bool: - """Return if sensor is available.""" - return ( - self.coordinator.last_update_success and self.key in self.coordinator.data - ) - def get(self, key: str | None = None, default: Any | None = None) -> Any: """Return a specific value from coordinator data.""" return self.coordinator.data.get(key or self.key, default) @@ -60,6 +53,10 @@ def set(self, *args: Any) -> None: self.coordinator.data[key] = value self.async_write_ha_state() + def has(self, key: str) -> bool: + """Return True if a specific value is in coordinator data.""" + return key in self.coordinator.data + def raise_for_scope(self): """Raise an error if a scope is not available.""" if not self.scoped: diff --git a/custom_components/teslemetry/lock.py b/custom_components/teslemetry/lock.py index 9b58aa9..378dbe9 100644 --- a/custom_components/teslemetry/lock.py +++ b/custom_components/teslemetry/lock.py @@ -26,7 +26,7 @@ async def async_setup_entry( async_add_entities( klass(vehicle, Scopes.VEHICLE_CMDS in data.scopes) for klass in ( - TeslemetryLockEntity, + TeslemetryVehicleLockEntity, TeslemetryCableLockEntity, TeslemetrySpeedLimitEntity, ) @@ -34,7 +34,7 @@ async def async_setup_entry( ) -class TeslemetryLockEntity(TeslemetryVehicleEntity, LockEntity): +class TeslemetryVehicleLockEntity(TeslemetryVehicleEntity, LockEntity): """Lock entity for Teslemetry.""" def __init__(self, data: TeslemetryVehicleData, scoped: bool) -> None: diff --git a/custom_components/teslemetry/number.py b/custom_components/teslemetry/number.py index ad0c1f4..4a9ccf0 100644 --- a/custom_components/teslemetry/number.py +++ b/custom_components/teslemetry/number.py @@ -1,6 +1,7 @@ """Number platform for Teslemetry integration.""" from __future__ import annotations +from itertools import chain from collections.abc import Callable from dataclasses import dataclass @@ -110,27 +111,28 @@ async def async_setup_entry( """Set up the Teslemetry sensor platform from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - # Add vehicle entities async_add_entities( - TeslemetryVehicleNumberEntity( - vehicle, - description, - any(scope in data.scopes for scope in description.scopes), - ) - for vehicle in data.vehicles - for description in VEHICLE_DESCRIPTIONS - ) - - # Add energy site entities - async_add_entities( - TeslemetryEnergyInfoNumberSensorEntity( - energysite, - description, - any(scope in data.scopes for scope in description.scopes), - ) - for energysite in data.energysites - for description in ENERGY_INFO_DESCRIPTIONS - if description.key in energysite.info_coordinator.data + chain( + # Add vehicle entities + TeslemetryVehicleNumberEntity( + vehicle, + description, + any(scope in data.scopes for scope in description.scopes), + ) + for vehicle in data.vehicles + for description in VEHICLE_DESCRIPTIONS + ), + ( + # Add energy site entities + TeslemetryEnergyInfoNumberSensorEntity( + energysite, + description, + any(scope in data.scopes for scope in description.scopes), + ) + for energysite in data.energysites + for description in ENERGY_INFO_DESCRIPTIONS + if description.key in energysite.info_coordinator.data + ), ) diff --git a/custom_components/teslemetry/select.py b/custom_components/teslemetry/select.py index 7bebc24..87c6b35 100644 --- a/custom_components/teslemetry/select.py +++ b/custom_components/teslemetry/select.py @@ -61,22 +61,52 @@ async def async_setup_entry( entities = [] for vehicle in data.vehicles: scoped = Scopes.VEHICLE_CMDS in data.scopes - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_left", scoped)) - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_right", scoped)) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_left", scoped + ) + ) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_right", scoped + ) + ) if vehicle.coordinator.data.get("vehicle_config_rear_seat_heaters"): - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_rear_left", scoped)) - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_rear_center", scoped)) - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_rear_right", scoped)) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_rear_left", scoped + ) + ) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_rear_center", scoped + ) + ) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_rear_right", scoped + ) + ) if vehicle.coordinator.data.get("vehicle_config_third_row_seats") != "None": - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_third_row_left", scoped)) - entities.append(TeslemetrySeatHeaterSelectEntity(vehicle, "climate_state_seat_heater_third_row_right", scoped)) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_third_row_left", scoped + ) + ) + entities.append( + TeslemetrySeatHeaterSelectEntity( + vehicle, "climate_state_seat_heater_third_row_right", scoped + ) + ) for energysite in data.energysites: for description in ENERGY_INFO_DESCRIPTIONS: if description.key in energysite.info_coordinator.data: - entities.append(TeslemetryEnergySiteSelectEntity( - energysite, description, Scopes.ENERGY_CMDS in data.scopes - )) + entities.append( + TeslemetryEnergySiteSelectEntity( + energysite, description, Scopes.ENERGY_CMDS in data.scopes + ) + ) async_add_entities(entities) diff --git a/custom_components/teslemetry/sensor.py b/custom_components/teslemetry/sensor.py index bf3113a..5a017fb 100644 --- a/custom_components/teslemetry/sensor.py +++ b/custom_components/teslemetry/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for Teslemetry integration.""" from __future__ import annotations +from itertools import chain from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -154,7 +155,7 @@ class TeslemetrySensorEntityDescription(SensorEntityDescription): key="vehicle_state_tpms_pressure_fl", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, - #suggested_unit_of_measurement=UnitOfPressure.PSI, + # suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, @@ -163,7 +164,7 @@ class TeslemetrySensorEntityDescription(SensorEntityDescription): key="vehicle_state_tpms_pressure_fr", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, - #suggested_unit_of_measurement=UnitOfPressure.PSI, + # suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, @@ -172,7 +173,7 @@ class TeslemetrySensorEntityDescription(SensorEntityDescription): key="vehicle_state_tpms_pressure_rl", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, - #suggested_unit_of_measurement=UnitOfPressure.PSI, + # suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, @@ -181,7 +182,7 @@ class TeslemetrySensorEntityDescription(SensorEntityDescription): key="vehicle_state_tpms_pressure_rr", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, - #suggested_unit_of_measurement=UnitOfPressure.PSI, + # suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, @@ -243,10 +244,6 @@ class TeslemetrySensorEntityDescription(SensorEntityDescription): timedelta(seconds=30), ), ), - TeslemetrySensorEntityDescription( - key="drive_state_active_route_destination", - entity_category=EntityCategory.DIAGNOSTIC, - ), ) ENERGY_LIVE_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( @@ -369,39 +366,33 @@ async def async_setup_entry( """Set up the Teslemetry sensor platform from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - # Add vehicles async_add_entities( - TeslemetryVehicleSensorEntity(vehicle, description) - for vehicle in data.vehicles - for description in VEHICLE_DESCRIPTIONS - ) - - # Add energy site live - async_add_entities( - TeslemetryEnergyLiveSensorEntity(energysite, description) - for energysite in data.energysites - for description in ENERGY_LIVE_DESCRIPTIONS - if description.key in energysite.live_coordinator.data - ) - - # Add wall connectors - async_add_entities( - TeslemetryWallConnectorSensorEntity(energysite, din, description) - for energysite in data.energysites - for din in energysite.live_coordinator.data.get("wall_connectors", {}) - for description in WALL_CONNECTOR_DESCRIPTIONS - ) - - # Add energy site info - async_add_entities( - TeslemetryEnergyInfoSensorEntity(energysite, description) - for energysite in data.energysites - for description in ENERGY_INFO_DESCRIPTIONS - if description.key in energysite.info_coordinator.data + chain( + # Add vehicles + TeslemetryVehicleSensorEntity(vehicle, description) + for vehicle in data.vehicles + for description in VEHICLE_DESCRIPTIONS + ), + ( # Add energy site live + TeslemetryEnergyLiveSensorEntity(energysite, description) + for energysite in data.energysites + for description in ENERGY_LIVE_DESCRIPTIONS + if description.key in energysite.live_coordinator.data + )( # Add wall connectors + TeslemetryWallConnectorSensorEntity(energysite, din, description) + for energysite in data.energysites + for din in energysite.live_coordinator.data.get("wall_connectors", {}) + for description in WALL_CONNECTOR_DESCRIPTIONS + )( # Add energy site info + TeslemetryEnergyInfoSensorEntity(energysite, description) + for energysite in data.energysites + for description in ENERGY_INFO_DESCRIPTIONS + if description.key in energysite.info_coordinator.data + ), ) -class TeslemetrySensorEntity: +class TeslemetrySensorEntity(SensorEntity): """Base class for all Teslemetry sensors.""" @@ -421,8 +412,12 @@ def __init__( @property def available(self) -> bool: - """Return if sensor is available.""" - return super().available and self.entity_description.available_fn(self.get()) + """Return if sensor entity is available.""" + return ( + super().available + and self.has() + and self.entity_description.available_fn(self.get()) + ) @property def native_value(self) -> StateType | datetime: @@ -444,6 +439,11 @@ def __init__( super().__init__(data, description.key) self.entity_description = description + @property + def available(self) -> bool: + """Return if sensor entity is available.""" + return super().available and self.has() + @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" @@ -469,6 +469,11 @@ def __init__( ) self.entity_description = description + @property + def available(self) -> bool: + """Return if sensor entity is available.""" + return super().available and self.has() + @property def native_value(self) -> StateType: """Return the state of the sensor.""" @@ -494,6 +499,11 @@ def __init__( super().__init__(data, description.key) self.entity_description = description + @property + def available(self) -> bool: + """Return if sensor entity is available.""" + return super().available and self.has() + @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" diff --git a/custom_components/teslemetry/switch.py b/custom_components/teslemetry/switch.py index e743285..c75155a 100644 --- a/custom_components/teslemetry/switch.py +++ b/custom_components/teslemetry/switch.py @@ -152,12 +152,10 @@ class TeslemetrySwitchEntity(SwitchEntity): @property def is_on(self) -> bool: """Return the state of the Switch.""" - return self.get() - - @property - def is_on(self) -> bool: - """Return the state of the Switch.""" - return self.get() + value = self.get() + if value is None: + return None + return value async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the Switch.""" diff --git a/custom_components/teslemetry/update.py b/custom_components/teslemetry/update.py index 0318804..a294db8 100644 --- a/custom_components/teslemetry/update.py +++ b/custom_components/teslemetry/update.py @@ -41,6 +41,11 @@ def __init__( super().__init__(data, "vehicle_state_software_update_status") self.scoped = scoped + @property + def available(self) -> bool: + """Return if update entity is available.""" + return super().available and self.has() + @property def supported_features(self) -> UpdateEntityFeature: """Flag supported features."""