From dbb726f41f6779f7ae225456412347aea8c46f4d Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 21 Dec 2023 16:34:31 +1000 Subject: [PATCH] Add Select platform to Tessie (#105423) * Add select platform * Add error coverage * Fix case * fix value * Remove virtual key issue * Add TessieSeatHeaterOptions enum and update TessieSeatHeaterSelectEntity options * use ENUM in tests * Porting other fixes * Update entity --- homeassistant/components/tessie/__init__.py | 8 ++- homeassistant/components/tessie/const.py | 9 +++ homeassistant/components/tessie/select.py | 58 +++++++++++++++++ homeassistant/components/tessie/strings.json | 65 ++++++++++++++++++++ tests/components/tessie/test_select.py | 65 ++++++++++++++++++++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tessie/select.py create mode 100644 tests/components/tessie/test_select.py diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 65a695614b4..d34a1335ebc 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -14,7 +14,13 @@ 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, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index b7dcaea4420..43f9b2c719b 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -20,6 +20,15 @@ class TessieStatus(StrEnum): ONLINE = "online" +class TessieSeatHeaterOptions(StrEnum): + """Tessie seat heater options.""" + + OFF = "off" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + + class TessieClimateKeeper(StrEnum): """Tessie Climate Keeper Modes.""" diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py new file mode 100644 index 00000000000..d40abed6478 --- /dev/null +++ b/homeassistant/components/tessie/select.py @@ -0,0 +1,58 @@ +"""Select platform for Tessie integration.""" +from __future__ import annotations + +from tessie_api import set_seat_heat + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, TessieSeatHeaterOptions +from .entity import TessieEntity + +SEAT_HEATERS = { + "climate_state_seat_heater_left": "front_left", + "climate_state_seat_heater_right": "front_right", + "climate_state_seat_heater_rear_left": "rear_left", + "climate_state_seat_heater_rear_center": "rear_center", + "climate_state_seat_heater_rear_right": "rear_right", + "climate_state_seat_heater_third_row_left": "third_row_left", + "climate_state_seat_heater_third_row_right": "third_row_right", +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie select platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + TessieSeatHeaterSelectEntity(coordinator, key) + for coordinator in coordinators + for key in SEAT_HEATERS + if key in coordinator.data + ) + + +class TessieSeatHeaterSelectEntity(TessieEntity, SelectEntity): + """Select entity for current charge.""" + + _attr_options = [ + TessieSeatHeaterOptions.OFF, + TessieSeatHeaterOptions.LOW, + TessieSeatHeaterOptions.MEDIUM, + TessieSeatHeaterOptions.HIGH, + ] + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + return self._attr_options[self._value] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + level = self._attr_options.index(option) + await self.run(set_seat_heat, seat=SEAT_HEATERS[self.key], level=level) + self.set((self.key, level)) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 02f22a6f55a..f1279ab0daf 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -102,6 +102,71 @@ "name": "Passenger temperature setting" } }, + "select": { + "climate_state_seat_heater_left": { + "name": "Seat heater left", + "state": { + "off": "[%key:common::state::off%]", + "low": "Low", + "medium": "Medium", + "high": "High" + } + }, + "climate_state_seat_heater_right": { + "name": "Seat heater right", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::low%]", + "medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]", + "high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]" + } + }, + "climate_state_seat_heater_rear_left": { + "name": "Seat heater rear left", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::low%]", + "medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]", + "high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]" + } + }, + "climate_state_seat_heater_rear_center": { + "name": "Seat heater rear center", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::low%]", + "medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]", + "high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]" + } + }, + "climate_state_seat_heater_rear_right": { + "name": "Seat heater rear right", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::low%]", + "medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]", + "high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]" + } + }, + "climate_state_seat_heater_third_row_left": { + "name": "Seat heater third row left", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::low%]", + "medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]", + "high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]" + } + }, + "climate_state_seat_heater_third_row_right": { + "name": "Seat heater third row right", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::low%]", + "medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]", + "high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]" + } + } + }, "binary_sensor": { "state": { "name": "Status" diff --git a/tests/components/tessie/test_select.py b/tests/components/tessie/test_select.py new file mode 100644 index 00000000000..705e66d3dbb --- /dev/null +++ b/tests/components/tessie/test_select.py @@ -0,0 +1,65 @@ +"""Test the Tessie select platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.tessie.const import TessieSeatHeaterOptions +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .common import ERROR_UNKNOWN, TEST_RESPONSE, setup_platform + + +async def test_select(hass: HomeAssistant) -> None: + """Tests that the select entity is correct.""" + + assert len(hass.states.async_all(SELECT_DOMAIN)) == 0 + + await setup_platform(hass) + + assert len(hass.states.async_all(SELECT_DOMAIN)) == 5 + + entity_id = "select.test_seat_heater_left" + assert hass.states.get(entity_id).state == STATE_OFF + + # Test changing select + with patch( + "homeassistant.components.tessie.select.set_seat_heat", + return_value=TEST_RESPONSE, + ) as mock_set: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: [entity_id], ATTR_OPTION: TessieSeatHeaterOptions.LOW}, + blocking=True, + ) + mock_set.assert_called_once() + assert mock_set.call_args[1]["seat"] == "front_left" + assert mock_set.call_args[1]["level"] == 1 + assert hass.states.get(entity_id).state == TessieSeatHeaterOptions.LOW + + +async def test_errors(hass: HomeAssistant) -> None: + """Tests unknown error is handled.""" + + await setup_platform(hass) + entity_id = "select.test_seat_heater_left" + + # Test setting cover open with unknown error + with patch( + "homeassistant.components.tessie.select.set_seat_heat", + side_effect=ERROR_UNKNOWN, + ) as mock_set, pytest.raises(HomeAssistantError) as error: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: [entity_id], ATTR_OPTION: TessieSeatHeaterOptions.LOW}, + blocking=True, + ) + mock_set.assert_called_once() + assert error.from_exception == ERROR_UNKNOWN