From ec6a052ff519f333e00e9174a7634f49a67f5bc6 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:46:26 -0400 Subject: [PATCH] Add Hot Water+ Level select entity to A. O. Smith integration (#151548) --- homeassistant/components/aosmith/__init__.py | 2 +- homeassistant/components/aosmith/icons.json | 5 ++ homeassistant/components/aosmith/select.py | 70 +++++++++++++++++ homeassistant/components/aosmith/strings.json | 11 +++ tests/components/aosmith/conftest.py | 37 ++++++--- .../aosmith/snapshots/test_device.ambr | 2 +- .../aosmith/snapshots/test_select.ambr | 62 +++++++++++++++ tests/components/aosmith/test_init.py | 1 + tests/components/aosmith/test_select.py | 77 +++++++++++++++++++ 9 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/aosmith/select.py create mode 100644 tests/components/aosmith/snapshots/test_select.ambr create mode 100644 tests/components/aosmith/test_select.py diff --git a/homeassistant/components/aosmith/__init__.py b/homeassistant/components/aosmith/__init__.py index 7593365c573..210993b2203 100644 --- a/homeassistant/components/aosmith/__init__.py +++ b/homeassistant/components/aosmith/__init__.py @@ -16,7 +16,7 @@ from .coordinator import ( AOSmithStatusCoordinator, ) -PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER] +PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR, Platform.WATER_HEATER] async def async_setup_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> bool: diff --git a/homeassistant/components/aosmith/icons.json b/homeassistant/components/aosmith/icons.json index e31a68464ce..a7dcfc4adc9 100644 --- a/homeassistant/components/aosmith/icons.json +++ b/homeassistant/components/aosmith/icons.json @@ -1,5 +1,10 @@ { "entity": { + "select": { + "hot_water_plus_level": { + "default": "mdi:water-plus" + } + }, "sensor": { "hot_water_availability": { "default": "mdi:water-thermometer" diff --git a/homeassistant/components/aosmith/select.py b/homeassistant/components/aosmith/select.py new file mode 100644 index 00000000000..e85bd8b702a --- /dev/null +++ b/homeassistant/components/aosmith/select.py @@ -0,0 +1,70 @@ +"""The select platform for the A. O. Smith integration.""" + +from homeassistant.components.select import SelectEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import AOSmithConfigEntry +from .coordinator import AOSmithStatusCoordinator +from .entity import AOSmithStatusEntity + +HWP_LEVEL_HA_TO_AOSMITH = { + "off": 0, + "level1": 1, + "level2": 2, + "level3": 3, +} +HWP_LEVEL_AOSMITH_TO_HA = {value: key for key, value in HWP_LEVEL_HA_TO_AOSMITH.items()} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AOSmithConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up A. O. Smith select platform.""" + data = entry.runtime_data + + async_add_entities( + AOSmithHotWaterPlusSelectEntity(data.status_coordinator, device.junction_id) + for device in data.status_coordinator.data.values() + if device.supports_hot_water_plus + ) + + +class AOSmithHotWaterPlusSelectEntity(AOSmithStatusEntity, SelectEntity): + """Class for the Hot Water+ select entity.""" + + _attr_translation_key = "hot_water_plus_level" + _attr_options = list(HWP_LEVEL_HA_TO_AOSMITH) + + def __init__(self, coordinator: AOSmithStatusCoordinator, junction_id: str) -> None: + """Initialize the entity.""" + super().__init__(coordinator, junction_id) + self._attr_unique_id = f"hot_water_plus_level_{junction_id}" + + @property + def suggested_object_id(self) -> str | None: + """Override the suggested object id to make '+' get converted to 'plus' in the entity id.""" + return "hot_water_plus_level" + + @property + def current_option(self) -> str | None: + """Return the current Hot Water+ mode.""" + hot_water_plus_level = self.device.status.hot_water_plus_level + return ( + None + if hot_water_plus_level is None + else HWP_LEVEL_AOSMITH_TO_HA.get(hot_water_plus_level) + ) + + async def async_select_option(self, option: str) -> None: + """Set the Hot Water+ mode.""" + aosmith_hwp_level = HWP_LEVEL_HA_TO_AOSMITH[option] + await self.client.update_mode( + junction_id=self.junction_id, + mode=self.device.status.current_mode, + hot_water_plus_level=aosmith_hwp_level, + ) + + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/aosmith/strings.json b/homeassistant/components/aosmith/strings.json index c88b9cab783..fa2d5a67020 100644 --- a/homeassistant/components/aosmith/strings.json +++ b/homeassistant/components/aosmith/strings.json @@ -26,6 +26,17 @@ } }, "entity": { + "select": { + "hot_water_plus_level": { + "name": "Hot Water+ level", + "state": { + "off": "[%key:common::state::off%]", + "level1": "Level 1", + "level2": "Level 2", + "level3": "Level 3" + } + } + }, "sensor": { "hot_water_availability": { "name": "Hot water availability" diff --git a/tests/components/aosmith/conftest.py b/tests/components/aosmith/conftest.py index 2929d743d34..c11d13ff8cd 100644 --- a/tests/components/aosmith/conftest.py +++ b/tests/components/aosmith/conftest.py @@ -29,7 +29,11 @@ FIXTURE_USER_INPUT = { def build_device_fixture( - heat_pump: bool, mode_pending: bool, setpoint_pending: bool, has_vacation_mode: bool + heat_pump: bool, + mode_pending: bool, + setpoint_pending: bool, + has_vacation_mode: bool, + supports_hot_water_plus: bool, ): """Build a fixture for a device.""" supported_modes: list[SupportedOperationModeInfo] = [ @@ -37,7 +41,7 @@ def build_device_fixture( mode=OperationMode.ELECTRIC, original_name="ELECTRIC", has_day_selection=True, - supports_hot_water_plus=False, + supports_hot_water_plus=supports_hot_water_plus, ), ] @@ -47,7 +51,7 @@ def build_device_fixture( mode=OperationMode.HYBRID, original_name="HYBRID", has_day_selection=False, - supports_hot_water_plus=False, + supports_hot_water_plus=supports_hot_water_plus, ) ) supported_modes.append( @@ -55,7 +59,7 @@ def build_device_fixture( mode=OperationMode.HEAT_PUMP, original_name="HEAT_PUMP", has_day_selection=False, - supports_hot_water_plus=False, + supports_hot_water_plus=supports_hot_water_plus, ) ) @@ -69,17 +73,18 @@ def build_device_fixture( ) ) - device_type = ( - DeviceType.NEXT_GEN_HEAT_PUMP if heat_pump else DeviceType.RE3_CONNECTED - ) - current_mode = OperationMode.HEAT_PUMP if heat_pump else OperationMode.ELECTRIC - model = "HPTS-50 200 202172000" if heat_pump else "EE12-50H55DVF 100,3806368" + if heat_pump and supports_hot_water_plus: + device_type = DeviceType.RE3_PREMIUM + elif heat_pump: + device_type = DeviceType.NEXT_GEN_HEAT_PUMP + else: + device_type = DeviceType.RE3_CONNECTED return Device( brand="aosmith", - model=model, + model="Example model", device_type=device_type, dsn="dsn", junction_id="junctionId", @@ -87,7 +92,7 @@ def build_device_fixture( serial="serial", install_location="Basement", supported_modes=supported_modes, - supports_hot_water_plus=False, + supports_hot_water_plus=supports_hot_water_plus, status=DeviceStatus( firmware_version="2.14", is_online=True, @@ -98,7 +103,7 @@ def build_device_fixture( temperature_setpoint_previous=130, temperature_setpoint_maximum=130, hot_water_status=90, - hot_water_plus_level=None, + hot_water_plus_level=1 if supports_hot_water_plus else None, ), ) @@ -165,6 +170,12 @@ def get_devices_fixture_has_vacation_mode() -> bool: return True +@pytest.fixture +def get_devices_fixture_supports_hot_water_plus() -> bool: + """Return whether to include hot water plus support in the get_devices fixture.""" + return False + + @pytest.fixture async def mock_client( hass: HomeAssistant, @@ -172,6 +183,7 @@ async def mock_client( get_devices_fixture_mode_pending: bool, get_devices_fixture_setpoint_pending: bool, get_devices_fixture_has_vacation_mode: bool, + get_devices_fixture_supports_hot_water_plus: bool, ) -> Generator[MagicMock]: """Return a mocked client.""" get_devices_fixture = [ @@ -180,6 +192,7 @@ async def mock_client( mode_pending=get_devices_fixture_mode_pending, setpoint_pending=get_devices_fixture_setpoint_pending, has_vacation_mode=get_devices_fixture_has_vacation_mode, + supports_hot_water_plus=get_devices_fixture_supports_hot_water_plus, ) ] get_all_device_info_fixture = await async_load_json_object_fixture( diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index c4c1b0b1b93..057619a0246 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -20,7 +20,7 @@ 'labels': set({ }), 'manufacturer': 'A. O. Smith', - 'model': 'HPTS-50 200 202172000', + 'model': 'Example model', 'model_id': None, 'name': 'My water heater', 'name_by_user': None, diff --git a/tests/components/aosmith/snapshots/test_select.ambr b/tests/components/aosmith/snapshots/test_select.ambr new file mode 100644 index 00000000000..9e0c10319c3 --- /dev/null +++ b/tests/components/aosmith/snapshots/test_select.ambr @@ -0,0 +1,62 @@ +# serializer version: 1 +# name: test_state[True][select.my_water_heater_hot_water_plus_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'level1', + 'level2', + 'level3', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.my_water_heater_hot_water_plus_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hot Water+ level', + 'platform': 'aosmith', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'hot_water_plus_level', + 'unique_id': 'hot_water_plus_level_junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[True][select.my_water_heater_hot_water_plus_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My water heater Hot Water+ level', + 'options': list([ + 'off', + 'level1', + 'level2', + 'level3', + ]), + }), + 'context': , + 'entity_id': 'select.my_water_heater_hot_water_plus_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'level1', + }) +# --- diff --git a/tests/components/aosmith/test_init.py b/tests/components/aosmith/test_init.py index 940b0cbc6b5..975e6b2a061 100644 --- a/tests/components/aosmith/test_init.py +++ b/tests/components/aosmith/test_init.py @@ -56,6 +56,7 @@ async def test_config_entry_not_ready_get_energy_use_data_error( mode_pending=False, setpoint_pending=False, has_vacation_mode=True, + supports_hot_water_plus=False, ) ] diff --git a/tests/components/aosmith/test_select.py b/tests/components/aosmith/test_select.py new file mode 100644 index 00000000000..75444b7d8c9 --- /dev/null +++ b/tests/components/aosmith/test_select.py @@ -0,0 +1,77 @@ +"""Tests for the select platform of the A. O. Smith integration.""" + +from collections.abc import AsyncGenerator +from unittest.mock import MagicMock, patch + +from py_aosmith.models import OperationMode +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.SELECT]): + yield + + +@pytest.mark.parametrize( + ("get_devices_fixture_supports_hot_water_plus"), + [True], +) +async def test_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test the state of the select entity.""" + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) + + +@pytest.mark.parametrize( + ("get_devices_fixture_supports_hot_water_plus"), + [True], +) +@pytest.mark.parametrize( + ("hass_level", "aosmith_level"), + [ + ("off", 0), + ("level1", 1), + ("level2", 2), + ("level3", 3), + ], +) +async def test_set_hot_water_plus_level( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + hass_level: str, + aosmith_level: int, +) -> None: + """Test setting the Hot Water+ level.""" + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_water_heater_hot_water_plus_level", + ATTR_OPTION: hass_level, + }, + ) + await hass.async_block_till_done() + + mock_client.update_mode.assert_called_once_with( + junction_id="junctionId", + mode=OperationMode.HEAT_PUMP, + hot_water_plus_level=aosmith_level, + )