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.core import CALLBACK_TYPE, HomeAssistant, callback
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 .const import (
@ -626,39 +626,37 @@ class HomeConnectCoordinator(
"""Check if the appliance data hasn't been refreshed too often recently."""
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] = [
timestamp
for timestamp in self._execution_tracker[appliance_ha_id]
for timestamp in execution_tracker
if now - timestamp < MAX_EXECUTIONS_TIME_WINDOW
]
execution_tracker.append(now)
if len(execution_tracker) >= MAX_EXECUTIONS:
ir.async_create_issue(
self.hass,
DOMAIN,
f"home_connect_too_many_connected_paired_events_{appliance_ha_id}",
is_fixable=True,
is_persistent=True,
severity=ir.IssueSeverity.ERROR,
translation_key="home_connect_too_many_connected_paired_events",
data={
"entry_id": self.config_entry.entry_id,
"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",
},
)
if initial_len < MAX_EXECUTIONS:
_LOGGER.warning(
'Too many connected/paired events for appliance "%s" '
"(%s times in less than %s minutes), updates have been disabled "
"and they will be enabled again whenever the connection stabilizes. "
"Consider trying to unplug the appliance "
"for a while to perform a soft reset",
self.data[appliance_ha_id].info.name,
MAX_EXECUTIONS,
MAX_EXECUTIONS_TIME_WINDOW // 60,
)
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

View File

@ -124,17 +124,6 @@
}
},
"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": {
"title": "Deprecated alarm clock entity detected in some automations or scripts",
"fix_flow": {

View File

@ -2,7 +2,6 @@
from collections.abc import Awaitable, Callable
from datetime import timedelta
from http import HTTPStatus
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
@ -53,16 +52,11 @@ from homeassistant.core import (
HomeAssistant,
callback,
)
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
INITIAL_FETCH_CLIENT_METHODS = [
"get_settings",
@ -580,8 +574,7 @@ async def test_paired_disconnected_devices_not_fetching(
async def test_coordinator_disabling_updates_for_appliance(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
freezer: FrozenDateTimeFactory,
client: MagicMock,
config_entry: MockConfigEntry,
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.
"""
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 config_entry.state is ConfigEntryState.LOADED
@ -606,13 +598,26 @@ async def test_coordinator_disabling_updates_for_appliance(
EventType.CONNECTED,
data=ArrayOfEvents([]),
)
for _ in range(8)
for _ in range(6)
]
)
await hass.async_block_till_done()
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue
freezer.tick(timedelta(minutes=10))
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
@ -644,18 +649,36 @@ async def test_coordinator_disabling_updates_for_appliance(
assert hass.states.is_state("switch.dishwasher_power", STATE_ON)
_client = await hass_client()
resp = await _client.post(
"/api/repairs/issues/fix",
json={"handler": DOMAIN, "issue_id": issue.issue_id},
# After 55 minutes, the updates should be enabled again
# because one hour has passed since the first connect events,
# so there are 2 connected events in the execution_tracker
freezer.tick(timedelta(minutes=55))
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
data=ArrayOfEvents([]),
)
]
)
assert resp.status == HTTPStatus.OK
flow_id = (await resp.json())["flow_id"]
resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}")
assert resp.status == HTTPStatus.OK
await hass.async_block_till_done()
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(
[
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(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
client: MagicMock,
config_entry: MockConfigEntry,
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.
"""
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 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()
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED