diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index fdffaffa538..65a695614b4 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import TessieDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 978e594b68f..02f22a6f55a 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -160,6 +160,23 @@ "vehicle_state_tpms_soft_warning_rr": { "name": "Tire pressure warning rear right" } + }, + "switch": { + "charge_state_charge_enable_request": { + "name": "Charge" + }, + "climate_state_defrost_mode": { + "name": "Defrost mode" + }, + "vehicle_state_sentry_mode": { + "name": "Sentry mode" + }, + "vehicle_state_valet_mode": { + "name": "Valet mode" + }, + "climate_state_steering_wheel_heater": { + "name": "Steering wheel heater" + } } } } diff --git a/homeassistant/components/tessie/switch.py b/homeassistant/components/tessie/switch.py new file mode 100644 index 00000000000..216f48da348 --- /dev/null +++ b/homeassistant/components/tessie/switch.py @@ -0,0 +1,121 @@ +"""Switch platform for Tessie integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from tessie_api import ( + disable_sentry_mode, + disable_valet_mode, + enable_sentry_mode, + enable_valet_mode, + start_charging, + start_defrost, + start_steering_wheel_heater, + stop_charging, + stop_defrost, + stop_steering_wheel_heater, +) + +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 .const import DOMAIN +from .coordinator import TessieDataUpdateCoordinator +from .entity import TessieEntity + + +@dataclass(frozen=True, kw_only=True) +class TessieSwitchEntityDescription(SwitchEntityDescription): + """Describes Tessie Switch entity.""" + + on_func: Callable + off_func: Callable + device_class: SwitchDeviceClass = SwitchDeviceClass.SWITCH + + +DESCRIPTIONS: tuple[TessieSwitchEntityDescription, ...] = ( + TessieSwitchEntityDescription( + key="charge_state_charge_enable_request", + on_func=start_charging, + off_func=stop_charging, + icon="mdi:ev-station", + ), + TessieSwitchEntityDescription( + key="climate_state_defrost_mode", + on_func=start_defrost, + off_func=stop_defrost, + icon="mdi:snowflake", + ), + TessieSwitchEntityDescription( + key="vehicle_state_sentry_mode", + on_func=enable_sentry_mode, + off_func=disable_sentry_mode, + icon="mdi:shield-car", + ), + TessieSwitchEntityDescription( + key="vehicle_state_valet_mode", + on_func=enable_valet_mode, + off_func=disable_valet_mode, + icon="mdi:car-key", + ), + TessieSwitchEntityDescription( + key="climate_state_steering_wheel_heater", + on_func=start_steering_wheel_heater, + off_func=stop_steering_wheel_heater, + icon="mdi:steering", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie Switch platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + TessieSwitchEntity(coordinator, description) + for coordinator in coordinators + for description in DESCRIPTIONS + if description.key in coordinator.data + ] + ) + + +class TessieSwitchEntity(TessieEntity, SwitchEntity): + """Base class for Tessie Switch.""" + + entity_description: TessieSwitchEntityDescription + + def __init__( + self, + coordinator: TessieDataUpdateCoordinator, + description: TessieSwitchEntityDescription, + ) -> None: + """Initialize the Switch.""" + super().__init__(coordinator, description.key) + self.entity_description = description + + @property + def is_on(self) -> bool: + """Return the state of the Switch.""" + return self._value + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Switch.""" + await self.run(self.entity_description.on_func) + self.set((self.entity_description.key, True)) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Switch.""" + await self.run(self.entity_description.off_func) + self.set((self.entity_description.key, False)) diff --git a/tests/components/tessie/test_switch.py b/tests/components/tessie/test_switch.py new file mode 100644 index 00000000000..7ecd51bbd54 --- /dev/null +++ b/tests/components/tessie/test_switch.py @@ -0,0 +1,53 @@ +"""Test the Tessie switch platform.""" +from unittest.mock import patch + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.components.tessie.switch import DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import HomeAssistant + +from .common import TEST_VEHICLE_STATE_ONLINE, setup_platform + + +async def test_switches(hass: HomeAssistant) -> None: + """Tests that the switches are correct.""" + + assert len(hass.states.async_all("switch")) == 0 + + await setup_platform(hass) + + assert len(hass.states.async_all("switch")) == len(DESCRIPTIONS) + + assert (hass.states.get("switch.test_charge").state == STATE_ON) == ( + TEST_VEHICLE_STATE_ONLINE["charge_state"]["charge_enable_request"] + ) + assert (hass.states.get("switch.test_sentry_mode").state == STATE_ON) == ( + TEST_VEHICLE_STATE_ONLINE["vehicle_state"]["sentry_mode"] + ) + + with patch( + "homeassistant.components.tessie.entity.TessieEntity.run", + return_value=True, + ) as mock_run: + # Test Switch On + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ["switch.test_charge"]}, + blocking=True, + ) + mock_run.assert_called_once() + mock_run.reset_mock() + + # Test Switch Off + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ["switch.test_charge"]}, + blocking=True, + ) + mock_run.assert_called_once()