Home Connect entities availability based on the connected state of the appliance (#136951)

* Base the entity availability on the connected state of the appliance

* cache `ha_id`

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Inlcude coordinator `available` property at entity

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
J. Diego Rodríguez Royo 2025-02-01 12:46:49 +01:00 committed by GitHub
parent efcfd97d1b
commit 63ab13681a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 433 additions and 11 deletions

View File

@ -121,9 +121,10 @@ class HomeConnectCoordinator(
while True:
try:
async for event_message in self.client.stream_all_events():
event_message_ha_id = event_message.ha_id
match event_message.type:
case EventType.STATUS:
statuses = self.data[event_message.ha_id].status
statuses = self.data[event_message_ha_id].status
for event in event_message.data.items:
status_key = StatusKey(event.key)
if status_key in statuses:
@ -134,10 +135,11 @@ class HomeConnectCoordinator(
raw_key=status_key.value,
value=event.value,
)
self._call_event_listener(event_message)
case EventType.NOTIFY:
settings = self.data[event_message.ha_id].settings
events = self.data[event_message.ha_id].events
settings = self.data[event_message_ha_id].settings
events = self.data[event_message_ha_id].events
for event in event_message.data.items:
if event.key in SettingKey:
setting_key = SettingKey(event.key)
@ -151,13 +153,25 @@ class HomeConnectCoordinator(
)
else:
events[event.key] = event
self._call_event_listener(event_message)
case EventType.EVENT:
events = self.data[event_message.ha_id].events
events = self.data[event_message_ha_id].events
for event in event_message.data.items:
events[event.key] = event
self._call_event_listener(event_message)
self._call_event_listener(event_message)
case EventType.CONNECTED:
self.data[event_message_ha_id].info.connected = True
self._call_all_event_listeners_for_appliance(
event_message_ha_id
)
case EventType.DISCONNECTED:
self.data[event_message_ha_id].info.connected = False
self._call_all_event_listeners_for_appliance(
event_message_ha_id
)
except (EventStreamInterruptedError, HomeConnectRequestError) as error:
_LOGGER.debug(
@ -186,6 +200,12 @@ class HomeConnectCoordinator(
):
listener()
@callback
def _call_all_event_listeners_for_appliance(self, ha_id: str):
for listener, context in self._listeners.values():
if isinstance(context, tuple) and context[0] == ha_id:
listener()
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
"""Fetch data from Home Connect."""
try:

View File

@ -56,3 +56,10 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
def bsh_key(self) -> str:
"""Return the BSH key."""
return self.entity_description.key
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
self.appliance.info.connected and self._attr_available and super().available
)

View File

@ -18,7 +18,13 @@ from homeassistant.components.home_connect.const import (
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform
from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.issue_registry as ir
from homeassistant.setup import async_setup_component
@ -44,6 +50,59 @@ async def test_binary_sensors(
assert config_entry.state == ConfigEntryState.LOADED
async def test_binary_sensors_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if binary sensor entities availability are based on the appliance connection state."""
entity_ids = [
"binary_sensor.washer_door",
"binary_sensor.washer_remote_control",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize(
("value", "expected"),
[

View File

@ -27,6 +27,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
@ -55,6 +56,59 @@ async def test_light(
assert config_entry.state == ConfigEntryState.LOADED
@pytest.mark.parametrize("appliance_ha_id", ["Hood"], indirect=True)
async def test_light_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if light entities availability are based on the appliance connection state."""
entity_ids = [
"light.hood_functional_light",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize(
(
"entity_id",

View File

@ -4,7 +4,14 @@ from collections.abc import Awaitable, Callable
import random
from unittest.mock import AsyncMock, MagicMock
from aiohomeconnect.model import ArrayOfSettings, GetSetting, SettingKey
from aiohomeconnect.model import (
ArrayOfEvents,
ArrayOfSettings,
EventMessage,
EventType,
GetSetting,
SettingKey,
)
from aiohomeconnect.model.error import HomeConnectError
from aiohomeconnect.model.setting import SettingConstraints
import pytest
@ -17,7 +24,7 @@ from homeassistant.components.number import (
SERVICE_SET_VALUE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -42,6 +49,64 @@ async def test_number(
assert config_entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize("appliance_ha_id", ["FridgeFreezer"], indirect=True)
async def test_number_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if number entities availability are based on the appliance connection state."""
entity_ids = [
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
]
client.get_setting.side_effect = None
# Setting constrains are not needed for this test
# so we rise an error to easily test the availability
client.get_setting = AsyncMock(side_effect=HomeConnectError())
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize("appliance_ha_id", ["FridgeFreezer"], indirect=True)
@pytest.mark.parametrize(
(

View File

@ -29,6 +29,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_SELECT_OPTION,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
@ -57,6 +58,58 @@ async def test_select(
assert config_entry.state is ConfigEntryState.LOADED
async def test_select_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if select entities availability are based on the appliance connection state."""
entity_ids = [
"select.washer_active_program",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
async def test_filter_programs(
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],

View File

@ -24,7 +24,7 @@ from homeassistant.components.home_connect.const import (
BSH_EVENT_PRESENT_STATE_PRESENT,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@ -94,6 +94,60 @@ async def test_sensors(
assert config_entry.state == ConfigEntryState.LOADED
@pytest.mark.parametrize("appliance_ha_id", [TEST_HC_APP], indirect=True)
async def test_sensor_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if sensor entities availability are based on the appliance connection state."""
entity_ids = [
"sensor.dishwasher_operation_state",
"sensor.dishwasher_salt_nearly_empty",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
# Appliance_ha_id program sequence with a delayed start.
PROGRAM_SEQUENCE_EVENTS = (
EVENT_PROG_DELAYED_START,

View File

@ -36,6 +36,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
@ -65,6 +66,61 @@ async def test_switches(
assert config_entry.state == ConfigEntryState.LOADED
@pytest.mark.parametrize("appliance_ha_id", ["Dishwasher"], indirect=True)
async def test_switch_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if switch entities availability are based on the appliance connection state."""
entity_ids = [
"switch.dishwasher_power",
"switch.dishwasher_child_lock",
"switch.dishwasher_program_eco50",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize(
(
"entity_id",

View File

@ -4,13 +4,14 @@ from collections.abc import Awaitable, Callable
from datetime import time
from unittest.mock import MagicMock
from aiohomeconnect.model import ArrayOfSettings, GetSetting, SettingKey
from aiohomeconnect.model import ArrayOfSettings, EventMessage, GetSetting, SettingKey
from aiohomeconnect.model.error import HomeConnectError
from aiohomeconnect.model.event import ArrayOfEvents, EventType
import pytest
from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, Platform
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -35,6 +36,59 @@ async def test_time(
assert config_entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize("appliance_ha_id", ["Oven"], indirect=True)
async def test_time_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if time entities availability are based on the appliance connection state."""
entity_ids = [
"time.oven_alarm_clock",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize("appliance_ha_id", ["Oven"], indirect=True)
@pytest.mark.parametrize(
("entity_id", "setting_key"),