diff --git a/custom_components/teslemetry/button.py b/custom_components/teslemetry/button.py index b5400bd..e17e975 100644 --- a/custom_components/teslemetry/button.py +++ b/custom_components/teslemetry/button.py @@ -70,4 +70,5 @@ def __init__( async def async_press(self) -> None: """Press the button.""" + await self.wake_up_if_asleep() await self.entity_description.func(self.api) diff --git a/custom_components/teslemetry/climate.py b/custom_components/teslemetry/climate.py index 2114e2b..2e6f078 100644 --- a/custom_components/teslemetry/climate.py +++ b/custom_components/teslemetry/climate.py @@ -26,7 +26,7 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER, data.scopes) + TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER, Scopes.VEHICLE_CMDS in data.scopes) for vehicle in data.vehicles ) @@ -48,13 +48,13 @@ def __init__( self, vehicle: TeslemetryVehicleData, side: TeslemetryClimateSide, - scopes: Scopes, + scoped: bool, ) -> None: """Initialize the climate.""" super().__init__(vehicle, side) + self.scoped = scoped - # Require VEHICLE_CMDS to make changes - if Scopes.VEHICLE_CMDS not in scopes: + if not scoped: self._attr_supported_features = ClimateEntityFeature(0) @property @@ -91,16 +91,16 @@ def preset_mode(self) -> str | None: async def async_turn_on(self) -> None: """Set the climate state to on.""" - with handle_command(): - await self.wake_up_if_asleep() - await self.api.auto_conditioning_start() + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.auto_conditioning_start() self.set(("climate_state_is_climate_on", True)) async def async_turn_off(self) -> None: """Set the climate state to off.""" - with handle_command(): - await self.wake_up_if_asleep() - await self.api.auto_conditioning_stop() + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.auto_conditioning_stop() self.set( ("climate_state_is_climate_on", False), ("climate_state_climate_keeper_mode", "off"), @@ -109,12 +109,12 @@ async def async_turn_off(self) -> None: async def async_set_temperature(self, **kwargs: Any) -> None: """Set the climate temperature.""" temp = kwargs[ATTR_TEMPERATURE] - with handle_command(): - await self.wake_up_if_asleep() - await self.api.set_temps( - driver_temp=temp, - passenger_temp=temp, - ) + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.set_temps( + driver_temp=temp, + passenger_temp=temp, + ) self.set((f"climate_state_{self.key}_setting", temp)) @@ -127,11 +127,11 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the climate preset mode.""" - with handle_command(): - await self.wake_up_if_asleep() - await self.api.set_climate_keeper_mode( - climate_keeper_mode=self._attr_preset_modes.index(preset_mode) - ) + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.set_climate_keeper_mode( + climate_keeper_mode=self._attr_preset_modes.index(preset_mode) + ) self.set( ( "climate_state_climate_keeper_mode", diff --git a/custom_components/teslemetry/cover.py b/custom_components/teslemetry/cover.py index 2efe4fd..ddc8cdf 100644 --- a/custom_components/teslemetry/cover.py +++ b/custom_components/teslemetry/cover.py @@ -26,12 +26,15 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - klass(vehicle, data.scopes) - for klass in ( - TeslemetryWindowEntity, - TeslemetryChargePortEntity, - TeslemetryFrontTrunkEntity, - TeslemetryRearTrunkEntity, + klass(vehicle, any(scope in data.scopes for scope in scopes)) + for (klass, scopes) in ( + (TeslemetryWindowEntity, [Scopes.VEHICLE_CMDS]), + ( + TeslemetryChargePortEntity, + [Scopes.VEHICLE_CMDS, Scopes.VEHICLE_CHARGING_CMDS], + ), + (TeslemetryFrontTrunkEntity, [Scopes.VEHICLE_CMDS]), + (TeslemetryRearTrunkEntity, [Scopes.VEHICLE_CMDS]), ) for vehicle in data.vehicles ) @@ -43,10 +46,11 @@ class TeslemetryWindowEntity(TeslemetryVehicleEntity, CoverEntity): _attr_device_class = CoverDeviceClass.WINDOW _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scopes]) -> None: + def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: """Initialize the sensor.""" super().__init__(vehicle, "windows") - if Scopes.VEHICLE_CMDS not in scopes: + self.scoped = scoped + if not scoped: self._attr_supported_features = CoverEntityFeature(0) @property @@ -60,7 +64,9 @@ def is_closed(self) -> bool | None: ) async def async_open_cover(self, **kwargs: Any) -> None: - """Open windows.""" + """Vent windows.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.window_control(command=WindowCommands.VENT) self.set( ("vehicle_state_fd_window", TeslemetryCoverStates.OPEN), @@ -71,6 +77,8 @@ async def async_open_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None: """Close windows.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.window_control(command=WindowCommands.CLOSE) self.set( ("vehicle_state_fd_window", TeslemetryCoverStates.CLOSED), @@ -86,12 +94,11 @@ class TeslemetryChargePortEntity(TeslemetryVehicleEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scopes]) -> None: + def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: """Initialize the sensor.""" super().__init__(vehicle, "charge_state_charge_port_door_open") - - # Require VEHICLE_CMDS to make changes - if Scopes.VEHICLE_CMDS not in scopes: + self.scoped = scoped + if not scoped: self._attr_supported_features = CoverEntityFeature(0) @property @@ -101,11 +108,15 @@ def is_closed(self) -> bool | None: async def async_open_cover(self, **kwargs: Any) -> None: """Open windows.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.charge_port_door_open() self.set((self.key, True)) async def async_close_cover(self, **kwargs: Any) -> None: """Close windows.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.charge_port_door_close() self.set((self.key, False)) @@ -116,12 +127,12 @@ class TeslemetryFrontTrunkEntity(TeslemetryVehicleEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN - def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scopes]) -> None: + def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: """Initialize the sensor.""" super().__init__(vehicle, "vehicle_state_ft") - # Require VEHICLE_CMDS to make changes - if Scopes.VEHICLE_CMDS not in scopes: + self.scoped = scoped + if not scoped: self._attr_supported_features = CoverEntityFeature(0) @property @@ -131,6 +142,8 @@ def is_closed(self) -> bool | None: async def async_open_cover(self, **kwargs: Any) -> None: """Open front trunk.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.actuate_trunk(which_trunk=Trunks.FRONT) self.set((self.key, TeslemetryCoverStates.OPEN)) @@ -141,12 +154,11 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, vehicle: TeslemetryVehicleData, scopes: list[Scopes]) -> None: + def __init__(self, vehicle: TeslemetryVehicleData, scoped) -> None: """Initialize the sensor.""" super().__init__(vehicle, "vehicle_state_rt") - - # Require VEHICLE_CMDS to make changes - if Scopes.VEHICLE_CMDS not in scopes: + self.scoped = scoped + if not scoped: self._attr_supported_features = CoverEntityFeature(0) @property @@ -157,11 +169,15 @@ def is_closed(self) -> bool | None: async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" if self.get() == TeslemetryCoverStates.CLOSED: + self.raise_for_scope() + await self.wake_up_if_asleep() self.api.actuate_trunk(Trunks.REAR) self.set((self.key, TeslemetryCoverStates.OPEN)) async def async_close_cover(self, **kwargs: Any) -> None: """Close rear trunk.""" if self.get() == TeslemetryCoverStates.OPEN: + self.raise_for_scope() + await self.wake_up_if_asleep() self.api.actuate_trunk(Trunks.REAR) self.set((self.key, TeslemetryCoverStates.CLOSED)) diff --git a/custom_components/teslemetry/lock.py b/custom_components/teslemetry/lock.py index 292ca07..db72ed8 100644 --- a/custom_components/teslemetry/lock.py +++ b/custom_components/teslemetry/lock.py @@ -1,6 +1,6 @@ """Lock platform for Teslemetry integration.""" from __future__ import annotations - +from tesla_fleet_api.const import Scopes from typing import Any from homeassistant.components.lock import LockEntity @@ -24,7 +24,11 @@ async def async_setup_entry( async_add_entities( klass(vehicle) - for klass in (TeslemetryLockEntity, TeslemetryCableLockEntity) + for klass in ( + TeslemetryLockEntity, + TeslemetryCableLockEntity, + Scopes.VEHICLE_CMDS in data.scopes, + ) for vehicle in data.vehicles ) @@ -32,12 +36,10 @@ async def async_setup_entry( class TeslemetryLockEntity(TeslemetryVehicleEntity, LockEntity): """Lock entity for Teslemetry.""" - def __init__( - self, - vehicle: TeslemetryVehicleData, - ) -> None: + def __init__(self, vehicle: TeslemetryVehicleData, scoped: bool) -> None: """Initialize the sensor.""" super().__init__(vehicle, "vehicle_state_locked") + self.scoped = scoped @property def is_locked(self) -> bool | None: @@ -46,11 +48,15 @@ def is_locked(self) -> bool | None: async def async_lock(self, **kwargs: Any) -> None: """Lock the doors.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.door_lock() self.set((self.key, True)) async def async_unlock(self, **kwargs: Any) -> None: """Unlock the doors.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.door_unlock() self.set((self.key, False)) @@ -80,5 +86,7 @@ async def async_lock(self, **kwargs: Any) -> None: async def async_unlock(self, **kwargs: Any) -> None: """Unlock charge cable lock.""" + self.raise_for_scope() + await self.wake_up_if_asleep() await self.api.charge_port_door_open() self.set((self.key, TeslemetryChargeCableLockStates.DISENGAGED)) diff --git a/custom_components/teslemetry/media_player.py b/custom_components/teslemetry/media_player.py index be02cab..7b5c0ec 100644 --- a/custom_components/teslemetry/media_player.py +++ b/custom_components/teslemetry/media_player.py @@ -1,5 +1,6 @@ """Media Player platform for Teslemetry integration.""" from __future__ import annotations +from tesla_fleet_api.const import Scopes from homeassistant.components.media_player import ( MediaPlayerDeviceClass, @@ -11,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, Scopes +from .const import DOMAIN from .entity import ( TeslemetryVehicleEntity, ) @@ -58,7 +59,12 @@ def __init__( super().__init__(vehicle, "media") self.scoped = scoped if not scoped: - _attr_supported_features = MediaPlayerEntityFeature(0) + self._attr_supported_features = MediaPlayerEntityFeature(0) + + @property + def max_volume(self) -> float: + """Return the maximum volume level.""" + return self.get("vehicle_state_media_info_audio_volume_max", MAX_VOLUME) @property def state(self) -> MediaPlayerState: @@ -68,12 +74,19 @@ def state(self) -> MediaPlayerState: MediaPlayerState.OFF, ) + @property + def volume_step(self) -> float: + """Volume step size.""" + return ( + 1.0 + / self.max_volume + / self.get("vehicle_state_media_info_audio_volume_increment", 1.0 / 3) + ) + @property def volume_level(self) -> float: """Volume level of the media player (0..1).""" - return self.get("vehicle_state_media_info_audio_volume", 0) / self.get( - "vehicle_state_media_info_audio_volume_max", MAX_VOLUME - ) + return self.get("vehicle_state_media_info_audio_volume", 0) / self.max_volume @property def media_duration(self) -> int | None: @@ -119,9 +132,30 @@ async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.api.adjust_volume( - int( - volume - * self.get("vehicle_state_media_info_audio_volume_max", MAX_VOLUME) - ) - ) + await self.api.adjust_volume(int(volume * self.max_volume)) + + async def async_media_play(self) -> None: + """Send play command.""" + if self.state != MediaPlayerState.PLAYING: + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.media_toggle_playback() + + async def async_media_pause(self) -> None: + """Send pause command.""" + if self.state == MediaPlayerState.PLAYING: + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.media_toggle_playback() + + async def async_media_next_track(self) -> None: + """Send next track command.""" + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.media_next_track() + + async def async_media_previous_track(self) -> None: + """Send previous track command.""" + self.raise_for_scope() + await self.wake_up_if_asleep() + await self.api.media_previous_track() diff --git a/custom_components/teslemetry/select.py b/custom_components/teslemetry/select.py index 4030c70..cc97776 100644 --- a/custom_components/teslemetry/select.py +++ b/custom_components/teslemetry/select.py @@ -1,13 +1,14 @@ """Select platform for Teslemetry integration.""" from __future__ import annotations +from tesla_fleet_api.const import Scopes from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, TeslemetrySeatHeaterOptions, Scopes +from .const import DOMAIN, TeslemetrySeatHeaterOptions from .entity import ( TeslemetryVehicleEntity, ) diff --git a/custom_components/teslemetry/update.py b/custom_components/teslemetry/update.py index 1d3924e..27b3844 100644 --- a/custom_components/teslemetry/update.py +++ b/custom_components/teslemetry/update.py @@ -49,7 +49,7 @@ def available(self) -> bool: @property def supported_features(self) -> UpdateEntityFeature: """Flag supported features.""" - if self.can_update and self.get("vehicle_state_software_update_status") in ( + if self.scoped and self.get("vehicle_state_software_update_status") in ( TeslemetryUpdateStatus.AVAILABLE, TeslemetryUpdateStatus.SCHEDULED, ):