mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Fix home connect available (#139760)
* Fix home connect available * Extend and clarify test * Do not change connected state on stream interrupted
This commit is contained in:
parent
344cfedd6f
commit
e8099fd3b2
@ -98,6 +98,7 @@ class HomeConnectCoordinator(
|
||||
CALLBACK_TYPE, tuple[CALLBACK_TYPE, tuple[EventKey, ...]]
|
||||
] = {}
|
||||
self.device_registry = dr.async_get(self.hass)
|
||||
self.data = {}
|
||||
|
||||
@cached_property
|
||||
def context_listeners(self) -> dict[tuple[str, EventKey], list[CALLBACK_TYPE]]:
|
||||
@ -161,6 +162,14 @@ class HomeConnectCoordinator(
|
||||
async for event_message in self.client.stream_all_events():
|
||||
retry_time = 10
|
||||
event_message_ha_id = event_message.ha_id
|
||||
if (
|
||||
event_message_ha_id in self.data
|
||||
and not self.data[event_message_ha_id].info.connected
|
||||
):
|
||||
self.data[event_message_ha_id].info.connected = True
|
||||
self._call_all_event_listeners_for_appliance(
|
||||
event_message_ha_id
|
||||
)
|
||||
match event_message.type:
|
||||
case EventType.STATUS:
|
||||
statuses = self.data[event_message_ha_id].status
|
||||
@ -295,6 +304,8 @@ class HomeConnectCoordinator(
|
||||
translation_placeholders=get_dict_from_home_connect_error(error),
|
||||
) from error
|
||||
except HomeConnectError as error:
|
||||
for appliance_data in self.data.values():
|
||||
appliance_data.info.connected = False
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="fetch_api_error",
|
||||
@ -303,7 +314,7 @@ class HomeConnectCoordinator(
|
||||
|
||||
return {
|
||||
appliance.ha_id: await self._get_appliance_data(
|
||||
appliance, self.data.get(appliance.ha_id) if self.data else None
|
||||
appliance, self.data.get(appliance.ha_id)
|
||||
)
|
||||
for appliance in appliances.homeappliances
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ from typing import cast
|
||||
from aiohomeconnect.model import EventKey, OptionKey
|
||||
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@ -51,8 +52,10 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.update_native_value()
|
||||
available = self._attr_available = self.appliance.info.connected
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, self.state)
|
||||
state = STATE_UNAVAILABLE if not available else self.state
|
||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, state)
|
||||
|
||||
@property
|
||||
def bsh_key(self) -> str:
|
||||
@ -61,10 +64,13 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
self.appliance.info.connected and self._attr_available and super().available
|
||||
)
|
||||
"""Return True if entity is available.
|
||||
|
||||
Do not use self.last_update_success for available state
|
||||
as event updates should take precedence over the coordinator
|
||||
refresh.
|
||||
"""
|
||||
return self._attr_available
|
||||
|
||||
|
||||
class HomeConnectOptionEntity(HomeConnectEntity):
|
||||
|
@ -1 +1,19 @@
|
||||
"""Tests for the Home Connect integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.model import ArrayOfHomeAppliances, ArrayOfStatus
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
|
||||
MOCK_APPLIANCES = ArrayOfHomeAppliances.from_dict(
|
||||
load_json_object_fixture("home_connect/appliances.json")["data"] # type: ignore[arg-type]
|
||||
)
|
||||
MOCK_PROGRAMS: dict[str, Any] = load_json_object_fixture("home_connect/programs.json")
|
||||
MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json")
|
||||
MOCK_STATUS = ArrayOfStatus.from_dict(
|
||||
load_json_object_fixture("home_connect/status.json")["data"] # type: ignore[arg-type]
|
||||
)
|
||||
MOCK_AVAILABLE_COMMANDS: dict[str, Any] = load_json_object_fixture(
|
||||
"home_connect/available_commands.json"
|
||||
)
|
||||
|
@ -11,11 +11,9 @@ from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfCommands,
|
||||
ArrayOfEvents,
|
||||
ArrayOfHomeAppliances,
|
||||
ArrayOfOptions,
|
||||
ArrayOfPrograms,
|
||||
ArrayOfSettings,
|
||||
ArrayOfStatus,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
@ -41,20 +39,15 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
MOCK_APPLIANCES = ArrayOfHomeAppliances.from_dict(
|
||||
load_json_object_fixture("home_connect/appliances.json")["data"]
|
||||
)
|
||||
MOCK_PROGRAMS: dict[str, Any] = load_json_object_fixture("home_connect/programs.json")
|
||||
MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json")
|
||||
MOCK_STATUS = ArrayOfStatus.from_dict(
|
||||
load_json_object_fixture("home_connect/status.json")["data"]
|
||||
)
|
||||
MOCK_AVAILABLE_COMMANDS: dict[str, Any] = load_json_object_fixture(
|
||||
"home_connect/available_commands.json"
|
||||
from . import (
|
||||
MOCK_APPLIANCES,
|
||||
MOCK_AVAILABLE_COMMANDS,
|
||||
MOCK_PROGRAMS,
|
||||
MOCK_SETTINGS,
|
||||
MOCK_STATUS,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test for Home Connect coordinator."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
@ -20,6 +21,7 @@ from aiohomeconnect.model.error import (
|
||||
HomeConnectError,
|
||||
HomeConnectRequestError,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import (
|
||||
@ -36,8 +38,11 @@ from homeassistant.core import (
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MOCK_APPLIANCES
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@ -74,6 +79,123 @@ async def test_coordinator_update_failing_get_appliances(
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_credentials")
|
||||
@pytest.mark.parametrize("platforms", [("binary_sensor",)])
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["Washer"], indirect=True)
|
||||
async def test_coordinator_failure_refresh_and_stream(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
client: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
appliance_ha_id: str,
|
||||
) -> None:
|
||||
"""Test entity available state via coordinator refresh and event stream."""
|
||||
entity_id_1 = "binary_sensor.washer_remote_control"
|
||||
entity_id_2 = "binary_sensor.washer_remote_start"
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
state = hass.states.get(entity_id_1)
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
state = hass.states.get(entity_id_2)
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
|
||||
client.get_home_appliances.side_effect = HomeConnectError()
|
||||
|
||||
# Force a coordinator refresh.
|
||||
await hass.services.async_call(
|
||||
"homeassistant", "update_entity", {"entity_id": entity_id_1}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id_1)
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
state = hass.states.get(entity_id_2)
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Test that the entity becomes available again after a successful update.
|
||||
|
||||
client.get_home_appliances.side_effect = None
|
||||
client.get_home_appliances.return_value = copy.deepcopy(MOCK_APPLIANCES)
|
||||
|
||||
# Move time forward to pass the debounce time.
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Force a coordinator refresh.
|
||||
await hass.services.async_call(
|
||||
"homeassistant", "update_entity", {"entity_id": entity_id_1}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id_1)
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
state = hass.states.get(entity_id_2)
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
|
||||
# Test that the event stream makes the entity go available too.
|
||||
|
||||
# First make the entity unavailable.
|
||||
client.get_home_appliances.side_effect = HomeConnectError()
|
||||
|
||||
# Move time forward to pass the debounce time
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Force a coordinator refresh
|
||||
await hass.services.async_call(
|
||||
"homeassistant", "update_entity", {"entity_id": entity_id_1}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id_1)
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
state = hass.states.get(entity_id_2)
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Now make the entity available again.
|
||||
client.get_home_appliances.side_effect = None
|
||||
client.get_home_appliances.return_value = copy.deepcopy(MOCK_APPLIANCES)
|
||||
|
||||
# One event should make all entities for this appliance available again.
|
||||
event_message = EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.STATUS,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=EventKey.BSH_COMMON_STATUS_REMOTE_CONTROL_ACTIVE,
|
||||
raw_key=EventKey.BSH_COMMON_STATUS_REMOTE_CONTROL_ACTIVE.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=False,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
await client.add_events([event_message])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id_1)
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
state = hass.states.get(entity_id_2)
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_method",
|
||||
[
|
||||
@ -330,11 +452,13 @@ async def test_event_listener_resilience(
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert len(config_entry._background_tasks) == 1
|
||||
|
||||
assert hass.states.is_state(entity_id, initial_state)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == initial_state
|
||||
|
||||
await hass.async_block_till_done()
|
||||
future.set_exception(exception)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -362,4 +486,6 @@ async def test_event_listener_resilience(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.is_state(entity_id, after_event_expected_state)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == after_event_expected_state
|
||||
|
Loading…
x
Reference in New Issue
Block a user