Add door binary sensor to Whirlpool (#143947)

This commit is contained in:
Abílio Costa 2025-04-30 18:15:19 +01:00 committed by GitHub
parent 4d9ab42ab5
commit fc440f310b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 237 additions and 19 deletions

View File

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

View File

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

View File

@ -1,5 +1,7 @@
"""Tests for the Whirlpool Sixth Sense integration.""" """Tests for the Whirlpool Sixth Sense integration."""
from unittest.mock import MagicMock
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN 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) entity_entry = entity_registry.async_get(entity_state.entity_id)
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
assert entity_state == snapshot(name=f"{entity_entry.entity_id}-state") 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()

View File

@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'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': <ANY>,
'entity_id': 'binary_sensor.dryer_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.washer_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'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': <ANY>,
'entity_id': 'binary_sensor.washer_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

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

View File

@ -39,7 +39,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er 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( @pytest.fixture(
@ -60,10 +60,7 @@ async def update_ac_state(
mock_aircon_api_instance: MagicMock, mock_aircon_api_instance: MagicMock,
): ):
"""Simulate an update trigger from the API.""" """Simulate an update trigger from the API."""
for call in mock_aircon_api_instance.register_attr_callback.call_args_list: await trigger_attr_callback(hass, mock_aircon_api_instance)
update_ha_state_cb = call[0][0]
update_ha_state_cb()
await hass.async_block_till_done()
return hass.states.get(entity_id) return hass.states.get(entity_id)

View File

@ -1,7 +1,6 @@
"""Test the Whirlpool Sensor domain.""" """Test the Whirlpool Sensor domain."""
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -14,7 +13,7 @@ from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import as_timestamp, utc_from_timestamp, utcnow 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 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" 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 # Freeze time for WasherDryerTimeSensor
@pytest.mark.freeze_time("2025-05-04 12:00:00") @pytest.mark.freeze_time("2025-05-04 12:00:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("entity_registry_enabled_by_default")