Compare commits

...

1 Commits

Author SHA1 Message Date
abmantis
3e4a6d9092 Add refrigerator temperature level select to whirlpool 2026-02-02 23:27:28 +00:00
7 changed files with 277 additions and 2 deletions

View File

@@ -17,7 +17,7 @@ from .const import BRANDS_CONF_MAP, CONF_BRAND, DOMAIN, REGIONS_CONF_MAP
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SELECT, Platform.SENSOR]
type WhirlpoolConfigEntry = ConfigEntry[AppliancesManager]

View File

@@ -0,0 +1,91 @@
"""The select platform for Whirlpool Appliances."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
from typing import Final, override
from whirlpool.appliance import Appliance
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import WhirlpoolConfigEntry
from .const import DOMAIN
from .entity import WhirlpoolEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class WhirlpoolSelectDescription(SelectEntityDescription):
"""Class describing Whirlpool select entities."""
value_fn: Callable[[Appliance], str | None]
set_fn: Callable[[Appliance, str], Awaitable[bool]]
REFRIGERATOR_DESCRIPTIONS: Final[tuple[WhirlpoolSelectDescription, ...]] = (
WhirlpoolSelectDescription(
key="refrigerator_temperature",
translation_key="refrigerator_temperature",
options=["-4", "-2", "0", "3", "5"],
value_fn=lambda fridge: str(val)
if (val := fridge.get_offset_temp()) is not None
else None,
set_fn=lambda fridge, option: fridge.set_offset_temp(int(option)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: WhirlpoolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the select platform."""
appliances_manager = config_entry.runtime_data
async_add_entities(
[
WhirlpoolSelectEntity(refrigerator, description)
for refrigerator in appliances_manager.refrigerators
for description in REFRIGERATOR_DESCRIPTIONS
]
)
class WhirlpoolSelectEntity(WhirlpoolEntity, SelectEntity):
"""Whirlpool select entity."""
def __init__(
self, appliance: Appliance, description: WhirlpoolSelectDescription
) -> None:
"""Initialize the select entity."""
super().__init__(appliance, unique_id_suffix=f"-{description.key}")
self.entity_description: WhirlpoolSelectDescription = description
@override
@property
def current_option(self) -> str | None:
"""Retrieve currently selected option."""
return self.entity_description.value_fn(self._appliance)
@override
async def async_select_option(self, option: str) -> None:
"""Set the selected option."""
try:
WhirlpoolSelectEntity._check_service_request(
await self.entity_description.set_fn(self._appliance, option)
)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_value_set",
) from err

View File

@@ -46,6 +46,11 @@
}
},
"entity": {
"select": {
"refrigerator_temperature": {
"name": "Temperature"
}
},
"sensor": {
"dryer_state": {
"name": "[%key:component::whirlpool::entity::sensor::washer_state::name%]",
@@ -211,6 +216,9 @@
"appliances_fetch_failed": {
"message": "Failed to fetch appliances"
},
"invalid_value_set": {
"message": "Invalid value provided"
},
"request_failed": {
"message": "Request failed"
}

View File

@@ -4,7 +4,7 @@ from unittest import mock
from unittest.mock import Mock
import pytest
from whirlpool import aircon, appliancesmanager, auth, dryer, oven, washer
from whirlpool import aircon, appliancesmanager, auth, dryer, oven, refrigerator, washer
from whirlpool.backendselector import Brand, Region
from .const import MOCK_SAID1, MOCK_SAID2
@@ -55,6 +55,7 @@ def fixture_mock_appliances_manager_api(
mock_dryer_api,
mock_oven_single_cavity_api,
mock_oven_dual_cavity_api,
mock_refrigerator_api,
):
"""Set up AppliancesManager fixture."""
with (
@@ -77,6 +78,7 @@ def fixture_mock_appliances_manager_api(
mock_oven_single_cavity_api,
mock_oven_dual_cavity_api,
]
mock_appliances_manager.return_value.refrigerators = [mock_refrigerator_api]
yield mock_appliances_manager
@@ -203,3 +205,15 @@ def mock_oven_dual_cavity_api():
mock_oven.get_temp.return_value = 180
mock_oven.get_target_temp.return_value = 200
return mock_oven
@pytest.fixture
def mock_refrigerator_api():
"""Get a mock of a refrigerator."""
mock_refrigerator = Mock(spec=refrigerator.Refrigerator, said="said_refrigerator")
mock_refrigerator.name = "Beer fridge"
mock_refrigerator.appliance_info = Mock(
data_model="refrigerator", category="refrigerator", model_number="12345"
)
mock_refrigerator.get_offset_temp.return_value = 0
return mock_refrigerator

View File

@@ -0,0 +1,65 @@
# serializer version: 1
# name: test_all_entities[select.beer_fridge_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'-4',
'-2',
'0',
'3',
'5',
]),
}),
'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.beer_fridge_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Temperature',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'whirlpool',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'refrigerator_temperature',
'unique_id': 'said_refrigerator-refrigerator_temperature',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[select.beer_fridge_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Beer fridge Temperature',
'options': list([
'-4',
'-2',
'0',
'3',
'5',
]),
}),
'context': <ANY>,
'entity_id': 'select.beer_fridge_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---

View File

@@ -84,6 +84,7 @@ async def test_setup_no_appliances(
mock_appliances_manager_api.return_value.washers = []
mock_appliances_manager_api.return_value.dryers = []
mock_appliances_manager_api.return_value.ovens = []
mock_appliances_manager_api.return_value.refrigerators = []
await init_integration(hass)
assert len(hass.states.async_all()) == 0

View File

@@ -0,0 +1,96 @@
"""Test the Whirlpool select domain."""
from unittest.mock import MagicMock
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import ATTR_OPTION, DOMAIN as SELECT_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import init_integration, snapshot_whirlpool_entities, trigger_attr_callback
async def test_all_entities(
hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry
) -> None:
"""Test all entities."""
await init_integration(hass)
snapshot_whirlpool_entities(hass, entity_registry, snapshot, Platform.SELECT)
@pytest.mark.parametrize(
(
"entity_id",
"mock_fixture",
"mock_getter_method_name",
"mock_setter_method_name",
"values",
),
[
(
"select.beer_fridge_temperature",
"mock_refrigerator_api",
"get_offset_temp",
"set_offset_temp",
[(-4, "-4"), (-2, "-2"), (0, "0"), (3, "3"), (5, "5")],
),
],
)
async def test_select_entities(
hass: HomeAssistant,
entity_id: str,
mock_fixture: str,
mock_getter_method_name: str,
mock_setter_method_name: str,
values: list[tuple[int, str]],
request: pytest.FixtureRequest,
) -> None:
"""Test reading and setting select options."""
await init_integration(hass)
mock_instance = request.getfixturevalue(mock_fixture)
# Test reading current option
mock_getter_method = getattr(mock_instance, mock_getter_method_name)
for raw_value, expected_state in values:
mock_getter_method.return_value = raw_value
await trigger_attr_callback(hass, mock_instance)
state = hass.states.get(entity_id)
assert state is not None
assert state.state == expected_state
# Test changing option
mock_setter_method = getattr(mock_instance, mock_setter_method_name)
for raw_value, selected_option in values:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: selected_option},
blocking=True,
)
assert mock_setter_method.call_count == 1
mock_setter_method.assert_called_with(raw_value)
mock_setter_method.reset_mock()
async def test_select_option_value_error(
hass: HomeAssistant, mock_refrigerator_api: MagicMock
) -> None:
"""Test handling of ValueError exception when selecting an option."""
await init_integration(hass)
mock_refrigerator_api.set_offset_temp.side_effect = ValueError
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.beer_fridge_temperature",
ATTR_OPTION: "something",
},
blocking=True,
)