Refactor helpers and bump Teslemetry (#119246)

This commit is contained in:
Brett Adams 2024-06-11 05:12:09 +10:00 committed by GitHub
parent 04c8a5574a
commit 4a9ebd9af1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 126 additions and 96 deletions

View File

@ -102,6 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
manufacturer="Tesla",
configuration_url="https://teslemetry.com/console",
name=product.get("site_name", "Energy Site"),
serial_number=str(site_id),
)
energysites.append(

View File

@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .entity import TeslemetryVehicleEntity
from .helpers import handle_vehicle_command
from .models import TeslemetryVehicleData
@ -84,4 +85,4 @@ class TeslemetryButtonEntity(TeslemetryVehicleEntity, ButtonEntity):
"""Press the button."""
await self.wake_up_if_asleep()
if self.entity_description.func:
await self.handle_command(self.entity_description.func(self))
await handle_vehicle_command(self.entity_description.func(self))

View File

@ -26,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .const import DOMAIN, TeslemetryClimateSide
from .entity import TeslemetryVehicleEntity
from .helpers import handle_vehicle_command
from .models import TeslemetryVehicleData
DEFAULT_MIN_TEMP = 15
@ -114,7 +115,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.auto_conditioning_start())
await handle_vehicle_command(self.api.auto_conditioning_start())
self._attr_hvac_mode = HVACMode.HEAT_COOL
self.async_write_ha_state()
@ -124,7 +125,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.auto_conditioning_stop())
await handle_vehicle_command(self.api.auto_conditioning_stop())
self._attr_hvac_mode = HVACMode.OFF
self._attr_preset_mode = self._attr_preset_modes[0]
@ -135,7 +136,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
if temp := kwargs.get(ATTR_TEMPERATURE):
await self.wake_up_if_asleep()
await self.handle_command(
await handle_vehicle_command(
self.api.set_temps(
driver_temp=temp,
passenger_temp=temp,
@ -159,7 +160,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the climate preset mode."""
await self.wake_up_if_asleep()
await self.handle_command(
await handle_vehicle_command(
self.api.set_climate_keeper_mode(
climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
)
@ -261,7 +262,7 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEn
)
await self.wake_up_if_asleep()
await self.handle_command(self.api.set_cop_temp(cop_mode))
await handle_vehicle_command(self.api.set_cop_temp(cop_mode))
self._attr_target_temperature = temp
if mode := kwargs.get(ATTR_HVAC_MODE):
@ -271,15 +272,15 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEn
async def _async_set_cop(self, hvac_mode: HVACMode) -> None:
if hvac_mode == HVACMode.OFF:
await self.handle_command(
await handle_vehicle_command(
self.api.set_cabin_overheat_protection(on=False, fan_only=False)
)
elif hvac_mode == HVACMode.COOL:
await self.handle_command(
await handle_vehicle_command(
self.api.set_cabin_overheat_protection(on=True, fan_only=False)
)
elif hvac_mode == HVACMode.FAN_ONLY:
await self.handle_command(
await handle_vehicle_command(
self.api.set_cabin_overheat_protection(on=True, fan_only=True)
)

View File

@ -17,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .entity import TeslemetryVehicleEntity
from .helpers import handle_vehicle_command
from .models import TeslemetryVehicleData
OPEN = 1
@ -88,7 +89,9 @@ class TeslemetryWindowEntity(TeslemetryVehicleEntity, CoverEntity):
"""Vent windows."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.window_control(command=WindowCommand.VENT))
await handle_vehicle_command(
self.api.window_control(command=WindowCommand.VENT)
)
self._attr_is_closed = False
self.async_write_ha_state()
@ -96,7 +99,9 @@ class TeslemetryWindowEntity(TeslemetryVehicleEntity, CoverEntity):
"""Close windows."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.window_control(command=WindowCommand.CLOSE))
await handle_vehicle_command(
self.api.window_control(command=WindowCommand.CLOSE)
)
self._attr_is_closed = True
self.async_write_ha_state()
@ -127,7 +132,7 @@ class TeslemetryChargePortEntity(TeslemetryVehicleEntity, CoverEntity):
"""Open charge port."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.charge_port_door_open())
await handle_vehicle_command(self.api.charge_port_door_open())
self._attr_is_closed = False
self.async_write_ha_state()
@ -135,7 +140,7 @@ class TeslemetryChargePortEntity(TeslemetryVehicleEntity, CoverEntity):
"""Close charge port."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.charge_port_door_close())
await handle_vehicle_command(self.api.charge_port_door_close())
self._attr_is_closed = True
self.async_write_ha_state()
@ -162,7 +167,7 @@ class TeslemetryFrontTrunkEntity(TeslemetryVehicleEntity, CoverEntity):
"""Open front trunk."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.actuate_trunk(Trunk.FRONT))
await handle_vehicle_command(self.api.actuate_trunk(Trunk.FRONT))
self._attr_is_closed = False
self.async_write_ha_state()
@ -198,7 +203,7 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity):
if self.is_closed is not False:
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.actuate_trunk(Trunk.REAR))
await handle_vehicle_command(self.api.actuate_trunk(Trunk.REAR))
self._attr_is_closed = False
self.async_write_ha_state()
@ -207,6 +212,6 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity):
if self.is_closed is not True:
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.actuate_trunk(Trunk.REAR))
await handle_vehicle_command(self.api.actuate_trunk(Trunk.REAR))
self._attr_is_closed = True
self.async_write_ha_state()

View File

@ -1,22 +1,21 @@
"""Teslemetry parent entity class."""
from abc import abstractmethod
import asyncio
from typing import Any
from tesla_fleet_api import EnergySpecific, VehicleSpecific
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LOGGER, TeslemetryState
from .const import DOMAIN
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
from .helpers import wake_up_vehicle
from .models import TeslemetryEnergyData, TeslemetryVehicleData
@ -76,15 +75,6 @@ class TeslemetryEntity(
"""Return True if a specific value is in coordinator data."""
return self.key in self.coordinator.data
async def handle_command(self, command) -> dict[str, Any]:
"""Handle a command."""
try:
result = await command
except TeslaFleetError as e:
raise HomeAssistantError(f"Teslemetry command failed, {e.message}") from e
LOGGER.debug("Command result: %s", result)
return result
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_attrs()
@ -113,7 +103,7 @@ class TeslemetryVehicleEntity(TeslemetryEntity):
"""Initialize common aspects of a Teslemetry entity."""
self._attr_unique_id = f"{data.vin}-{key}"
self._wakelock = data.wakelock
self.vehicle = data
self._attr_device_info = data.device
super().__init__(data.coordinator, data.api, key)
@ -125,44 +115,7 @@ class TeslemetryVehicleEntity(TeslemetryEntity):
async def wake_up_if_asleep(self) -> None:
"""Wake up the vehicle if its asleep."""
async with self._wakelock:
times = 0
while self.coordinator.data["state"] != TeslemetryState.ONLINE:
try:
if times == 0:
cmd = await self.api.wake_up()
else:
cmd = await self.api.vehicle()
state = cmd["response"]["state"]
except TeslaFleetError as e:
raise HomeAssistantError(str(e)) from e
self.coordinator.data["state"] = state
if state != TeslemetryState.ONLINE:
times += 1
if times >= 4: # Give up after 30 seconds total
raise HomeAssistantError("Could not wake up vehicle")
await asyncio.sleep(times * 5)
async def handle_command(self, command) -> dict[str, Any]:
"""Handle a vehicle command."""
result = await super().handle_command(command)
if (response := result.get("response")) is None:
if error := result.get("error"):
# No response with error
raise HomeAssistantError(error)
# No response without error (unexpected)
raise HomeAssistantError(f"Unknown response: {response}")
if (result := response.get("result")) is not True:
if reason := response.get("reason"):
if reason in ("already_set", "not_charging", "requested"):
# Reason is acceptable
return result
# Result of false with reason
raise HomeAssistantError(reason)
# Result of false without reason (unexpected)
raise HomeAssistantError("Command failed with no reason")
# Response with result of true
return result
await wake_up_vehicle(self.vehicle)
class TeslemetryEnergyLiveEntity(TeslemetryEntity):

View File

@ -0,0 +1,63 @@
"""Teslemetry helper functions."""
import asyncio
from typing import Any
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.exceptions import HomeAssistantError
from .const import LOGGER, TeslemetryState
async def wake_up_vehicle(vehicle) -> None:
"""Wake up a vehicle."""
async with vehicle.wakelock:
times = 0
while vehicle.coordinator.data["state"] != TeslemetryState.ONLINE:
try:
if times == 0:
cmd = await vehicle.api.wake_up()
else:
cmd = await vehicle.api.vehicle()
state = cmd["response"]["state"]
except TeslaFleetError as e:
raise HomeAssistantError(str(e)) from e
vehicle.coordinator.data["state"] = state
if state != TeslemetryState.ONLINE:
times += 1
if times >= 4: # Give up after 30 seconds total
raise HomeAssistantError("Could not wake up vehicle")
await asyncio.sleep(times * 5)
async def handle_command(command) -> dict[str, Any]:
"""Handle a command."""
try:
result = await command
except TeslaFleetError as e:
raise HomeAssistantError(f"Teslemetry command failed, {e.message}") from e
LOGGER.debug("Command result: %s", result)
return result
async def handle_vehicle_command(command) -> dict[str, Any]:
"""Handle a vehicle command."""
result = await handle_command(command)
if (response := result.get("response")) is None:
if error := result.get("error"):
# No response with error
raise HomeAssistantError(error)
# No response without error (unexpected)
raise HomeAssistantError(f"Unknown response: {response}")
if (result := response.get("result")) is not True:
if reason := response.get("reason"):
if reason in ("already_set", "not_charging", "requested"):
# Reason is acceptable
return result
# Result of false with reason
raise HomeAssistantError(reason)
# Result of false without reason (unexpected)
raise HomeAssistantError("Command failed with no reason")
# Response with result of true
return result

View File

@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .const import DOMAIN
from .entity import TeslemetryVehicleEntity
from .helpers import handle_vehicle_command
from .models import TeslemetryVehicleData
ENGAGED = "Engaged"
@ -52,7 +53,7 @@ class TeslemetryVehicleLockEntity(TeslemetryVehicleEntity, LockEntity):
"""Lock the doors."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.door_lock())
await handle_vehicle_command(self.api.door_lock())
self._attr_is_locked = True
self.async_write_ha_state()
@ -60,7 +61,7 @@ class TeslemetryVehicleLockEntity(TeslemetryVehicleEntity, LockEntity):
"""Unlock the doors."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.door_unlock())
await handle_vehicle_command(self.api.door_unlock())
self._attr_is_locked = False
self.async_write_ha_state()
@ -95,6 +96,6 @@ class TeslemetryCableLockEntity(TeslemetryVehicleEntity, LockEntity):
"""Unlock charge cable lock."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.charge_port_door_open())
await handle_vehicle_command(self.api.charge_port_door_open())
self._attr_is_locked = False
self.async_write_ha_state()

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.5.12"]
"requirements": ["tesla-fleet-api==0.6.1"]
}

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .entity import TeslemetryVehicleEntity
from .helpers import handle_vehicle_command
from .models import TeslemetryVehicleData
STATES = {
@ -114,7 +115,7 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
"""Set volume level, range 0..1."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(
await handle_vehicle_command(
self.api.adjust_volume(int(volume * self._volume_max))
)
self._attr_volume_level = volume
@ -125,7 +126,7 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
if self.state != MediaPlayerState.PLAYING:
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_toggle_playback())
await handle_vehicle_command(self.api.media_toggle_playback())
self._attr_state = MediaPlayerState.PLAYING
self.async_write_ha_state()
@ -134,7 +135,7 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
if self.state == MediaPlayerState.PLAYING:
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_toggle_playback())
await handle_vehicle_command(self.api.media_toggle_playback())
self._attr_state = MediaPlayerState.PAUSED
self.async_write_ha_state()
@ -142,10 +143,10 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
"""Send next track command."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_next_track())
await handle_vehicle_command(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.handle_command(self.api.media_prev_track())
await handle_vehicle_command(self.api.media_prev_track())

View File

@ -23,6 +23,7 @@ from homeassistant.helpers.icon import icon_for_battery_level
from . import TeslemetryConfigEntry
from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity
from .helpers import handle_command, handle_vehicle_command
from .models import TeslemetryEnergyData, TeslemetryVehicleData
@ -163,7 +164,7 @@ class TeslemetryVehicleNumberEntity(TeslemetryVehicleEntity, NumberEntity):
value = int(value)
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.entity_description.func(self.api, value))
await handle_vehicle_command(self.entity_description.func(self.api, value))
self._attr_native_value = value
self.async_write_ha_state()
@ -198,6 +199,6 @@ class TeslemetryEnergyInfoNumberSensorEntity(TeslemetryEnergyInfoEntity, NumberE
"""Set new value."""
value = int(value)
self.raise_for_scope()
await self.handle_command(self.entity_description.func(self.api, value))
await handle_command(self.entity_description.func(self.api, value))
self._attr_native_value = value
self.async_write_ha_state()

View File

@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity
from .helpers import handle_command, handle_vehicle_command
from .models import TeslemetryEnergyData, TeslemetryVehicleData
OFF = "off"
@ -146,8 +147,8 @@ class TeslemetrySeatHeaterSelectEntity(TeslemetryVehicleEntity, SelectEntity):
level = self._attr_options.index(option)
# AC must be on to turn on seat heater
if level and not self.get("climate_state_is_climate_on"):
await self.handle_command(self.api.auto_conditioning_start())
await self.handle_command(
await handle_vehicle_command(self.api.auto_conditioning_start())
await handle_vehicle_command(
self.api.remote_seat_heater_request(self.entity_description.position, level)
)
self._attr_current_option = option
@ -191,8 +192,8 @@ class TeslemetryWheelHeaterSelectEntity(TeslemetryVehicleEntity, SelectEntity):
level = self._attr_options.index(option)
# AC must be on to turn on steering wheel heater
if level and not self.get("climate_state_is_climate_on"):
await self.handle_command(self.api.auto_conditioning_start())
await self.handle_command(
await handle_vehicle_command(self.api.auto_conditioning_start())
await handle_vehicle_command(
self.api.remote_steering_wheel_heat_level_request(level)
)
self._attr_current_option = option
@ -224,7 +225,7 @@ class TeslemetryOperationSelectEntity(TeslemetryEnergyInfoEntity, SelectEntity):
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
self.raise_for_scope()
await self.handle_command(self.api.operation(option))
await handle_command(self.api.operation(option))
self._attr_current_option = option
self.async_write_ha_state()
@ -254,7 +255,7 @@ class TeslemetryExportRuleSelectEntity(TeslemetryEnergyInfoEntity, SelectEntity)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
self.raise_for_scope()
await self.handle_command(
await handle_command(
self.api.grid_import_export(customer_preferred_export_rule=option)
)
self._attr_current_option = option

View File

@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity
from .helpers import handle_command, handle_vehicle_command
from .models import TeslemetryEnergyData, TeslemetryVehicleData
@ -156,7 +157,7 @@ class TeslemetryVehicleSwitchEntity(TeslemetryVehicleEntity, TeslemetrySwitchEnt
"""Turn on the Switch."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.entity_description.on_func(self.api))
await handle_vehicle_command(self.entity_description.on_func(self.api))
self._attr_is_on = True
self.async_write_ha_state()
@ -164,7 +165,7 @@ class TeslemetryVehicleSwitchEntity(TeslemetryVehicleEntity, TeslemetrySwitchEnt
"""Turn off the Switch."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.entity_description.off_func(self.api))
await handle_vehicle_command(self.entity_description.off_func(self.api))
self._attr_is_on = False
self.async_write_ha_state()
@ -205,7 +206,7 @@ class TeslemetryChargeFromGridSwitchEntity(
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Switch."""
self.raise_for_scope()
await self.handle_command(
await handle_command(
self.api.grid_import_export(
disallow_charge_from_grid_with_solar_installed=False
)
@ -216,7 +217,7 @@ class TeslemetryChargeFromGridSwitchEntity(
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Switch."""
self.raise_for_scope()
await self.handle_command(
await handle_command(
self.api.grid_import_export(
disallow_charge_from_grid_with_solar_installed=True
)
@ -247,13 +248,13 @@ class TeslemetryStormModeSwitchEntity(
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Switch."""
self.raise_for_scope()
await self.handle_command(self.api.storm_mode(enabled=True))
await handle_command(self.api.storm_mode(enabled=True))
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Switch."""
self.raise_for_scope()
await self.handle_command(self.api.storm_mode(enabled=False))
await handle_command(self.api.storm_mode(enabled=False))
self._attr_is_on = False
self.async_write_ha_state()

View File

@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TeslemetryConfigEntry
from .entity import TeslemetryVehicleEntity
from .helpers import handle_vehicle_command
from .models import TeslemetryVehicleData
AVAILABLE = "available"
@ -102,6 +103,6 @@ class TeslemetryUpdateEntity(TeslemetryVehicleEntity, UpdateEntity):
"""Install an update."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.schedule_software_update(offset_sec=60))
await handle_vehicle_command(self.api.schedule_software_update(offset_sec=60))
self._attr_in_progress = True
self.async_write_ha_state()

View File

@ -2707,7 +2707,7 @@ temperusb==1.6.1
# tensorflow==2.5.0
# homeassistant.components.teslemetry
tesla-fleet-api==0.5.12
tesla-fleet-api==0.6.1
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View File

@ -2105,7 +2105,7 @@ temescal==0.5
temperusb==1.6.1
# homeassistant.components.teslemetry
tesla-fleet-api==0.5.12
tesla-fleet-api==0.6.1
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View File

@ -23,7 +23,7 @@
'model': 'Powerwall 2, Tesla Backup Gateway 2',
'name': 'Energy Site',
'name_by_user': None,
'serial_number': None,
'serial_number': '123456',
'suggested_area': None,
'sw_version': None,
'via_device_id': None,

View File

@ -343,7 +343,7 @@ async def test_asleep_or_offline(
mock_wake_up.return_value = WAKE_UP_ASLEEP
mock_vehicle.return_value = WAKE_UP_ASLEEP
with (
patch("homeassistant.components.teslemetry.entity.asyncio.sleep"),
patch("homeassistant.components.teslemetry.helpers.asyncio.sleep"),
pytest.raises(HomeAssistantError) as error,
):
await hass.services.async_call(