Add Hot Water+ Level select entity to A. O. Smith integration (#151548)

pull/152226/head
Brandon Rothweiler 2025-09-12 18:46:26 -04:00 committed by GitHub
parent c91d64e04d
commit ec6a052ff5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 253 additions and 14 deletions

View File

@ -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:

View File

@ -1,5 +1,10 @@
{
"entity": {
"select": {
"hot_water_plus_level": {
"default": "mdi:water-plus"
}
},
"sensor": {
"hot_water_availability": {
"default": "mdi:water-thermometer"

View File

@ -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()

View File

@ -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"

View File

@ -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(

View File

@ -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,

View File

@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'select.my_water_heater_hot_water_plus_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'level1',
})
# ---

View File

@ -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,
)
]

View File

@ -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,
)