Skip to content

Commit 2f244b2

Browse files
authored
2025.3.4 (#141081)
2 parents 4d1c89f + 1b7e53f commit 2f244b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+3090
-840
lines changed

homeassistant/components/elkm1/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ def hostname_from_url(url: str) -> str:
101101

102102
def _host_validator(config: dict[str, str]) -> dict[str, str]:
103103
"""Validate that a host is properly configured."""
104-
if config[CONF_HOST].startswith("elks://"):
104+
if config[CONF_HOST].startswith(("elks://", "elksv1_2://")):
105105
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
106-
raise vol.Invalid("Specify username and password for elks://")
106+
raise vol.Invalid(
107+
"Specify username and password for elks:// or elksv1_2://"
108+
)
107109
elif not config[CONF_HOST].startswith("elk://") and not config[
108110
CONF_HOST
109111
].startswith("serial://"):

homeassistant/components/google_generative_ai_conversation/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import mimetypes
56
from pathlib import Path
67

78
from google import genai # type: ignore[attr-defined]
@@ -83,7 +84,12 @@ def append_files_to_prompt():
8384
)
8485
if not Path(filename).exists():
8586
raise HomeAssistantError(f"`{filename}` does not exist")
86-
prompt_parts.append(client.files.upload(file=filename))
87+
mimetype = mimetypes.guess_type(filename)[0]
88+
with open(filename, "rb") as file:
89+
uploaded_file = client.files.upload(
90+
file=file, config={"mime_type": mimetype}
91+
)
92+
prompt_parts.append(uploaded_file)
8793

8894
await hass.async_add_executor_job(append_files_to_prompt)
8995

homeassistant/components/home_connect/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,14 +629,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
629629
home_connect_client = HomeConnectClient(config_entry_auth)
630630

631631
coordinator = HomeConnectCoordinator(hass, entry, home_connect_client)
632-
await coordinator.async_config_entry_first_refresh()
633-
632+
await coordinator.async_setup()
634633
entry.runtime_data = coordinator
635634

636635
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
637636

638637
entry.runtime_data.start_event_listener()
639638

639+
entry.async_create_background_task(
640+
hass,
641+
coordinator.async_refresh(),
642+
f"home_connect-initial-full-refresh-{entry.entry_id}",
643+
)
644+
640645
return True
641646

642647

homeassistant/components/home_connect/common.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -137,41 +137,6 @@ def setup_home_connect_entry(
137137
defaultdict(list)
138138
)
139139

140-
entities: list[HomeConnectEntity] = []
141-
for appliance in entry.runtime_data.data.values():
142-
entities_to_add = get_entities_for_appliance(entry, appliance)
143-
if get_option_entities_for_appliance:
144-
entities_to_add.extend(get_option_entities_for_appliance(entry, appliance))
145-
for event_key in (
146-
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
147-
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
148-
):
149-
changed_options_listener_remove_callback = (
150-
entry.runtime_data.async_add_listener(
151-
partial(
152-
_create_option_entities,
153-
entry,
154-
appliance,
155-
known_entity_unique_ids,
156-
get_option_entities_for_appliance,
157-
async_add_entities,
158-
),
159-
(appliance.info.ha_id, event_key),
160-
)
161-
)
162-
entry.async_on_unload(changed_options_listener_remove_callback)
163-
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
164-
changed_options_listener_remove_callback
165-
)
166-
known_entity_unique_ids.update(
167-
{
168-
cast(str, entity.unique_id): appliance.info.ha_id
169-
for entity in entities_to_add
170-
}
171-
)
172-
entities.extend(entities_to_add)
173-
async_add_entities(entities)
174-
175140
entry.async_on_unload(
176141
entry.runtime_data.async_add_special_listener(
177142
partial(

homeassistant/components/home_connect/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
DOMAIN = "home_connect"
1212

13+
API_DEFAULT_RETRY_AFTER = 60
1314

1415
APPLIANCES_WITH_PROGRAMS = (
1516
"CleaningRobot",
@@ -284,6 +285,7 @@
284285
"LaundryCare.Washer.EnumType.SpinSpeed.Off",
285286
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
286287
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
288+
"LaundryCare.Washer.EnumType.SpinSpeed.RPM700",
287289
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
288290
"LaundryCare.Washer.EnumType.SpinSpeed.RPM900",
289291
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",

homeassistant/components/home_connect/coordinator.py

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
import asyncio
5+
from asyncio import sleep as asyncio_sleep
66
from collections import defaultdict
77
from collections.abc import Callable
88
from dataclasses import dataclass
@@ -29,18 +29,19 @@
2929
HomeConnectApiError,
3030
HomeConnectError,
3131
HomeConnectRequestError,
32+
TooManyRequestsError,
3233
UnauthorizedError,
3334
)
3435
from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption
3536
from propcache.api import cached_property
3637

3738
from homeassistant.config_entries import ConfigEntry
3839
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
39-
from homeassistant.exceptions import ConfigEntryAuthFailed
40+
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
4041
from homeassistant.helpers import device_registry as dr
4142
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
4243

43-
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN
44+
from .const import API_DEFAULT_RETRY_AFTER, APPLIANCES_WITH_PROGRAMS, DOMAIN
4445
from .utils import get_dict_from_home_connect_error
4546

4647
_LOGGER = logging.getLogger(__name__)
@@ -154,7 +155,7 @@ def start_event_listener(self) -> None:
154155
f"home_connect-events_listener_task-{self.config_entry.entry_id}",
155156
)
156157

157-
async def _event_listener(self) -> None:
158+
async def _event_listener(self) -> None: # noqa: C901
158159
"""Match event with listener for event type."""
159160
retry_time = 10
160161
while True:
@@ -269,7 +270,7 @@ async def _event_listener(self) -> None:
269270
type(error).__name__,
270271
retry_time,
271272
)
272-
await asyncio.sleep(retry_time)
273+
await asyncio_sleep(retry_time)
273274
retry_time = min(retry_time * 2, 3600)
274275
except HomeConnectApiError as error:
275276
_LOGGER.error("Error while listening for events: %s", error)
@@ -278,6 +279,13 @@ async def _event_listener(self) -> None:
278279
)
279280
break
280281

282+
# Trigger to delete the possible depaired device entities
283+
# from known_entities variable at common.py
284+
for listener, context in self._special_listeners.values():
285+
assert isinstance(context, tuple)
286+
if EventKey.BSH_COMMON_APPLIANCE_DEPAIRED in context:
287+
listener()
288+
281289
@callback
282290
def _call_event_listener(self, event_message: EventMessage) -> None:
283291
"""Call listener for event."""
@@ -295,6 +303,42 @@ def _call_all_event_listeners_for_appliance(self, ha_id: str) -> None:
295303

296304
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
297305
"""Fetch data from Home Connect."""
306+
await self._async_setup()
307+
308+
for appliance_data in self.data.values():
309+
appliance = appliance_data.info
310+
ha_id = appliance.ha_id
311+
while True:
312+
try:
313+
self.data[ha_id] = await self._get_appliance_data(
314+
appliance, self.data.get(ha_id)
315+
)
316+
except TooManyRequestsError as err:
317+
_LOGGER.debug(
318+
"Rate limit exceeded on initial fetch: %s",
319+
err,
320+
)
321+
await asyncio_sleep(err.retry_after or API_DEFAULT_RETRY_AFTER)
322+
else:
323+
break
324+
325+
for listener, context in self._special_listeners.values():
326+
assert isinstance(context, tuple)
327+
if EventKey.BSH_COMMON_APPLIANCE_PAIRED in context:
328+
listener()
329+
330+
return self.data
331+
332+
async def async_setup(self) -> None:
333+
"""Set up the devices."""
334+
try:
335+
await self._async_setup()
336+
except UpdateFailed as err:
337+
raise ConfigEntryNotReady from err
338+
339+
async def _async_setup(self) -> None:
340+
"""Set up the devices."""
341+
old_appliances = set(self.data.keys())
298342
try:
299343
appliances = await self.client.get_home_appliances()
300344
except UnauthorizedError as error:
@@ -312,12 +356,38 @@ async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
312356
translation_placeholders=get_dict_from_home_connect_error(error),
313357
) from error
314358

315-
return {
316-
appliance.ha_id: await self._get_appliance_data(
317-
appliance, self.data.get(appliance.ha_id)
359+
for appliance in appliances.homeappliances:
360+
self.device_registry.async_get_or_create(
361+
config_entry_id=self.config_entry.entry_id,
362+
identifiers={(DOMAIN, appliance.ha_id)},
363+
manufacturer=appliance.brand,
364+
name=appliance.name,
365+
model=appliance.vib,
318366
)
319-
for appliance in appliances.homeappliances
320-
}
367+
if appliance.ha_id not in self.data:
368+
self.data[appliance.ha_id] = HomeConnectApplianceData(
369+
commands=set(),
370+
events={},
371+
info=appliance,
372+
options={},
373+
programs=[],
374+
settings={},
375+
status={},
376+
)
377+
else:
378+
self.data[appliance.ha_id].info.connected = appliance.connected
379+
old_appliances.remove(appliance.ha_id)
380+
381+
for ha_id in old_appliances:
382+
self.data.pop(ha_id, None)
383+
device = self.device_registry.async_get_device(
384+
identifiers={(DOMAIN, ha_id)}
385+
)
386+
if device:
387+
self.device_registry.async_update_device(
388+
device_id=device.id,
389+
remove_config_entry_id=self.config_entry.entry_id,
390+
)
321391

322392
async def _get_appliance_data(
323393
self,
@@ -339,6 +409,8 @@ async def _get_appliance_data(
339409
await self.client.get_settings(appliance.ha_id)
340410
).settings
341411
}
412+
except TooManyRequestsError:
413+
raise
342414
except HomeConnectError as error:
343415
_LOGGER.debug(
344416
"Error fetching settings for %s: %s",
@@ -353,6 +425,8 @@ async def _get_appliance_data(
353425
status.key: status
354426
for status in (await self.client.get_status(appliance.ha_id)).status
355427
}
428+
except TooManyRequestsError:
429+
raise
356430
except HomeConnectError as error:
357431
_LOGGER.debug(
358432
"Error fetching status for %s: %s",
@@ -369,6 +443,8 @@ async def _get_appliance_data(
369443
if appliance.type in APPLIANCES_WITH_PROGRAMS:
370444
try:
371445
all_programs = await self.client.get_all_programs(appliance.ha_id)
446+
except TooManyRequestsError:
447+
raise
372448
except HomeConnectError as error:
373449
_LOGGER.debug(
374450
"Error fetching programs for %s: %s",
@@ -427,6 +503,8 @@ async def _get_appliance_data(
427503
await self.client.get_available_commands(appliance.ha_id)
428504
).commands
429505
}
506+
except TooManyRequestsError:
507+
raise
430508
except HomeConnectError:
431509
commands = set()
432510

@@ -461,6 +539,8 @@ async def get_options_definitions(
461539
).options
462540
or []
463541
}
542+
except TooManyRequestsError:
543+
raise
464544
except HomeConnectError as error:
465545
_LOGGER.debug(
466546
"Error fetching options for %s: %s",

homeassistant/components/home_connect/entity.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
"""Home Connect entity base class."""
22

33
from abc import abstractmethod
4+
from collections.abc import Callable, Coroutine
45
import contextlib
6+
from datetime import datetime
57
import logging
6-
from typing import cast
8+
from typing import Any, Concatenate, cast
79

810
from aiohomeconnect.model import EventKey, OptionKey
9-
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
11+
from aiohomeconnect.model.error import (
12+
ActiveProgramNotSetError,
13+
HomeConnectError,
14+
TooManyRequestsError,
15+
)
1016

1117
from homeassistant.const import STATE_UNAVAILABLE
1218
from homeassistant.core import callback
1319
from homeassistant.exceptions import HomeAssistantError
1420
from homeassistant.helpers.device_registry import DeviceInfo
1521
from homeassistant.helpers.entity import EntityDescription
22+
from homeassistant.helpers.event import async_call_later
1623
from homeassistant.helpers.update_coordinator import CoordinatorEntity
1724

18-
from .const import DOMAIN
25+
from .const import API_DEFAULT_RETRY_AFTER, DOMAIN
1926
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
2027
from .utils import get_dict_from_home_connect_error
2128

@@ -127,3 +134,34 @@ async def async_set_option(self, value: str | float | bool) -> None:
127134
def bsh_key(self) -> OptionKey:
128135
"""Return the BSH key."""
129136
return cast(OptionKey, self.entity_description.key)
137+
138+
139+
def constraint_fetcher[_EntityT: HomeConnectEntity, **_P](
140+
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
141+
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
142+
"""Decorate the function to catch Home Connect too many requests error and retry later.
143+
144+
If it needs to be called later, it will call async_write_ha_state function
145+
"""
146+
147+
async def handler_to_return(
148+
self: _EntityT, *args: _P.args, **kwargs: _P.kwargs
149+
) -> None:
150+
async def handler(_datetime: datetime | None = None) -> None:
151+
try:
152+
await func(self, *args, **kwargs)
153+
except TooManyRequestsError as err:
154+
if (retry_after := err.retry_after) is None:
155+
retry_after = API_DEFAULT_RETRY_AFTER
156+
async_call_later(self.hass, retry_after, handler)
157+
except HomeConnectError as err:
158+
_LOGGER.error(
159+
"Error fetching constraints for %s: %s", self.entity_id, err
160+
)
161+
else:
162+
if _datetime is not None:
163+
self.async_write_ha_state()
164+
165+
await handler()
166+
167+
return handler_to_return

0 commit comments

Comments
 (0)