Re-enable Home Connect updates automatically (#148657)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
J. Diego Rodríguez Royo 2025-07-13 22:46:42 +02:00 committed by GitHub
parent b2fe17c6d4
commit 74288a3bc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 64 deletions

View File

@ -38,7 +38,7 @@ from propcache.api import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
@ -626,39 +626,37 @@ class HomeConnectCoordinator(
"""Check if the appliance data hasn't been refreshed too often recently.""" """Check if the appliance data hasn't been refreshed too often recently."""
now = self.hass.loop.time() now = self.hass.loop.time()
if len(self._execution_tracker[appliance_ha_id]) >= MAX_EXECUTIONS:
return True execution_tracker = self._execution_tracker[appliance_ha_id]
initial_len = len(execution_tracker)
execution_tracker = self._execution_tracker[appliance_ha_id] = [ execution_tracker = self._execution_tracker[appliance_ha_id] = [
timestamp timestamp
for timestamp in self._execution_tracker[appliance_ha_id] for timestamp in execution_tracker
if now - timestamp < MAX_EXECUTIONS_TIME_WINDOW if now - timestamp < MAX_EXECUTIONS_TIME_WINDOW
] ]
execution_tracker.append(now) execution_tracker.append(now)
if len(execution_tracker) >= MAX_EXECUTIONS: if len(execution_tracker) >= MAX_EXECUTIONS:
ir.async_create_issue( if initial_len < MAX_EXECUTIONS:
self.hass, _LOGGER.warning(
DOMAIN, 'Too many connected/paired events for appliance "%s" '
f"home_connect_too_many_connected_paired_events_{appliance_ha_id}", "(%s times in less than %s minutes), updates have been disabled "
is_fixable=True, "and they will be enabled again whenever the connection stabilizes. "
is_persistent=True, "Consider trying to unplug the appliance "
severity=ir.IssueSeverity.ERROR, "for a while to perform a soft reset",
translation_key="home_connect_too_many_connected_paired_events", self.data[appliance_ha_id].info.name,
data={ MAX_EXECUTIONS,
"entry_id": self.config_entry.entry_id, MAX_EXECUTIONS_TIME_WINDOW // 60,
"appliance_ha_id": appliance_ha_id, )
},
translation_placeholders={
"appliance_name": self.data[appliance_ha_id].info.name,
"times": str(MAX_EXECUTIONS),
"time_window": str(MAX_EXECUTIONS_TIME_WINDOW // 60),
"home_connect_resource_url": "https://www.home-connect.com/global/help-support/error-codes#/Togglebox=15362315-13320636-1/",
"home_assistant_core_issue_url": "https://github.com/home-assistant/core/issues/147299",
},
)
return True return True
if initial_len >= MAX_EXECUTIONS:
_LOGGER.info(
'Connected/paired events from the appliance "%s" have stabilized,'
" updates have been re-enabled",
self.data[appliance_ha_id].info.name,
)
return False return False

View File

@ -124,17 +124,6 @@
} }
}, },
"issues": { "issues": {
"home_connect_too_many_connected_paired_events": {
"title": "{appliance_name} sent too many connected or paired events",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::home_connect_too_many_connected_paired_events::title%]",
"description": "The appliance \"{appliance_name}\" has been reported as connected or paired {times} times in less than {time_window} minutes, so refreshes on connected or paired events has been disabled to avoid exceeding the API rate limit.\n\nPlease refer to the [Home Connect Wi-Fi requirements and recommendations]({home_connect_resource_url}). If everything seems right with your network configuration, restart the appliance.\n\nClick \"submit\" to re-enable the updates.\nIf the issue persists, please see the following issue in the [Home Assistant core repository]({home_assistant_core_issue_url})."
}
}
}
},
"deprecated_time_alarm_clock_in_automations_scripts": { "deprecated_time_alarm_clock_in_automations_scripts": {
"title": "Deprecated alarm clock entity detected in some automations or scripts", "title": "Deprecated alarm clock entity detected in some automations or scripts",
"fix_flow": { "fix_flow": {

View File

@ -2,7 +2,6 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import timedelta from datetime import timedelta
from http import HTTPStatus
from typing import Any, cast from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
@ -53,16 +52,11 @@ from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
) )
from homeassistant.helpers import ( from homeassistant.helpers import device_registry as dr, entity_registry as er
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
INITIAL_FETCH_CLIENT_METHODS = [ INITIAL_FETCH_CLIENT_METHODS = [
"get_settings", "get_settings",
@ -580,8 +574,7 @@ async def test_paired_disconnected_devices_not_fetching(
async def test_coordinator_disabling_updates_for_appliance( async def test_coordinator_disabling_updates_for_appliance(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, freezer: FrozenDateTimeFactory,
issue_registry: ir.IssueRegistry,
client: MagicMock, client: MagicMock,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]], integration_setup: Callable[[MagicMock], Awaitable[bool]],
@ -592,7 +585,6 @@ async def test_coordinator_disabling_updates_for_appliance(
When the user confirms the issue the updates should be enabled again. When the user confirms the issue the updates should be enabled again.
""" """
appliance_ha_id = "SIEMENS-HCS02DWH1-6BE58C26DCC1" appliance_ha_id = "SIEMENS-HCS02DWH1-6BE58C26DCC1"
issue_id = f"home_connect_too_many_connected_paired_events_{appliance_ha_id}"
assert await integration_setup(client) assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
@ -606,13 +598,26 @@ async def test_coordinator_disabling_updates_for_appliance(
EventType.CONNECTED, EventType.CONNECTED,
data=ArrayOfEvents([]), data=ArrayOfEvents([]),
) )
for _ in range(8) for _ in range(6)
] ]
) )
await hass.async_block_till_done() await hass.async_block_till_done()
issue = issue_registry.async_get_issue(DOMAIN, issue_id) freezer.tick(timedelta(minutes=10))
assert issue await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
data=ArrayOfEvents([]),
)
for _ in range(2)
]
)
await hass.async_block_till_done()
# At this point, the updates have been blocked because
# 6 + 2 connected events have been received in less than an hour
get_settings_original_side_effect = client.get_settings.side_effect get_settings_original_side_effect = client.get_settings.side_effect
@ -644,18 +649,36 @@ async def test_coordinator_disabling_updates_for_appliance(
assert hass.states.is_state("switch.dishwasher_power", STATE_ON) assert hass.states.is_state("switch.dishwasher_power", STATE_ON)
_client = await hass_client() # After 55 minutes, the updates should be enabled again
resp = await _client.post( # because one hour has passed since the first connect events,
"/api/repairs/issues/fix", # so there are 2 connected events in the execution_tracker
json={"handler": DOMAIN, "issue_id": issue.issue_id}, freezer.tick(timedelta(minutes=55))
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
data=ArrayOfEvents([]),
)
]
) )
assert resp.status == HTTPStatus.OK await hass.async_block_till_done()
flow_id = (await resp.json())["flow_id"]
resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}")
assert resp.status == HTTPStatus.OK
assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert hass.states.is_state("switch.dishwasher_power", STATE_OFF)
# If more connect events are sent, it should be blocked again
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
data=ArrayOfEvents([]),
)
for _ in range(5) # 2 + 1 + 5 = 8 connect events in less than an hour
]
)
await hass.async_block_till_done()
client.get_settings = get_settings_original_side_effect
await client.add_events( await client.add_events(
[ [
EventMessage( EventMessage(
@ -672,7 +695,6 @@ async def test_coordinator_disabling_updates_for_appliance(
async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_reload( async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_reload(
hass: HomeAssistant, hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
client: MagicMock, client: MagicMock,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]], integration_setup: Callable[[MagicMock], Awaitable[bool]],
@ -682,7 +704,6 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r
The repair issue should also be deleted. The repair issue should also be deleted.
""" """
appliance_ha_id = "SIEMENS-HCS02DWH1-6BE58C26DCC1" appliance_ha_id = "SIEMENS-HCS02DWH1-6BE58C26DCC1"
issue_id = f"home_connect_too_many_connected_paired_events_{appliance_ha_id}"
assert await integration_setup(client) assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
@ -701,14 +722,9 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r
) )
await hass.async_block_till_done() await hass.async_block_till_done()
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert await integration_setup(client) assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED