Skip to content

Commit bceccd8

Browse files
authored
2025.1.2 (#135241)
2 parents d59a91a + 0027d90 commit bceccd8

File tree

21 files changed

+165
-46
lines changed

21 files changed

+165
-46
lines changed

homeassistant/components/backup/config.py

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass, field, replace
88
from datetime import datetime, timedelta
99
from enum import StrEnum
10+
import random
1011
from typing import TYPE_CHECKING, Self, TypedDict
1112

1213
from cronsim import CronSim
@@ -28,6 +29,10 @@
2829
CRON_PATTERN_DAILY = "45 4 * * *"
2930
CRON_PATTERN_WEEKLY = "45 4 * * {}"
3031

32+
# Randomize the start time of the backup by up to 60 minutes to avoid
33+
# all backups running at the same time.
34+
BACKUP_START_TIME_JITTER = 60 * 60
35+
3136

3237
class StoredBackupConfig(TypedDict):
3338
"""Represent the stored backup config."""
@@ -329,6 +334,8 @@ async def _create_backup(now: datetime) -> None:
329334
except Exception: # noqa: BLE001
330335
LOGGER.exception("Unexpected error creating automatic backup")
331336

337+
next_time += timedelta(seconds=random.randint(0, BACKUP_START_TIME_JITTER))
338+
LOGGER.debug("Scheduling next automatic backup at %s", next_time)
332339
manager.remove_next_backup_event = async_track_point_in_time(
333340
manager.hass, _create_backup, next_time
334341
)

homeassistant/components/cloud/backup.py

+65-18
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import base64
67
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
78
import hashlib
89
import logging
10+
import random
911
from typing import Any, Self
1012

1113
from aiohttp import ClientError, ClientTimeout, StreamReader
@@ -26,6 +28,9 @@
2628

2729
_LOGGER = logging.getLogger(__name__)
2830
_STORAGE_BACKUP = "backup"
31+
_RETRY_LIMIT = 5
32+
_RETRY_SECONDS_MIN = 60
33+
_RETRY_SECONDS_MAX = 600
2934

3035

3136
async def _b64md5(stream: AsyncIterator[bytes]) -> str:
@@ -138,37 +143,34 @@ async def async_download_backup(
138143
raise BackupAgentError("Failed to get download details") from err
139144

140145
try:
141-
resp = await self._cloud.websession.get(details["url"])
146+
resp = await self._cloud.websession.get(
147+
details["url"],
148+
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
149+
)
150+
142151
resp.raise_for_status()
143152
except ClientError as err:
144153
raise BackupAgentError("Failed to download backup") from err
145154

146155
return ChunkAsyncStreamIterator(resp.content)
147156

148-
async def async_upload_backup(
157+
async def _async_do_upload_backup(
149158
self,
150159
*,
151160
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
152-
backup: AgentBackup,
153-
**kwargs: Any,
161+
filename: str,
162+
base64md5hash: str,
163+
metadata: dict[str, Any],
164+
size: int,
154165
) -> None:
155-
"""Upload a backup.
156-
157-
:param open_stream: A function returning an async iterator that yields bytes.
158-
:param backup: Metadata about the backup that should be uploaded.
159-
"""
160-
if not backup.protected:
161-
raise BackupAgentError("Cloud backups must be protected")
162-
163-
base64md5hash = await _b64md5(await open_stream())
164-
166+
"""Upload a backup."""
165167
try:
166168
details = await async_files_upload_details(
167169
self._cloud,
168170
storage_type=_STORAGE_BACKUP,
169-
filename=self._get_backup_filename(),
170-
metadata=backup.as_dict(),
171-
size=backup.size,
171+
filename=filename,
172+
metadata=metadata,
173+
size=size,
172174
base64md5hash=base64md5hash,
173175
)
174176
except (ClientError, CloudError) as err:
@@ -178,7 +180,7 @@ async def async_upload_backup(
178180
upload_status = await self._cloud.websession.put(
179181
details["url"],
180182
data=await open_stream(),
181-
headers=details["headers"] | {"content-length": str(backup.size)},
183+
headers=details["headers"] | {"content-length": str(size)},
182184
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
183185
)
184186
_LOGGER.log(
@@ -190,6 +192,51 @@ async def async_upload_backup(
190192
except (TimeoutError, ClientError) as err:
191193
raise BackupAgentError("Failed to upload backup") from err
192194

195+
async def async_upload_backup(
196+
self,
197+
*,
198+
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
199+
backup: AgentBackup,
200+
**kwargs: Any,
201+
) -> None:
202+
"""Upload a backup.
203+
204+
:param open_stream: A function returning an async iterator that yields bytes.
205+
:param backup: Metadata about the backup that should be uploaded.
206+
"""
207+
if not backup.protected:
208+
raise BackupAgentError("Cloud backups must be protected")
209+
210+
base64md5hash = await _b64md5(await open_stream())
211+
filename = self._get_backup_filename()
212+
metadata = backup.as_dict()
213+
size = backup.size
214+
215+
tries = 1
216+
while tries <= _RETRY_LIMIT:
217+
try:
218+
await self._async_do_upload_backup(
219+
open_stream=open_stream,
220+
filename=filename,
221+
base64md5hash=base64md5hash,
222+
metadata=metadata,
223+
size=size,
224+
)
225+
break
226+
except BackupAgentError as err:
227+
if tries == _RETRY_LIMIT:
228+
raise
229+
tries += 1
230+
retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX)
231+
_LOGGER.info(
232+
"Failed to upload backup, retrying (%s/%s) in %ss: %s",
233+
tries,
234+
_RETRY_LIMIT,
235+
retry_timer,
236+
err,
237+
)
238+
await asyncio.sleep(retry_timer)
239+
193240
async def async_delete_backup(
194241
self,
195242
backup_id: str,

homeassistant/components/cookidoo/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
"iot_class": "cloud_polling",
99
"loggers": ["cookidoo_api"],
1010
"quality_scale": "silver",
11-
"requirements": ["cookidoo-api==0.11.2"]
11+
"requirements": ["cookidoo-api==0.12.2"]
1212
}

homeassistant/components/flick_electric/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"integration_type": "service",
88
"iot_class": "cloud_polling",
99
"loggers": ["pyflick"],
10-
"requirements": ["PyFlick==1.1.2"]
10+
"requirements": ["PyFlick==1.1.3"]
1111
}

homeassistant/components/flick_electric/sensor.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,19 @@ def native_value(self) -> Decimal:
5151
_LOGGER.warning(
5252
"Unexpected quantity for unit price: %s", self.coordinator.data
5353
)
54-
return self.coordinator.data.cost
54+
return self.coordinator.data.cost * 100
5555

5656
@property
5757
def extra_state_attributes(self) -> dict[str, Any] | None:
5858
"""Return the state attributes."""
59-
components: dict[str, Decimal] = {}
59+
components: dict[str, float] = {}
6060

6161
for component in self.coordinator.data.components:
6262
if component.charge_setter not in ATTR_COMPONENTS:
6363
_LOGGER.warning("Found unknown component: %s", component.charge_setter)
6464
continue
6565

66-
components[component.charge_setter] = component.value
66+
components[component.charge_setter] = float(component.value * 100)
6767

6868
return {
6969
ATTR_START_AT: self.coordinator.data.start_at,

homeassistant/components/frontend/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
"documentation": "https://www.home-assistant.io/integrations/frontend",
2222
"integration_type": "system",
2323
"quality_scale": "internal",
24-
"requirements": ["home-assistant-frontend==20250106.0"]
24+
"requirements": ["home-assistant-frontend==20250109.0"]
2525
}

homeassistant/components/husqvarna_automower/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
"iot_class": "cloud_push",
99
"loggers": ["aioautomower"],
1010
"quality_scale": "silver",
11-
"requirements": ["aioautomower==2024.12.0"]
11+
"requirements": ["aioautomower==2025.1.0"]
1212
}

homeassistant/components/meteo_france/__init__.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from meteofrance_api.client import MeteoFranceClient
77
from meteofrance_api.helpers import is_valid_warning_department
88
from meteofrance_api.model import CurrentPhenomenons, Forecast, Rain
9+
from requests import RequestException
910
import voluptuous as vol
1011

1112
from homeassistant.config_entries import ConfigEntry
@@ -83,7 +84,13 @@ async def _async_update_data_alert() -> CurrentPhenomenons:
8384
update_method=_async_update_data_rain,
8485
update_interval=SCAN_INTERVAL_RAIN,
8586
)
86-
await coordinator_rain.async_config_entry_first_refresh()
87+
try:
88+
await coordinator_rain._async_refresh(log_failures=False) # noqa: SLF001
89+
except RequestException:
90+
_LOGGER.warning(
91+
"1 hour rain forecast not available: %s is not in covered zone",
92+
entry.title,
93+
)
8794

8895
department = coordinator_forecast.data.position.get("dept")
8996
_LOGGER.debug(
@@ -128,8 +135,9 @@ async def _async_update_data_alert() -> CurrentPhenomenons:
128135
hass.data[DOMAIN][entry.entry_id] = {
129136
UNDO_UPDATE_LISTENER: undo_listener,
130137
COORDINATOR_FORECAST: coordinator_forecast,
131-
COORDINATOR_RAIN: coordinator_rain,
132138
}
139+
if coordinator_rain and coordinator_rain.last_update_success:
140+
hass.data[DOMAIN][entry.entry_id][COORDINATOR_RAIN] = coordinator_rain
133141
if coordinator_alert and coordinator_alert.last_update_success:
134142
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
135143

homeassistant/components/meteo_france/sensor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ async def async_setup_entry(
187187
"""Set up the Meteo-France sensor platform."""
188188
data = hass.data[DOMAIN][entry.entry_id]
189189
coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
190-
coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN]
190+
coordinator_rain: DataUpdateCoordinator[Rain] | None = data.get(COORDINATOR_RAIN)
191191
coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
192192
COORDINATOR_ALERT
193193
)

homeassistant/components/overkiz/executor.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from urllib.parse import urlparse
77

88
from pyoverkiz.enums import OverkizCommand, Protocol
9-
from pyoverkiz.exceptions import OverkizException
9+
from pyoverkiz.exceptions import BaseOverkizException
1010
from pyoverkiz.models import Command, Device, StateDefinition
1111
from pyoverkiz.types import StateType as OverkizStateType
1212

@@ -105,7 +105,7 @@ async def async_execute_command(
105105
"Home Assistant",
106106
)
107107
# Catch Overkiz exceptions to support `continue_on_error` functionality
108-
except OverkizException as exception:
108+
except BaseOverkizException as exception:
109109
raise HomeAssistantError(exception) from exception
110110

111111
# ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here

homeassistant/components/reolink/util.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ def get_device_uid_and_ch(
8282
ch = int(device_uid[1][5:])
8383
is_chime = True
8484
else:
85-
ch = host.api.channel_for_uid(device_uid[1])
85+
device_uid_part = "_".join(device_uid[1:])
86+
ch = host.api.channel_for_uid(device_uid_part)
8687
return (device_uid, ch, is_chime)
8788

8889

homeassistant/components/suez_water/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"iot_class": "cloud_polling",
88
"loggers": ["pysuez", "regex"],
99
"quality_scale": "bronze",
10-
"requirements": ["pysuezV2==2.0.1"]
10+
"requirements": ["pysuezV2==2.0.3"]
1111
}

homeassistant/components/zha/entity.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def device_info(self) -> DeviceInfo:
8787
manufacturer=zha_device_info[ATTR_MANUFACTURER],
8888
model=zha_device_info[ATTR_MODEL],
8989
name=zha_device_info[ATTR_NAME],
90-
via_device=(DOMAIN, zha_gateway.state.node_info.ieee),
90+
via_device=(DOMAIN, str(zha_gateway.state.node_info.ieee)),
9191
)
9292

9393
@callback

homeassistant/const.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
APPLICATION_NAME: Final = "HomeAssistant"
2626
MAJOR_VERSION: Final = 2025
2727
MINOR_VERSION: Final = 1
28-
PATCH_VERSION: Final = "1"
28+
PATCH_VERSION: Final = "2"
2929
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
3030
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
3131
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

homeassistant/package_constraints.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ habluetooth==3.7.0
3535
hass-nabucasa==0.87.0
3636
hassil==2.1.0
3737
home-assistant-bluetooth==1.13.0
38-
home-assistant-frontend==20250106.0
38+
home-assistant-frontend==20250109.0
3939
home-assistant-intents==2025.1.1
4040
httpx==0.27.2
4141
ifaddr==0.2.0

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "homeassistant"
7-
version = "2025.1.1"
7+
version = "2025.1.2"
88
license = {text = "Apache-2.0"}
99
description = "Open-source home automation platform running on Python 3."
1010
readme = "README.rst"

requirements_all.txt

+5-5
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ ProgettiHWSW==0.1.3
4848
PyChromecast==14.0.5
4949

5050
# homeassistant.components.flick_electric
51-
PyFlick==1.1.2
51+
PyFlick==1.1.3
5252

5353
# homeassistant.components.flume
5454
PyFlume==0.6.5
@@ -201,7 +201,7 @@ aioaseko==1.0.0
201201
aioasuswrt==1.4.0
202202

203203
# homeassistant.components.husqvarna_automower
204-
aioautomower==2024.12.0
204+
aioautomower==2025.1.0
205205

206206
# homeassistant.components.azure_devops
207207
aioazuredevops==2.2.1
@@ -704,7 +704,7 @@ connect-box==0.3.1
704704
construct==2.10.68
705705

706706
# homeassistant.components.cookidoo
707-
cookidoo-api==0.11.2
707+
cookidoo-api==0.12.2
708708

709709
# homeassistant.components.backup
710710
# homeassistant.components.utility_meter
@@ -1134,7 +1134,7 @@ hole==0.8.0
11341134
holidays==0.64
11351135

11361136
# homeassistant.components.frontend
1137-
home-assistant-frontend==20250106.0
1137+
home-assistant-frontend==20250109.0
11381138

11391139
# homeassistant.components.conversation
11401140
home-assistant-intents==2025.1.1
@@ -2309,7 +2309,7 @@ pysqueezebox==0.10.0
23092309
pystiebeleltron==0.0.1.dev2
23102310

23112311
# homeassistant.components.suez_water
2312-
pysuezV2==2.0.1
2312+
pysuezV2==2.0.3
23132313

23142314
# homeassistant.components.switchbee
23152315
pyswitchbee==1.8.3

0 commit comments

Comments
 (0)