From fc440f310b462f4ae26f1ecd8e713f3a48723f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 30 Apr 2025 18:15:19 +0100 Subject: [PATCH] Add door binary sensor to Whirlpool (#143947) --- .../components/whirlpool/__init__.py | 2 +- .../components/whirlpool/binary_sensor.py | 68 +++++++++++++ tests/components/whirlpool/__init__.py | 13 +++ .../snapshots/test_binary_sensor.ambr | 97 +++++++++++++++++++ .../whirlpool/test_binary_sensor.py | 55 +++++++++++ tests/components/whirlpool/test_climate.py | 7 +- tests/components/whirlpool/test_sensor.py | 14 +-- 7 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/whirlpool/binary_sensor.py create mode 100644 tests/components/whirlpool/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/whirlpool/test_binary_sensor.py diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 3aa85403d12..56cdf52c649 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -17,7 +17,7 @@ from .const import BRANDS_CONF_MAP, CONF_BRAND, DOMAIN, REGIONS_CONF_MAP _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] type WhirlpoolConfigEntry = ConfigEntry[AppliancesManager] diff --git a/homeassistant/components/whirlpool/binary_sensor.py b/homeassistant/components/whirlpool/binary_sensor.py new file mode 100644 index 00000000000..d8ec373f026 --- /dev/null +++ b/homeassistant/components/whirlpool/binary_sensor.py @@ -0,0 +1,68 @@ +"""Binary sensors for the Whirlpool Appliances integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta + +from whirlpool.appliance import Appliance + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import WhirlpoolConfigEntry +from .entity import WhirlpoolEntity + +SCAN_INTERVAL = timedelta(minutes=5) + + +@dataclass(frozen=True, kw_only=True) +class WhirlpoolBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Whirlpool binary sensor entity.""" + + value_fn: Callable[[Appliance], bool | None] + + +WASHER_DRYER_SENSORS: list[WhirlpoolBinarySensorEntityDescription] = [ + WhirlpoolBinarySensorEntityDescription( + key="door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda appliance: appliance.get_door_open(), + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: WhirlpoolConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Config flow entry for Whirlpool binary sensors.""" + entities: list = [] + appliances_manager = config_entry.runtime_data + for washer_dryer in appliances_manager.washer_dryers: + entities.extend( + WhirlpoolBinarySensor(washer_dryer, description) + for description in WASHER_DRYER_SENSORS + ) + async_add_entities(entities) + + +class WhirlpoolBinarySensor(WhirlpoolEntity, BinarySensorEntity): + """A class for the Whirlpool binary sensors.""" + + def __init__( + self, appliance: Appliance, description: WhirlpoolBinarySensorEntityDescription + ) -> None: + """Initialize the washer sensor.""" + super().__init__(appliance, unique_id_suffix=f"-{description.key}") + self.entity_description: WhirlpoolBinarySensorEntityDescription = description + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value_fn(self._appliance) diff --git a/tests/components/whirlpool/__init__.py b/tests/components/whirlpool/__init__.py index ef589092a4b..7d915b91116 100644 --- a/tests/components/whirlpool/__init__.py +++ b/tests/components/whirlpool/__init__.py @@ -1,5 +1,7 @@ """Tests for the Whirlpool Sixth Sense integration.""" +from unittest.mock import MagicMock + from syrupy import SnapshotAssertion from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN @@ -49,3 +51,14 @@ def snapshot_whirlpool_entities( entity_entry = entity_registry.async_get(entity_state.entity_id) assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") assert entity_state == snapshot(name=f"{entity_entry.entity_id}-state") + + +async def trigger_attr_callback( + hass: HomeAssistant, mock_api_instance: MagicMock +) -> None: + """Simulate an update trigger from the API.""" + + for call in mock_api_instance.register_attr_callback.call_args_list: + update_ha_state_cb = call[0][0] + update_ha_state_cb() + await hass.async_block_till_done() diff --git a/tests/components/whirlpool/snapshots/test_binary_sensor.ambr b/tests/components/whirlpool/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..1a902f806cf --- /dev/null +++ b/tests/components/whirlpool/snapshots/test_binary_sensor.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.dryer_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.dryer_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'said_dryer-door', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.dryer_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Dryer Door', + }), + 'context': , + 'entity_id': 'binary_sensor.dryer_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.washer_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washer_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'said_washer-door', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.washer_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Washer Door', + }), + 'context': , + 'entity_id': 'binary_sensor.washer_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/whirlpool/test_binary_sensor.py b/tests/components/whirlpool/test_binary_sensor.py new file mode 100644 index 00000000000..bdd4c05c05d --- /dev/null +++ b/tests/components/whirlpool/test_binary_sensor.py @@ -0,0 +1,55 @@ +"""Test the Whirlpool Binary Sensor domain.""" + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +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.BINARY_SENSOR) + + +@pytest.mark.parametrize( + ("entity_id", "mock_fixture", "mock_method"), + [ + ("binary_sensor.washer_door", "mock_washer_api", "get_door_open"), + ("binary_sensor.dryer_door", "mock_dryer_api", "get_door_open"), + ], +) +async def test_simple_binary_sensors( + hass: HomeAssistant, + entity_id: str, + mock_fixture: str, + mock_method: str, + request: pytest.FixtureRequest, +) -> None: + """Test simple binary sensors states.""" + mock_instance = request.getfixturevalue(mock_fixture) + mock_method = getattr(mock_instance, mock_method) + await init_integration(hass) + + mock_method.return_value = False + await trigger_attr_callback(hass, mock_instance) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + mock_method.return_value = True + await trigger_attr_callback(hass, mock_instance) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + mock_method.return_value = None + await trigger_attr_callback(hass, mock_instance) + state = hass.states.get(entity_id) + assert state.state is STATE_UNKNOWN diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 31ae253031b..e9fb47d1c28 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -39,7 +39,7 @@ 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 +from . import init_integration, snapshot_whirlpool_entities, trigger_attr_callback @pytest.fixture( @@ -60,10 +60,7 @@ async def update_ac_state( mock_aircon_api_instance: MagicMock, ): """Simulate an update trigger from the API.""" - for call in mock_aircon_api_instance.register_attr_callback.call_args_list: - update_ha_state_cb = call[0][0] - update_ha_state_cb() - await hass.async_block_till_done() + await trigger_attr_callback(hass, mock_aircon_api_instance) return hass.states.get(entity_id) diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 2424b37d6f5..9aa88c26123 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -1,7 +1,6 @@ """Test the Whirlpool Sensor domain.""" from datetime import UTC, datetime, timedelta -from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest @@ -14,7 +13,7 @@ from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import as_timestamp, utc_from_timestamp, utcnow -from . import init_integration, snapshot_whirlpool_entities +from . import init_integration, snapshot_whirlpool_entities, trigger_attr_callback from tests.common import async_fire_time_changed, mock_restore_cache_with_extra_data @@ -22,17 +21,6 @@ WASHER_ENTITY_ID_BASE = "sensor.washer" DRYER_ENTITY_ID_BASE = "sensor.dryer" -async def trigger_attr_callback( - hass: HomeAssistant, mock_api_instance: MagicMock -) -> None: - """Simulate an update trigger from the API.""" - - for call in mock_api_instance.register_attr_callback.call_args_list: - update_ha_state_cb = call[0][0] - update_ha_state_cb() - await hass.async_block_till_done() - - # Freeze time for WasherDryerTimeSensor @pytest.mark.freeze_time("2025-05-04 12:00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default")