mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Add streaming to Climate platform in Teslemetry (#138689)
* Add streaming climate * fixes * Add missing changes * Fix restore * Update homeassistant/components/teslemetry/climate.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Use dict * Add fan mode translations * Infer side * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/teslemetry/climate.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
646c97a26c
commit
013439f7c6
@ -6,9 +6,11 @@ from itertools import chain
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from tesla_fleet_api.const import CabinOverheatProtectionTemp, Scope
|
from tesla_fleet_api.const import CabinOverheatProtectionTemp, Scope
|
||||||
|
from tesla_fleet_api.vehicle import VehicleSpecific
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
|
HVAC_MODES,
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
@ -22,15 +24,32 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
||||||
from . import TeslemetryConfigEntry
|
from . import TeslemetryConfigEntry
|
||||||
from .const import DOMAIN, TeslemetryClimateSide
|
from .const import DOMAIN, TeslemetryClimateSide
|
||||||
from .entity import TeslemetryVehicleEntity
|
from .entity import (
|
||||||
|
TeslemetryRootEntity,
|
||||||
|
TeslemetryVehicleEntity,
|
||||||
|
TeslemetryVehicleStreamEntity,
|
||||||
|
)
|
||||||
from .helpers import handle_vehicle_command
|
from .helpers import handle_vehicle_command
|
||||||
from .models import TeslemetryVehicleData
|
from .models import TeslemetryVehicleData
|
||||||
|
|
||||||
DEFAULT_MIN_TEMP = 15
|
DEFAULT_MIN_TEMP = 15
|
||||||
DEFAULT_MAX_TEMP = 28
|
DEFAULT_MAX_TEMP = 28
|
||||||
|
COP_TEMPERATURES = {
|
||||||
|
30: CabinOverheatProtectionTemp.LOW,
|
||||||
|
35: CabinOverheatProtectionTemp.MEDIUM,
|
||||||
|
40: CabinOverheatProtectionTemp.HIGH,
|
||||||
|
}
|
||||||
|
PRESET_MODES = {
|
||||||
|
"Off": "off",
|
||||||
|
"On": "keep",
|
||||||
|
"Dog": "dog",
|
||||||
|
"Party": "camp",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@ -45,13 +64,21 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
chain(
|
chain(
|
||||||
(
|
(
|
||||||
TeslemetryClimateEntity(
|
TeslemetryPollingClimateEntity(
|
||||||
|
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
|
||||||
|
)
|
||||||
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
||||||
|
else TeslemetryStreamingClimateEntity(
|
||||||
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
|
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
|
||||||
)
|
)
|
||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TeslemetryCabinOverheatProtectionEntity(
|
TeslemetryPollingCabinOverheatProtectionEntity(
|
||||||
|
vehicle, entry.runtime_data.scopes
|
||||||
|
)
|
||||||
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
||||||
|
else TeslemetryStreamingCabinOverheatProtectionEntity(
|
||||||
vehicle, entry.runtime_data.scopes
|
vehicle, entry.runtime_data.scopes
|
||||||
)
|
)
|
||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
@ -60,66 +87,22 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
|
class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity):
|
||||||
"""Telemetry vehicle climate entity."""
|
"""Vehicle Climate Control."""
|
||||||
|
|
||||||
|
api: VehicleSpecific
|
||||||
|
|
||||||
_attr_precision = PRECISION_HALVES
|
_attr_precision = PRECISION_HALVES
|
||||||
|
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
|
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
|
||||||
_attr_supported_features = (
|
_attr_preset_modes = list(PRESET_MODES.values())
|
||||||
ClimateEntityFeature.TURN_ON
|
_attr_fan_modes = ["off", "bioweapon"]
|
||||||
| ClimateEntityFeature.TURN_OFF
|
_enable_turn_on_off_backwards_compatibility = False
|
||||||
| ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.PRESET_MODE
|
|
||||||
)
|
|
||||||
_attr_preset_modes = ["off", "keep", "dog", "camp"]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
data: TeslemetryVehicleData,
|
|
||||||
side: TeslemetryClimateSide,
|
|
||||||
scopes: Scope,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the climate."""
|
|
||||||
self.scoped = Scope.VEHICLE_CMDS in scopes
|
|
||||||
|
|
||||||
if not self.scoped:
|
|
||||||
self._attr_supported_features = ClimateEntityFeature(0)
|
|
||||||
self._attr_hvac_modes = []
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
data,
|
|
||||||
side,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _async_update_attrs(self) -> None:
|
|
||||||
"""Update the attributes of the entity."""
|
|
||||||
value = self.get("climate_state_is_climate_on")
|
|
||||||
if value:
|
|
||||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
|
||||||
else:
|
|
||||||
self._attr_hvac_mode = HVACMode.OFF
|
|
||||||
|
|
||||||
# If not scoped, prevent the user from changing the HVAC mode by making it the only option
|
|
||||||
if self._attr_hvac_mode and not self.scoped:
|
|
||||||
self._attr_hvac_modes = [self._attr_hvac_mode]
|
|
||||||
|
|
||||||
self._attr_current_temperature = self.get("climate_state_inside_temp")
|
|
||||||
self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
|
|
||||||
self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
|
|
||||||
self._attr_min_temp = cast(
|
|
||||||
float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
|
|
||||||
)
|
|
||||||
self._attr_max_temp = cast(
|
|
||||||
float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Set the climate state to on."""
|
"""Set the climate state to on."""
|
||||||
|
|
||||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
await self.wake_up_if_asleep()
|
|
||||||
await handle_vehicle_command(self.api.auto_conditioning_start())
|
await handle_vehicle_command(self.api.auto_conditioning_start())
|
||||||
|
|
||||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||||
@ -127,19 +110,21 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Set the climate state to off."""
|
"""Set the climate state to off."""
|
||||||
|
|
||||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
await self.wake_up_if_asleep()
|
|
||||||
await handle_vehicle_command(self.api.auto_conditioning_stop())
|
await handle_vehicle_command(self.api.auto_conditioning_stop())
|
||||||
|
|
||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
self._attr_preset_mode = self._attr_preset_modes[0]
|
self._attr_preset_mode = self._attr_preset_modes[0]
|
||||||
|
self._attr_fan_mode = self._attr_fan_modes[0]
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set the climate temperature."""
|
"""Set the climate temperature."""
|
||||||
|
|
||||||
if temp := kwargs.get(ATTR_TEMPERATURE):
|
if temp := kwargs.get(ATTR_TEMPERATURE):
|
||||||
await self.wake_up_if_asleep()
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
|
|
||||||
await handle_vehicle_command(
|
await handle_vehicle_command(
|
||||||
self.api.set_temps(
|
self.api.set_temps(
|
||||||
driver_temp=temp,
|
driver_temp=temp,
|
||||||
@ -163,18 +148,210 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set the climate preset mode."""
|
"""Set the climate preset mode."""
|
||||||
await self.wake_up_if_asleep()
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
|
|
||||||
await handle_vehicle_command(
|
await handle_vehicle_command(
|
||||||
self.api.set_climate_keeper_mode(
|
self.api.set_climate_keeper_mode(
|
||||||
climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
|
climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._attr_preset_mode = preset_mode
|
self._attr_preset_mode = preset_mode
|
||||||
if preset_mode != self._attr_preset_modes[0]:
|
if preset_mode == self._attr_preset_modes[0]:
|
||||||
# Changing preset mode will also turn on climate
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
|
else:
|
||||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
|
"""Set the Bioweapon defense mode."""
|
||||||
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
|
|
||||||
|
await handle_vehicle_command(
|
||||||
|
self.api.set_bioweapon_mode(
|
||||||
|
on=(fan_mode != "off"),
|
||||||
|
manual_override=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._attr_fan_mode = fan_mode
|
||||||
|
if fan_mode == self._attr_fan_modes[1]:
|
||||||
|
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
|
||||||
|
"""Polling vehicle climate entity."""
|
||||||
|
|
||||||
|
_attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TURN_ON
|
||||||
|
| ClimateEntityFeature.TURN_OFF
|
||||||
|
| ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| ClimateEntityFeature.PRESET_MODE
|
||||||
|
| ClimateEntityFeature.FAN_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslemetryVehicleData,
|
||||||
|
side: TeslemetryClimateSide,
|
||||||
|
scopes: list[Scope],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the climate."""
|
||||||
|
self.scoped = Scope.VEHICLE_CMDS in scopes
|
||||||
|
if not self.scoped:
|
||||||
|
self._attr_supported_features = ClimateEntityFeature(0)
|
||||||
|
|
||||||
|
super().__init__(data, side)
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the entity."""
|
||||||
|
value = self.get("climate_state_is_climate_on")
|
||||||
|
if value is None:
|
||||||
|
self._attr_hvac_mode = None
|
||||||
|
if value:
|
||||||
|
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||||
|
else:
|
||||||
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
|
self._attr_current_temperature = self.get("climate_state_inside_temp")
|
||||||
|
self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
|
||||||
|
self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
|
||||||
|
if self.get("climate_state_bioweapon_mode"):
|
||||||
|
self._attr_fan_mode = "bioweapon"
|
||||||
|
else:
|
||||||
|
self._attr_fan_mode = "off"
|
||||||
|
self._attr_min_temp = cast(
|
||||||
|
float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
|
||||||
|
)
|
||||||
|
self._attr_max_temp = cast(
|
||||||
|
float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryStreamingClimateEntity(
|
||||||
|
TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
|
||||||
|
):
|
||||||
|
"""Teslemetry steering wheel climate control."""
|
||||||
|
|
||||||
|
_attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TURN_ON
|
||||||
|
| ClimateEntityFeature.TURN_OFF
|
||||||
|
| ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| ClimateEntityFeature.PRESET_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslemetryVehicleData,
|
||||||
|
side: TeslemetryClimateSide,
|
||||||
|
scopes: list[Scope],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the climate."""
|
||||||
|
|
||||||
|
# Initialize defaults
|
||||||
|
self._attr_hvac_mode = None
|
||||||
|
self._attr_current_temperature = None
|
||||||
|
self._attr_target_temperature = None
|
||||||
|
self._attr_fan_mode = None
|
||||||
|
self._attr_preset_mode = None
|
||||||
|
|
||||||
|
self.scoped = Scope.VEHICLE_CMDS in scopes
|
||||||
|
if not self.scoped:
|
||||||
|
self._attr_supported_features = ClimateEntityFeature(0)
|
||||||
|
self.side = side
|
||||||
|
super().__init__(
|
||||||
|
data,
|
||||||
|
side,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._attr_min_temp = cast(
|
||||||
|
float,
|
||||||
|
data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
|
||||||
|
)
|
||||||
|
self._attr_max_temp = cast(
|
||||||
|
float,
|
||||||
|
data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
|
||||||
|
)
|
||||||
|
self.rhd: bool = data.coordinator.data.get("vehicle_config_rhd", False)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if (state := await self.async_get_last_state()) is not None:
|
||||||
|
self._attr_hvac_mode = (
|
||||||
|
HVACMode(state.state) if state.state in HVAC_MODES else None
|
||||||
|
)
|
||||||
|
self._attr_current_temperature = state.attributes.get("current_temperature")
|
||||||
|
self._attr_target_temperature = state.attributes.get("temperature")
|
||||||
|
self._attr_preset_mode = state.attributes.get("preset_mode")
|
||||||
|
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_InsideTemp(
|
||||||
|
self._async_handle_inside_temp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_HvacACEnabled(
|
||||||
|
self._async_handle_hvac_ac_enabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_ClimateKeeperMode(
|
||||||
|
self._async_handle_climate_keeper_mode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_RightHandDrive(self._async_handle_rhd)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.side == TeslemetryClimateSide.DRIVER:
|
||||||
|
if self.rhd:
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
|
||||||
|
self._async_handle_hvac_temperature_request
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
|
||||||
|
self._async_handle_hvac_temperature_request
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.side == TeslemetryClimateSide.PASSENGER:
|
||||||
|
if self.rhd:
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
|
||||||
|
self._async_handle_hvac_temperature_request
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
|
||||||
|
self._async_handle_hvac_temperature_request
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _async_handle_inside_temp(self, data: float | None):
|
||||||
|
self._attr_current_temperature = data
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _async_handle_hvac_ac_enabled(self, data: bool | None):
|
||||||
|
self._attr_hvac_mode = (
|
||||||
|
None if data is None else HVACMode.HEAT_COOL if data else HVACMode.OFF
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _async_handle_climate_keeper_mode(self, data: str | None):
|
||||||
|
self._attr_preset_mode = PRESET_MODES.get(data) if data else None
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _async_handle_hvac_temperature_request(self, data: float | None):
|
||||||
|
self._attr_target_temperature = data
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _async_handle_rhd(self, data: bool | None):
|
||||||
|
if data is not None:
|
||||||
|
self.rhd = data
|
||||||
|
|
||||||
|
|
||||||
COP_MODES = {
|
COP_MODES = {
|
||||||
"Off": HVACMode.OFF,
|
"Off": HVACMode.OFF,
|
||||||
@ -182,73 +359,27 @@ COP_MODES = {
|
|||||||
"FanOnly": HVACMode.FAN_ONLY,
|
"FanOnly": HVACMode.FAN_ONLY,
|
||||||
}
|
}
|
||||||
|
|
||||||
# String to celsius
|
|
||||||
COP_LEVELS = {
|
COP_LEVELS = {
|
||||||
"Low": 30,
|
"Low": 30,
|
||||||
"Medium": 35,
|
"Medium": 35,
|
||||||
"High": 40,
|
"High": 40,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Celsius to IntEnum
|
|
||||||
TEMP_LEVELS = {
|
|
||||||
30: CabinOverheatProtectionTemp.LOW,
|
|
||||||
35: CabinOverheatProtectionTemp.MEDIUM,
|
|
||||||
40: CabinOverheatProtectionTemp.HIGH,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntity):
|
||||||
|
"""Vehicle Cabin Overheat Protection."""
|
||||||
|
|
||||||
class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEntity):
|
api: VehicleSpecific
|
||||||
"""Telemetry vehicle cabin overheat protection entity."""
|
|
||||||
|
|
||||||
_attr_precision = PRECISION_WHOLE
|
_attr_precision = PRECISION_WHOLE
|
||||||
_attr_target_temperature_step = 5
|
_attr_target_temperature_step = 5
|
||||||
_attr_min_temp = COP_LEVELS["Low"]
|
_attr_min_temp = 30
|
||||||
_attr_max_temp = COP_LEVELS["High"]
|
_attr_max_temp = 40
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_hvac_modes = list(COP_MODES.values())
|
_attr_hvac_modes = list(COP_MODES.values())
|
||||||
|
|
||||||
_attr_entity_registry_enabled_default = False
|
_attr_entity_registry_enabled_default = False
|
||||||
|
|
||||||
def __init__(
|
_enable_turn_on_off_backwards_compatibility = False
|
||||||
self,
|
|
||||||
data: TeslemetryVehicleData,
|
|
||||||
scopes: Scope,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the climate."""
|
|
||||||
|
|
||||||
self.scoped = Scope.VEHICLE_CMDS in scopes
|
|
||||||
if self.scoped:
|
|
||||||
self._attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._attr_supported_features = ClimateEntityFeature(0)
|
|
||||||
self._attr_hvac_modes = []
|
|
||||||
|
|
||||||
super().__init__(data, "climate_state_cabin_overheat_protection")
|
|
||||||
|
|
||||||
# Supported Features from data
|
|
||||||
if self.scoped and self.get("vehicle_config_cop_user_set_temp_supported"):
|
|
||||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
|
|
||||||
def _async_update_attrs(self) -> None:
|
|
||||||
"""Update the attributes of the entity."""
|
|
||||||
|
|
||||||
if (state := self.get("climate_state_cabin_overheat_protection")) is None:
|
|
||||||
self._attr_hvac_mode = None
|
|
||||||
else:
|
|
||||||
self._attr_hvac_mode = COP_MODES.get(state)
|
|
||||||
|
|
||||||
# If not scoped, prevent the user from changing the HVAC mode by making it the only option
|
|
||||||
if self._attr_hvac_mode and not self.scoped:
|
|
||||||
self._attr_hvac_modes = [self._attr_hvac_mode]
|
|
||||||
|
|
||||||
if (level := self.get("climate_state_cop_activation_temperature")) is None:
|
|
||||||
self._attr_target_temperature = None
|
|
||||||
else:
|
|
||||||
self._attr_target_temperature = COP_LEVELS.get(level)
|
|
||||||
|
|
||||||
self._attr_current_temperature = self.get("climate_state_inside_temp")
|
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Set the climate state to on."""
|
"""Set the climate state to on."""
|
||||||
@ -260,26 +391,28 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEn
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set the climate temperature."""
|
"""Set the climate temperature."""
|
||||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
|
||||||
|
|
||||||
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None or (
|
if temp := kwargs.get(ATTR_TEMPERATURE):
|
||||||
cop_mode := TEMP_LEVELS.get(temp)
|
if (cop_mode := COP_TEMPERATURES.get(temp)) is None:
|
||||||
) is None:
|
raise ServiceValidationError(
|
||||||
raise ServiceValidationError(
|
translation_domain=DOMAIN,
|
||||||
translation_domain=DOMAIN,
|
translation_key="invalid_cop_temp",
|
||||||
translation_key="invalid_cop_temp",
|
)
|
||||||
)
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
|
|
||||||
await self.wake_up_if_asleep()
|
await handle_vehicle_command(self.api.set_cop_temp(cop_mode))
|
||||||
await handle_vehicle_command(self.api.set_cop_temp(cop_mode))
|
self._attr_target_temperature = temp
|
||||||
self._attr_target_temperature = temp
|
|
||||||
|
|
||||||
if mode := kwargs.get(ATTR_HVAC_MODE):
|
if mode := kwargs.get(ATTR_HVAC_MODE):
|
||||||
await self._async_set_cop(mode)
|
# Set HVAC mode will call write_ha_state
|
||||||
|
await self.async_set_hvac_mode(mode)
|
||||||
|
else:
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
self.async_write_ha_state()
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
|
"""Set the climate mode and state."""
|
||||||
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
|
|
||||||
async def _async_set_cop(self, hvac_mode: HVACMode) -> None:
|
|
||||||
if hvac_mode == HVACMode.OFF:
|
if hvac_mode == HVACMode.OFF:
|
||||||
await handle_vehicle_command(
|
await handle_vehicle_command(
|
||||||
self.api.set_cabin_overheat_protection(on=False, fan_only=False)
|
self.api.set_cabin_overheat_protection(on=False, fan_only=False)
|
||||||
@ -294,10 +427,125 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEn
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._attr_hvac_mode = hvac_mode
|
self._attr_hvac_mode = hvac_mode
|
||||||
|
self.async_write_ha_state()
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
||||||
"""Set the climate mode and state."""
|
|
||||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
class TeslemetryPollingCabinOverheatProtectionEntity(
|
||||||
await self.wake_up_if_asleep()
|
TeslemetryVehicleEntity, TeslemetryCabinOverheatProtectionEntity
|
||||||
await self._async_set_cop(hvac_mode)
|
):
|
||||||
|
"""Vehicle Cabin Overheat Protection."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslemetryVehicleData,
|
||||||
|
scopes: list[Scope],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the climate."""
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
data,
|
||||||
|
"climate_state_cabin_overheat_protection",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Supported Features
|
||||||
|
self._attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||||
|
)
|
||||||
|
if self.get("vehicle_config_cop_user_set_temp_supported"):
|
||||||
|
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
# Scopes
|
||||||
|
self.scoped = Scope.VEHICLE_CMDS in scopes
|
||||||
|
if not self.scoped:
|
||||||
|
self._attr_supported_features = ClimateEntityFeature(0)
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the entity."""
|
||||||
|
|
||||||
|
if (state := self.get("climate_state_cabin_overheat_protection")) is None:
|
||||||
|
self._attr_hvac_mode = None
|
||||||
|
else:
|
||||||
|
self._attr_hvac_mode = COP_MODES.get(state)
|
||||||
|
|
||||||
|
if (level := self.get("climate_state_cop_activation_temperature")) is None:
|
||||||
|
self._attr_target_temperature = None
|
||||||
|
else:
|
||||||
|
self._attr_target_temperature = COP_LEVELS.get(level)
|
||||||
|
|
||||||
|
self._attr_current_temperature = self.get("climate_state_inside_temp")
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryStreamingCabinOverheatProtectionEntity(
|
||||||
|
TeslemetryVehicleStreamEntity,
|
||||||
|
TeslemetryCabinOverheatProtectionEntity,
|
||||||
|
RestoreEntity,
|
||||||
|
):
|
||||||
|
"""Vehicle Cabin Overheat Protection."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslemetryVehicleData,
|
||||||
|
scopes: list[Scope],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the climate."""
|
||||||
|
|
||||||
|
# Initialize defaults
|
||||||
|
self._attr_hvac_mode = None
|
||||||
|
self._attr_current_temperature = None
|
||||||
|
self._attr_target_temperature = None
|
||||||
|
self._attr_fan_mode = None
|
||||||
|
self._attr_preset_mode = None
|
||||||
|
|
||||||
|
super().__init__(data, "climate_state_cabin_overheat_protection")
|
||||||
|
|
||||||
|
# Supported Features
|
||||||
|
self._attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||||
|
)
|
||||||
|
if data.coordinator.data.get("vehicle_config_cop_user_set_temp_supported"):
|
||||||
|
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
# Scopes
|
||||||
|
self.scoped = Scope.VEHICLE_CMDS in scopes
|
||||||
|
if not self.scoped:
|
||||||
|
self._attr_supported_features = ClimateEntityFeature(0)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if (state := await self.async_get_last_state()) is not None:
|
||||||
|
self._attr_hvac_mode = (
|
||||||
|
HVACMode(state.state) if state.state in HVAC_MODES else None
|
||||||
|
)
|
||||||
|
self._attr_current_temperature = state.attributes.get("temperature")
|
||||||
|
self._attr_target_temperature = state.attributes.get("target_temperature")
|
||||||
|
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_InsideTemp(
|
||||||
|
self._async_handle_inside_temp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_CabinOverheatProtectionMode(
|
||||||
|
self._async_handle_protection_mode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
self.vehicle.stream_vehicle.listen_CabinOverheatProtectionTemperatureLimit(
|
||||||
|
self._async_handle_temperature_limit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _async_handle_inside_temp(self, value: float | None):
|
||||||
|
self._attr_current_temperature = value
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _async_handle_protection_mode(self, value: str | None):
|
||||||
|
self._attr_hvac_mode = COP_MODES.get(value) if value is not None else None
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _async_handle_temperature_limit(self, value: str | None):
|
||||||
|
self._attr_target_temperature = (
|
||||||
|
COP_LEVELS.get(value) if value is not None else None
|
||||||
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -226,6 +226,12 @@
|
|||||||
"dog": "Dog mode",
|
"dog": "Dog mode",
|
||||||
"camp": "Camp mode"
|
"camp": "Camp mode"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"fan_mode": {
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"bioweapon": "Bioweapon defense"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_asleep_or_offline[HomeAssistantError]
|
|
||||||
'Timed out trying to wake up vehicle'
|
|
||||||
# ---
|
|
||||||
# name: test_asleep_or_offline[InvalidCommand]
|
|
||||||
'Failed to wake up vehicle: The data request or command is unknown.'
|
|
||||||
# ---
|
|
||||||
# name: test_climate[climate.test_cabin_overheat_protection-entry]
|
# name: test_climate[climate.test_cabin_overheat_protection-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@ -78,6 +72,10 @@
|
|||||||
}),
|
}),
|
||||||
'area_id': None,
|
'area_id': None,
|
||||||
'capabilities': dict({
|
'capabilities': dict({
|
||||||
|
'fan_modes': list([
|
||||||
|
'off',
|
||||||
|
'bioweapon',
|
||||||
|
]),
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
<HVACMode.OFF: 'off'>,
|
<HVACMode.OFF: 'off'>,
|
||||||
@ -113,7 +111,7 @@
|
|||||||
'original_name': 'Climate',
|
'original_name': 'Climate',
|
||||||
'platform': 'teslemetry',
|
'platform': 'teslemetry',
|
||||||
'previous_unique_id': None,
|
'previous_unique_id': None,
|
||||||
'supported_features': <ClimateEntityFeature: 401>,
|
'supported_features': <ClimateEntityFeature: 409>,
|
||||||
'translation_key': <TeslemetryClimateSide.DRIVER: 'driver_temp'>,
|
'translation_key': <TeslemetryClimateSide.DRIVER: 'driver_temp'>,
|
||||||
'unique_id': 'LRW3F7EK4NC700000-driver_temp',
|
'unique_id': 'LRW3F7EK4NC700000-driver_temp',
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
@ -123,6 +121,11 @@
|
|||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'current_temperature': 30.0,
|
'current_temperature': 30.0,
|
||||||
|
'fan_mode': 'off',
|
||||||
|
'fan_modes': list([
|
||||||
|
'off',
|
||||||
|
'bioweapon',
|
||||||
|
]),
|
||||||
'friendly_name': 'Test Climate',
|
'friendly_name': 'Test Climate',
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
@ -137,7 +140,7 @@
|
|||||||
'dog',
|
'dog',
|
||||||
'camp',
|
'camp',
|
||||||
]),
|
]),
|
||||||
'supported_features': <ClimateEntityFeature: 401>,
|
'supported_features': <ClimateEntityFeature: 409>,
|
||||||
'temperature': 22.0,
|
'temperature': 22.0,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
@ -220,6 +223,10 @@
|
|||||||
}),
|
}),
|
||||||
'area_id': None,
|
'area_id': None,
|
||||||
'capabilities': dict({
|
'capabilities': dict({
|
||||||
|
'fan_modes': list([
|
||||||
|
'off',
|
||||||
|
'bioweapon',
|
||||||
|
]),
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
<HVACMode.OFF: 'off'>,
|
<HVACMode.OFF: 'off'>,
|
||||||
@ -255,7 +262,7 @@
|
|||||||
'original_name': 'Climate',
|
'original_name': 'Climate',
|
||||||
'platform': 'teslemetry',
|
'platform': 'teslemetry',
|
||||||
'previous_unique_id': None,
|
'previous_unique_id': None,
|
||||||
'supported_features': <ClimateEntityFeature: 401>,
|
'supported_features': <ClimateEntityFeature: 409>,
|
||||||
'translation_key': <TeslemetryClimateSide.DRIVER: 'driver_temp'>,
|
'translation_key': <TeslemetryClimateSide.DRIVER: 'driver_temp'>,
|
||||||
'unique_id': 'LRW3F7EK4NC700000-driver_temp',
|
'unique_id': 'LRW3F7EK4NC700000-driver_temp',
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
@ -265,6 +272,11 @@
|
|||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'current_temperature': 30.0,
|
'current_temperature': 30.0,
|
||||||
|
'fan_mode': 'off',
|
||||||
|
'fan_modes': list([
|
||||||
|
'off',
|
||||||
|
'bioweapon',
|
||||||
|
]),
|
||||||
'friendly_name': 'Test Climate',
|
'friendly_name': 'Test Climate',
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
@ -279,7 +291,7 @@
|
|||||||
'dog',
|
'dog',
|
||||||
'camp',
|
'camp',
|
||||||
]),
|
]),
|
||||||
'supported_features': <ClimateEntityFeature: 401>,
|
'supported_features': <ClimateEntityFeature: 409>,
|
||||||
'temperature': 22.0,
|
'temperature': 22.0,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
@ -297,7 +309,9 @@
|
|||||||
'area_id': None,
|
'area_id': None,
|
||||||
'capabilities': dict({
|
'capabilities': dict({
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 40,
|
'max_temp': 40,
|
||||||
'min_temp': 30,
|
'min_temp': 30,
|
||||||
@ -339,6 +353,7 @@
|
|||||||
'capabilities': dict({
|
'capabilities': dict({
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 28.0,
|
'max_temp': 28.0,
|
||||||
'min_temp': 15.0,
|
'min_temp': 15.0,
|
||||||
@ -374,3 +389,85 @@
|
|||||||
# name: test_invalid_error[error]
|
# name: test_invalid_error[error]
|
||||||
'Command returned exception: The data request or command is unknown.'
|
'Command returned exception: The data request or command is unknown.'
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_select_streaming[climate.test_cabin_overheat_protection]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'current_temperature': None,
|
||||||
|
'friendly_name': 'Test Cabin overheat protection',
|
||||||
|
'hvac_modes': list([
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
|
<HVACMode.COOL: 'cool'>,
|
||||||
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
|
]),
|
||||||
|
'max_temp': 40,
|
||||||
|
'min_temp': 30,
|
||||||
|
'supported_features': <ClimateEntityFeature: 385>,
|
||||||
|
'target_temp_step': 5,
|
||||||
|
'temperature': None,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'climate.test_cabin_overheat_protection',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'cool',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_select_streaming[climate.test_climate LHD]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'current_temperature': 26.0,
|
||||||
|
'friendly_name': 'Test Climate',
|
||||||
|
'hvac_modes': list([
|
||||||
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
|
]),
|
||||||
|
'max_temp': 28.0,
|
||||||
|
'min_temp': 15.0,
|
||||||
|
'preset_mode': None,
|
||||||
|
'preset_modes': list([
|
||||||
|
'off',
|
||||||
|
'keep',
|
||||||
|
'dog',
|
||||||
|
'camp',
|
||||||
|
]),
|
||||||
|
'supported_features': <ClimateEntityFeature: 401>,
|
||||||
|
'temperature': 21.0,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'climate.test_climate',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'heat_cool',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_select_streaming[climate.test_climate]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'current_temperature': 26.0,
|
||||||
|
'friendly_name': 'Test Climate',
|
||||||
|
'hvac_modes': list([
|
||||||
|
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
|
]),
|
||||||
|
'max_temp': 28.0,
|
||||||
|
'min_temp': 15.0,
|
||||||
|
'preset_mode': None,
|
||||||
|
'preset_modes': list([
|
||||||
|
'off',
|
||||||
|
'keep',
|
||||||
|
'dog',
|
||||||
|
'camp',
|
||||||
|
]),
|
||||||
|
'supported_features': <ClimateEntityFeature: 401>,
|
||||||
|
'temperature': 21.0,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'climate.test_climate',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'heat_cool',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
from tesla_fleet_api.exceptions import InvalidCommand
|
from tesla_fleet_api.exceptions import InvalidCommand
|
||||||
|
from teslemetry_stream import Signal
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
@ -24,15 +24,12 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import assert_entities, setup_platform
|
from . import assert_entities, reload_platform, setup_platform
|
||||||
from .const import (
|
from .const import (
|
||||||
COMMAND_ERRORS,
|
COMMAND_ERRORS,
|
||||||
COMMAND_IGNORED_REASON,
|
COMMAND_IGNORED_REASON,
|
||||||
METADATA_NOSCOPE,
|
METADATA_NOSCOPE,
|
||||||
VEHICLE_DATA_ALT,
|
VEHICLE_DATA_ALT,
|
||||||
VEHICLE_DATA_ASLEEP,
|
|
||||||
WAKE_UP_ASLEEP,
|
|
||||||
WAKE_UP_ONLINE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +38,7 @@ async def test_climate(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_legacy: AsyncMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests that the climate entity is correct."""
|
"""Tests that the climate entity is correct."""
|
||||||
|
|
||||||
@ -195,6 +193,7 @@ async def test_climate_alt(
|
|||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
mock_vehicle_data: AsyncMock,
|
mock_vehicle_data: AsyncMock,
|
||||||
|
mock_legacy: AsyncMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests that the climate entity is correct."""
|
"""Tests that the climate entity is correct."""
|
||||||
|
|
||||||
@ -269,71 +268,12 @@ async def test_ignored_error(
|
|||||||
mock_on.assert_called_once()
|
mock_on.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
||||||
async def test_asleep_or_offline(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_vehicle_data: AsyncMock,
|
|
||||||
mock_wake_up: AsyncMock,
|
|
||||||
mock_vehicle: AsyncMock,
|
|
||||||
freezer: FrozenDateTimeFactory,
|
|
||||||
snapshot: SnapshotAssertion,
|
|
||||||
) -> None:
|
|
||||||
"""Tests asleep is handled."""
|
|
||||||
|
|
||||||
mock_vehicle_data.return_value = VEHICLE_DATA_ASLEEP
|
|
||||||
await setup_platform(hass, [Platform.CLIMATE])
|
|
||||||
entity_id = "climate.test_climate"
|
|
||||||
|
|
||||||
# Run a command but fail trying to wake up the vehicle
|
|
||||||
mock_wake_up.side_effect = InvalidCommand
|
|
||||||
with pytest.raises(HomeAssistantError) as error:
|
|
||||||
await hass.services.async_call(
|
|
||||||
CLIMATE_DOMAIN,
|
|
||||||
SERVICE_TURN_ON,
|
|
||||||
{ATTR_ENTITY_ID: [entity_id]},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert str(error.value) == snapshot(name="InvalidCommand")
|
|
||||||
mock_wake_up.assert_called_once()
|
|
||||||
|
|
||||||
mock_wake_up.side_effect = None
|
|
||||||
mock_wake_up.reset_mock()
|
|
||||||
|
|
||||||
# Run a command but timeout trying to wake up the vehicle
|
|
||||||
mock_wake_up.return_value = WAKE_UP_ASLEEP
|
|
||||||
mock_vehicle.return_value = WAKE_UP_ASLEEP
|
|
||||||
with (
|
|
||||||
patch("homeassistant.components.teslemetry.helpers.asyncio.sleep"),
|
|
||||||
pytest.raises(HomeAssistantError) as error,
|
|
||||||
):
|
|
||||||
await hass.services.async_call(
|
|
||||||
CLIMATE_DOMAIN,
|
|
||||||
SERVICE_TURN_ON,
|
|
||||||
{ATTR_ENTITY_ID: [entity_id]},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert str(error.value) == snapshot(name="HomeAssistantError")
|
|
||||||
mock_wake_up.assert_called_once()
|
|
||||||
mock_vehicle.assert_called()
|
|
||||||
|
|
||||||
mock_wake_up.reset_mock()
|
|
||||||
mock_vehicle.reset_mock()
|
|
||||||
mock_wake_up.return_value = WAKE_UP_ONLINE
|
|
||||||
mock_vehicle.return_value = WAKE_UP_ONLINE
|
|
||||||
|
|
||||||
# Run a command and wake up the vehicle immediately
|
|
||||||
await hass.services.async_call(
|
|
||||||
CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
mock_wake_up.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
async def test_climate_noscope(
|
async def test_climate_noscope(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
mock_metadata: AsyncMock,
|
mock_metadata: AsyncMock,
|
||||||
|
mock_legacy: AsyncMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests that the climate entity is correct."""
|
"""Tests that the climate entity is correct."""
|
||||||
mock_metadata.return_value = METADATA_NOSCOPE
|
mock_metadata.return_value = METADATA_NOSCOPE
|
||||||
@ -363,3 +303,47 @@ async def test_climate_noscope(
|
|||||||
{ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20},
|
{ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_select_streaming(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
mock_vehicle_data: AsyncMock,
|
||||||
|
mock_add_listener: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Tests that the select entities with streaming are correct."""
|
||||||
|
|
||||||
|
entry = await setup_platform(hass, [Platform.CLIMATE])
|
||||||
|
|
||||||
|
# Stream update
|
||||||
|
mock_add_listener.send(
|
||||||
|
{
|
||||||
|
"vin": VEHICLE_DATA_ALT["response"]["vin"],
|
||||||
|
"data": {
|
||||||
|
Signal.INSIDE_TEMP: 26,
|
||||||
|
Signal.HVAC_AC_ENABLED: True,
|
||||||
|
Signal.CLIMATE_KEEPER_MODE: "ClimateKeeperModeOn",
|
||||||
|
Signal.RIGHT_HAND_DRIVE: True,
|
||||||
|
Signal.HVAC_LEFT_TEMPERATURE_REQUEST: 22,
|
||||||
|
Signal.HVAC_RIGHT_TEMPERATURE_REQUEST: 21,
|
||||||
|
Signal.CABIN_OVERHEAT_PROTECTION_MODE: "CabinOverheatProtectionModeStateOn",
|
||||||
|
Signal.CABIN_OVERHEAT_PROTECTION_TEMPERATURE_LIMIT: 35,
|
||||||
|
},
|
||||||
|
"createdAt": "2024-10-04T10:45:17.537Z",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("climate.test_climate") == snapshot(
|
||||||
|
name="climate.test_climate LHD"
|
||||||
|
)
|
||||||
|
|
||||||
|
await reload_platform(hass, entry, [Platform.CLIMATE])
|
||||||
|
|
||||||
|
# Assert the entities restored their values
|
||||||
|
for entity_id in (
|
||||||
|
"climate.test_climate",
|
||||||
|
"climate.test_cabin_overheat_protection",
|
||||||
|
):
|
||||||
|
assert hass.states.get(entity_id) == snapshot(name=entity_id)
|
||||||
|
@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
from tesla_fleet_api.exceptions import (
|
from tesla_fleet_api.exceptions import (
|
||||||
InvalidToken,
|
InvalidToken,
|
||||||
SubscriptionRequired,
|
SubscriptionRequired,
|
||||||
TeslaFleetError,
|
TeslaFleetError,
|
||||||
VehicleOffline,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
|
|
||||||
from homeassistant.components.teslemetry.models import TeslemetryData
|
from homeassistant.components.teslemetry.models import TeslemetryData
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||||
@ -22,8 +19,6 @@ from homeassistant.helpers import device_registry as dr
|
|||||||
from . import setup_platform
|
from . import setup_platform
|
||||||
from .const import VEHICLE_DATA_ALT
|
from .const import VEHICLE_DATA_ALT
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
|
||||||
|
|
||||||
ERRORS = [
|
ERRORS = [
|
||||||
(InvalidToken, ConfigEntryState.SETUP_ERROR),
|
(InvalidToken, ConfigEntryState.SETUP_ERROR),
|
||||||
(SubscriptionRequired, ConfigEntryState.SETUP_ERROR),
|
(SubscriptionRequired, ConfigEntryState.SETUP_ERROR),
|
||||||
@ -69,22 +64,6 @@ async def test_devices(
|
|||||||
assert device == snapshot(name=f"{device.identifiers}")
|
assert device == snapshot(name=f"{device.identifiers}")
|
||||||
|
|
||||||
|
|
||||||
async def test_vehicle_refresh_offline(
|
|
||||||
hass: HomeAssistant, mock_vehicle_data: AsyncMock, freezer: FrozenDateTimeFactory
|
|
||||||
) -> None:
|
|
||||||
"""Test coordinator refresh with an error."""
|
|
||||||
entry = await setup_platform(hass, [Platform.CLIMATE])
|
|
||||||
assert entry.state is ConfigEntryState.LOADED
|
|
||||||
mock_vehicle_data.assert_called_once()
|
|
||||||
mock_vehicle_data.reset_mock()
|
|
||||||
|
|
||||||
mock_vehicle_data.side_effect = VehicleOffline
|
|
||||||
freezer.tick(VEHICLE_INTERVAL)
|
|
||||||
async_fire_time_changed(hass)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
mock_vehicle_data.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
|
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
|
||||||
async def test_vehicle_refresh_error(
|
async def test_vehicle_refresh_error(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user