Add switch platform to Teslemetry (#117482)

* Add switch platform

* Add tests

* Add test

* Fixes

* ruff

* Rename to storm watch

* Remove valet

* Apply suggestions from code review

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* ruff

* Review feedback

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
pull/117996/head
Brett Adams 2024-05-23 22:28:18 +10:00 committed by GitHub
parent bc51a4c524
commit 880b315890
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 952 additions and 1 deletions

View File

@ -30,6 +30,7 @@ PLATFORMS: Final = [
Platform.CLIMATE,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@ -135,6 +135,41 @@
"wall_connector_state": {
"default": "mdi:ev-station"
}
},
"switch": {
"charge_state_user_charge_enable_request": {
"default": "mdi:ev-station"
},
"climate_state_auto_seat_climate_left": {
"default": "mdi:car-seat-heater",
"state": {
"off": "mdi:car-seat"
}
},
"climate_state_auto_seat_climate_right": {
"default": "mdi:car-seat-heater",
"state": {
"off": "mdi:car-seat"
}
},
"climate_state_auto_steering_wheel_heat": {
"default": "mdi:steering"
},
"climate_state_defrost_mode": {
"default": "mdi:snowflake-melt"
},
"components_disallow_charge_from_grid_with_solar_installed": {
"state": {
"false": "mdi:transmission-tower",
"true": "mdi:solar-power"
}
},
"vehicle_state_sentry_mode": {
"default": "mdi:shield-car"
},
"vehicle_state_valet_mode": {
"default": "mdi:speedometer-slow"
}
}
}
}

View File

@ -273,6 +273,35 @@
"wall_connector_state": {
"name": "State code"
}
},
"switch": {
"charge_state_user_charge_enable_request": {
"name": "Charge"
},
"climate_state_auto_seat_climate_left": {
"name": "Auto seat climate left"
},
"climate_state_auto_seat_climate_right": {
"name": "Auto seat climate right"
},
"climate_state_auto_steering_wheel_heat": {
"name": "Auto steering wheel heater"
},
"climate_state_defrost_mode": {
"name": "Defrost"
},
"components_disallow_charge_from_grid_with_solar_installed": {
"name": "Allow charging from grid"
},
"user_settings_storm_mode_enabled": {
"name": "Storm watch"
},
"vehicle_state_sentry_mode": {
"name": "Sentry mode"
},
"vehicle_state_valet_mode": {
"name": "Valet mode"
}
}
}
}

View File

@ -0,0 +1,257 @@
"""Switch platform for Teslemetry integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from itertools import chain
from typing import Any
from tesla_fleet_api.const import Scope, Seat
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity
from .models import TeslemetryEnergyData, TeslemetryVehicleData
@dataclass(frozen=True, kw_only=True)
class TeslemetrySwitchEntityDescription(SwitchEntityDescription):
"""Describes Teslemetry Switch entity."""
on_func: Callable
off_func: Callable
scopes: list[Scope]
VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
TeslemetrySwitchEntityDescription(
key="vehicle_state_sentry_mode",
on_func=lambda api: api.set_sentry_mode(on=True),
off_func=lambda api: api.set_sentry_mode(on=False),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_auto_seat_climate_left",
on_func=lambda api: api.remote_auto_seat_climate_request(Seat.FRONT_LEFT, True),
off_func=lambda api: api.remote_auto_seat_climate_request(
Seat.FRONT_LEFT, False
),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_auto_seat_climate_right",
on_func=lambda api: api.remote_auto_seat_climate_request(
Seat.FRONT_RIGHT, True
),
off_func=lambda api: api.remote_auto_seat_climate_request(
Seat.FRONT_RIGHT, False
),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_auto_steering_wheel_heat",
on_func=lambda api: api.remote_auto_steering_wheel_heat_climate_request(
on=True
),
off_func=lambda api: api.remote_auto_steering_wheel_heat_climate_request(
on=False
),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_defrost_mode",
on_func=lambda api: api.set_preconditioning_max(on=True, manual_override=False),
off_func=lambda api: api.set_preconditioning_max(
on=False, manual_override=False
),
scopes=[Scope.VEHICLE_CMDS],
),
)
VEHICLE_CHARGE_DESCRIPTION = TeslemetrySwitchEntityDescription(
key="charge_state_user_charge_enable_request",
on_func=lambda api: api.charge_start(),
off_func=lambda api: api.charge_stop(),
scopes=[Scope.VEHICLE_CMDS, Scope.VEHICLE_CHARGING_CMDS],
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry Switch platform from a config entry."""
async_add_entities(
chain(
(
TeslemetryVehicleSwitchEntity(
vehicle, description, entry.runtime_data.scopes
)
for vehicle in entry.runtime_data.vehicles
for description in VEHICLE_DESCRIPTIONS
),
(
TeslemetryChargeSwitchEntity(
vehicle, VEHICLE_CHARGE_DESCRIPTION, entry.runtime_data.scopes
)
for vehicle in entry.runtime_data.vehicles
),
(
TeslemetryChargeFromGridSwitchEntity(
energysite,
entry.runtime_data.scopes,
)
for energysite in entry.runtime_data.energysites
if energysite.info_coordinator.data.get("components_battery")
and energysite.info_coordinator.data.get("components_solar")
),
(
TeslemetryStormModeSwitchEntity(energysite, entry.runtime_data.scopes)
for energysite in entry.runtime_data.energysites
if energysite.info_coordinator.data.get("components_storm_mode_capable")
),
)
)
class TeslemetrySwitchEntity(SwitchEntity):
"""Base class for all Teslemetry switch entities."""
_attr_device_class = SwitchDeviceClass.SWITCH
entity_description: TeslemetrySwitchEntityDescription
class TeslemetryVehicleSwitchEntity(TeslemetryVehicleEntity, TeslemetrySwitchEntity):
"""Base class for Teslemetry vehicle switch entities."""
def __init__(
self,
data: TeslemetryVehicleData,
description: TeslemetrySwitchEntityDescription,
scopes: list[Scope],
) -> None:
"""Initialize the Switch."""
super().__init__(data, description.key)
self.entity_description = description
self.scoped = any(scope in scopes for scope in description.scopes)
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
if self._value is None:
self._attr_is_on = None
else:
self._attr_is_on = bool(self._value)
async def async_turn_on(self, **kwargs: Any) -> None:
"""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))
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.wake_up_if_asleep()
await self.handle_command(self.entity_description.off_func(self.api))
self._attr_is_on = False
self.async_write_ha_state()
class TeslemetryChargeSwitchEntity(TeslemetryVehicleSwitchEntity):
"""Entity class for Teslemetry charge switch."""
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
if self._value is None:
self._attr_is_on = self.get("charge_state_charge_enable_request")
else:
self._attr_is_on = self._value
class TeslemetryChargeFromGridSwitchEntity(
TeslemetryEnergyInfoEntity, TeslemetrySwitchEntity
):
"""Entity class for Charge From Grid switch."""
def __init__(
self,
data: TeslemetryEnergyData,
scopes: list[Scope],
) -> None:
"""Initialize the Switch."""
self.scoped = Scope.ENERGY_CMDS in scopes
super().__init__(
data, "components_disallow_charge_from_grid_with_solar_installed"
)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
# When disallow_charge_from_grid_with_solar_installed is missing, its Off.
# But this sensor is flipped to match how the Tesla app works.
self._attr_is_on = not self.get(self.key, False)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Switch."""
self.raise_for_scope()
await self.handle_command(
self.api.grid_import_export(
disallow_charge_from_grid_with_solar_installed=False
)
)
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.grid_import_export(
disallow_charge_from_grid_with_solar_installed=True
)
)
self._attr_is_on = False
self.async_write_ha_state()
class TeslemetryStormModeSwitchEntity(
TeslemetryEnergyInfoEntity, TeslemetrySwitchEntity
):
"""Entity class for Storm Mode switch."""
def __init__(
self,
data: TeslemetryEnergyData,
scopes: list[Scope],
) -> None:
"""Initialize the Switch."""
super().__init__(data, "user_settings_storm_mode_enabled")
self.scoped = Scope.ENERGY_CMDS in scopes
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_is_on = bool(self._value)
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))
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))
self._attr_is_on = False
self.async_write_ha_state()

View File

@ -69,7 +69,7 @@
"timestamp": null,
"trip_charging": false,
"usable_battery_level": 77,
"user_charge_enable_request": null
"user_charge_enable_request": true
},
"climate_state": {
"allow_cabin_overheat_protection": true,

View File

@ -0,0 +1,489 @@
# serializer version: 1
# name: test_switch[switch.energy_site_allow_charging_from_grid-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.energy_site_allow_charging_from_grid',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Allow charging from grid',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'components_disallow_charge_from_grid_with_solar_installed',
'unique_id': '123456-components_disallow_charge_from_grid_with_solar_installed',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.energy_site_allow_charging_from_grid-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Energy Site Allow charging from grid',
}),
'context': <ANY>,
'entity_id': 'switch.energy_site_allow_charging_from_grid',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch[switch.energy_site_storm_watch-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.energy_site_storm_watch',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Storm watch',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'user_settings_storm_mode_enabled',
'unique_id': '123456-user_settings_storm_mode_enabled',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.energy_site_storm_watch-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Energy Site Storm watch',
}),
'context': <ANY>,
'entity_id': 'switch.energy_site_storm_watch',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch[switch.test_auto_seat_climate_left-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_auto_seat_climate_left',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Auto seat climate left',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'climate_state_auto_seat_climate_left',
'unique_id': 'VINVINVIN-climate_state_auto_seat_climate_left',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.test_auto_seat_climate_left-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Auto seat climate left',
}),
'context': <ANY>,
'entity_id': 'switch.test_auto_seat_climate_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch[switch.test_auto_seat_climate_right-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_auto_seat_climate_right',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Auto seat climate right',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'climate_state_auto_seat_climate_right',
'unique_id': 'VINVINVIN-climate_state_auto_seat_climate_right',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.test_auto_seat_climate_right-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Auto seat climate right',
}),
'context': <ANY>,
'entity_id': 'switch.test_auto_seat_climate_right',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch[switch.test_auto_steering_wheel_heater-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_auto_steering_wheel_heater',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Auto steering wheel heater',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'climate_state_auto_steering_wheel_heat',
'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.test_auto_steering_wheel_heater-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Auto steering wheel heater',
}),
'context': <ANY>,
'entity_id': 'switch.test_auto_steering_wheel_heater',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch[switch.test_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_charge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Charge',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charge_state_user_charge_enable_request',
'unique_id': 'VINVINVIN-charge_state_user_charge_enable_request',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.test_charge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Charge',
}),
'context': <ANY>,
'entity_id': 'switch.test_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch[switch.test_defrost-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_defrost',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Defrost',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'climate_state_defrost_mode',
'unique_id': 'VINVINVIN-climate_state_defrost_mode',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.test_defrost-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Defrost',
}),
'context': <ANY>,
'entity_id': 'switch.test_defrost',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch[switch.test_sentry_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_sentry_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Sentry mode',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'vehicle_state_sentry_mode',
'unique_id': 'VINVINVIN-vehicle_state_sentry_mode',
'unit_of_measurement': None,
})
# ---
# name: test_switch[switch.test_sentry_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Sentry mode',
}),
'context': <ANY>,
'entity_id': 'switch.test_sentry_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_alt[switch.energy_site_allow_charging_from_grid-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Energy Site Allow charging from grid',
}),
'context': <ANY>,
'entity_id': 'switch.energy_site_allow_charging_from_grid',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_alt[switch.energy_site_storm_watch-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Energy Site Storm watch',
}),
'context': <ANY>,
'entity_id': 'switch.energy_site_storm_watch',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_alt[switch.test_auto_seat_climate_left-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Auto seat climate left',
}),
'context': <ANY>,
'entity_id': 'switch.test_auto_seat_climate_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_alt[switch.test_auto_seat_climate_right-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Auto seat climate right',
}),
'context': <ANY>,
'entity_id': 'switch.test_auto_seat_climate_right',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_alt[switch.test_auto_steering_wheel_heater-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Auto steering wheel heater',
}),
'context': <ANY>,
'entity_id': 'switch.test_auto_steering_wheel_heater',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_alt[switch.test_charge-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Charge',
}),
'context': <ANY>,
'entity_id': 'switch.test_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_alt[switch.test_defrost-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Defrost',
}),
'context': <ANY>,
'entity_id': 'switch.test_defrost',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_alt[switch.test_sentry_mode-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Sentry mode',
}),
'context': <ANY>,
'entity_id': 'switch.test_sentry_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,140 @@
"""Test the Teslemetry switch platform."""
from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from tesla_fleet_api.exceptions import VehicleOffline
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, assert_entities_alt, setup_platform
from .const import COMMAND_OK, VEHICLE_DATA_ALT
async def test_switch(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests that the switch entities are correct."""
entry = await setup_platform(hass, [Platform.SWITCH])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_switch_alt(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_vehicle_data,
) -> None:
"""Tests that the switch entities are correct."""
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
entry = await setup_platform(hass, [Platform.SWITCH])
assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot)
async def test_switch_offline(
hass: HomeAssistant,
mock_vehicle_data,
) -> None:
"""Tests that the switch entities are correct when offline."""
mock_vehicle_data.side_effect = VehicleOffline
await setup_platform(hass, [Platform.SWITCH])
state = hass.states.get("switch.test_auto_seat_climate_left")
assert state.state == STATE_UNKNOWN
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("name", "on", "off"),
[
("test_charge", "VehicleSpecific.charge_start", "VehicleSpecific.charge_stop"),
(
"test_auto_seat_climate_left",
"VehicleSpecific.remote_auto_seat_climate_request",
"VehicleSpecific.remote_auto_seat_climate_request",
),
(
"test_auto_seat_climate_right",
"VehicleSpecific.remote_auto_seat_climate_request",
"VehicleSpecific.remote_auto_seat_climate_request",
),
(
"test_auto_steering_wheel_heater",
"VehicleSpecific.remote_auto_steering_wheel_heat_climate_request",
"VehicleSpecific.remote_auto_steering_wheel_heat_climate_request",
),
(
"test_defrost",
"VehicleSpecific.set_preconditioning_max",
"VehicleSpecific.set_preconditioning_max",
),
(
"energy_site_storm_watch",
"EnergySpecific.storm_mode",
"EnergySpecific.storm_mode",
),
(
"energy_site_allow_charging_from_grid",
"EnergySpecific.grid_import_export",
"EnergySpecific.grid_import_export",
),
(
"test_sentry_mode",
"VehicleSpecific.set_sentry_mode",
"VehicleSpecific.set_sentry_mode",
),
],
)
async def test_switch_services(
hass: HomeAssistant, name: str, on: str, off: str
) -> None:
"""Tests that the switch service calls work."""
await setup_platform(hass, [Platform.SWITCH])
entity_id = f"switch.{name}"
with patch(
f"homeassistant.components.teslemetry.{on}",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == STATE_ON
call.assert_called_once()
with patch(
f"homeassistant.components.teslemetry.{off}",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
call.assert_called_once()