Skip to content

Commit

Permalink
2025.1.2 (#135241)
Browse files Browse the repository at this point in the history
  • Loading branch information
bramkragten authored Jan 9, 2025
2 parents d59a91a + 0027d90 commit bceccd8
Show file tree
Hide file tree
Showing 21 changed files with 165 additions and 46 deletions.
7 changes: 7 additions & 0 deletions homeassistant/components/backup/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta
from enum import StrEnum
import random
from typing import TYPE_CHECKING, Self, TypedDict

from cronsim import CronSim
Expand All @@ -28,6 +29,10 @@
CRON_PATTERN_DAILY = "45 4 * * *"
CRON_PATTERN_WEEKLY = "45 4 * * {}"

# Randomize the start time of the backup by up to 60 minutes to avoid
# all backups running at the same time.
BACKUP_START_TIME_JITTER = 60 * 60


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

next_time += timedelta(seconds=random.randint(0, BACKUP_START_TIME_JITTER))
LOGGER.debug("Scheduling next automatic backup at %s", next_time)
manager.remove_next_backup_event = async_track_point_in_time(
manager.hass, _create_backup, next_time
)
Expand Down
83 changes: 65 additions & 18 deletions homeassistant/components/cloud/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

from __future__ import annotations

import asyncio
import base64
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
import hashlib
import logging
import random
from typing import Any, Self

from aiohttp import ClientError, ClientTimeout, StreamReader
Expand All @@ -26,6 +28,9 @@

_LOGGER = logging.getLogger(__name__)
_STORAGE_BACKUP = "backup"
_RETRY_LIMIT = 5
_RETRY_SECONDS_MIN = 60
_RETRY_SECONDS_MAX = 600


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

try:
resp = await self._cloud.websession.get(details["url"])
resp = await self._cloud.websession.get(
details["url"],
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
)

resp.raise_for_status()
except ClientError as err:
raise BackupAgentError("Failed to download backup") from err

return ChunkAsyncStreamIterator(resp.content)

async def async_upload_backup(
async def _async_do_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
**kwargs: Any,
filename: str,
base64md5hash: str,
metadata: dict[str, Any],
size: int,
) -> None:
"""Upload a backup.
:param open_stream: A function returning an async iterator that yields bytes.
:param backup: Metadata about the backup that should be uploaded.
"""
if not backup.protected:
raise BackupAgentError("Cloud backups must be protected")

base64md5hash = await _b64md5(await open_stream())

"""Upload a backup."""
try:
details = await async_files_upload_details(
self._cloud,
storage_type=_STORAGE_BACKUP,
filename=self._get_backup_filename(),
metadata=backup.as_dict(),
size=backup.size,
filename=filename,
metadata=metadata,
size=size,
base64md5hash=base64md5hash,
)
except (ClientError, CloudError) as err:
Expand All @@ -178,7 +180,7 @@ async def async_upload_backup(
upload_status = await self._cloud.websession.put(
details["url"],
data=await open_stream(),
headers=details["headers"] | {"content-length": str(backup.size)},
headers=details["headers"] | {"content-length": str(size)},
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
)
_LOGGER.log(
Expand All @@ -190,6 +192,51 @@ async def async_upload_backup(
except (TimeoutError, ClientError) as err:
raise BackupAgentError("Failed to upload backup") from err

async def async_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
**kwargs: Any,
) -> None:
"""Upload a backup.
:param open_stream: A function returning an async iterator that yields bytes.
:param backup: Metadata about the backup that should be uploaded.
"""
if not backup.protected:
raise BackupAgentError("Cloud backups must be protected")

base64md5hash = await _b64md5(await open_stream())
filename = self._get_backup_filename()
metadata = backup.as_dict()
size = backup.size

tries = 1
while tries <= _RETRY_LIMIT:
try:
await self._async_do_upload_backup(
open_stream=open_stream,
filename=filename,
base64md5hash=base64md5hash,
metadata=metadata,
size=size,
)
break
except BackupAgentError as err:
if tries == _RETRY_LIMIT:
raise
tries += 1
retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX)
_LOGGER.info(
"Failed to upload backup, retrying (%s/%s) in %ss: %s",
tries,
_RETRY_LIMIT,
retry_timer,
err,
)
await asyncio.sleep(retry_timer)

async def async_delete_backup(
self,
backup_id: str,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cookidoo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["cookidoo_api"],
"quality_scale": "silver",
"requirements": ["cookidoo-api==0.11.2"]
"requirements": ["cookidoo-api==0.12.2"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/flick_electric/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyflick"],
"requirements": ["PyFlick==1.1.2"]
"requirements": ["PyFlick==1.1.3"]
}
6 changes: 3 additions & 3 deletions homeassistant/components/flick_electric/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ def native_value(self) -> Decimal:
_LOGGER.warning(
"Unexpected quantity for unit price: %s", self.coordinator.data
)
return self.coordinator.data.cost
return self.coordinator.data.cost * 100

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

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

components[component.charge_setter] = component.value
components[component.charge_setter] = float(component.value * 100)

return {
ATTR_START_AT: self.coordinator.data.start_at,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250106.0"]
"requirements": ["home-assistant-frontend==20250109.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/husqvarna_automower/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2024.12.0"]
"requirements": ["aioautomower==2025.1.0"]
}
12 changes: 10 additions & 2 deletions homeassistant/components/meteo_france/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from meteofrance_api.client import MeteoFranceClient
from meteofrance_api.helpers import is_valid_warning_department
from meteofrance_api.model import CurrentPhenomenons, Forecast, Rain
from requests import RequestException
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
Expand Down Expand Up @@ -83,7 +84,13 @@ async def _async_update_data_alert() -> CurrentPhenomenons:
update_method=_async_update_data_rain,
update_interval=SCAN_INTERVAL_RAIN,
)
await coordinator_rain.async_config_entry_first_refresh()
try:
await coordinator_rain._async_refresh(log_failures=False) # noqa: SLF001
except RequestException:
_LOGGER.warning(
"1 hour rain forecast not available: %s is not in covered zone",
entry.title,
)

department = coordinator_forecast.data.position.get("dept")
_LOGGER.debug(
Expand Down Expand Up @@ -128,8 +135,9 @@ async def _async_update_data_alert() -> CurrentPhenomenons:
hass.data[DOMAIN][entry.entry_id] = {
UNDO_UPDATE_LISTENER: undo_listener,
COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain,
}
if coordinator_rain and coordinator_rain.last_update_success:
hass.data[DOMAIN][entry.entry_id][COORDINATOR_RAIN] = coordinator_rain
if coordinator_alert and coordinator_alert.last_update_success:
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/meteo_france/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ async def async_setup_entry(
"""Set up the Meteo-France sensor platform."""
data = hass.data[DOMAIN][entry.entry_id]
coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN]
coordinator_rain: DataUpdateCoordinator[Rain] | None = data.get(COORDINATOR_RAIN)
coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
COORDINATOR_ALERT
)
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/overkiz/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from urllib.parse import urlparse

from pyoverkiz.enums import OverkizCommand, Protocol
from pyoverkiz.exceptions import OverkizException
from pyoverkiz.exceptions import BaseOverkizException
from pyoverkiz.models import Command, Device, StateDefinition
from pyoverkiz.types import StateType as OverkizStateType

Expand Down Expand Up @@ -105,7 +105,7 @@ async def async_execute_command(
"Home Assistant",
)
# Catch Overkiz exceptions to support `continue_on_error` functionality
except OverkizException as exception:
except BaseOverkizException as exception:
raise HomeAssistantError(exception) from exception

# ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/reolink/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ def get_device_uid_and_ch(
ch = int(device_uid[1][5:])
is_chime = True
else:
ch = host.api.channel_for_uid(device_uid[1])
device_uid_part = "_".join(device_uid[1:])
ch = host.api.channel_for_uid(device_uid_part)
return (device_uid, ch, is_chime)


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/suez_water/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pysuez", "regex"],
"quality_scale": "bronze",
"requirements": ["pysuezV2==2.0.1"]
"requirements": ["pysuezV2==2.0.3"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/zha/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def device_info(self) -> DeviceInfo:
manufacturer=zha_device_info[ATTR_MANUFACTURER],
model=zha_device_info[ATTR_MODEL],
name=zha_device_info[ATTR_NAME],
via_device=(DOMAIN, zha_gateway.state.node_info.ieee),
via_device=(DOMAIN, str(zha_gateway.state.node_info.ieee)),
)

@callback
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 1
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ habluetooth==3.7.0
hass-nabucasa==0.87.0
hassil==2.1.0
home-assistant-bluetooth==1.13.0
home-assistant-frontend==20250106.0
home-assistant-frontend==20250109.0
home-assistant-intents==2025.1.1
httpx==0.27.2
ifaddr==0.2.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "homeassistant"
version = "2025.1.1"
version = "2025.1.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
Expand Down
10 changes: 5 additions & 5 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ ProgettiHWSW==0.1.3
PyChromecast==14.0.5

# homeassistant.components.flick_electric
PyFlick==1.1.2
PyFlick==1.1.3

# homeassistant.components.flume
PyFlume==0.6.5
Expand Down Expand Up @@ -201,7 +201,7 @@ aioaseko==1.0.0
aioasuswrt==1.4.0

# homeassistant.components.husqvarna_automower
aioautomower==2024.12.0
aioautomower==2025.1.0

# homeassistant.components.azure_devops
aioazuredevops==2.2.1
Expand Down Expand Up @@ -704,7 +704,7 @@ connect-box==0.3.1
construct==2.10.68

# homeassistant.components.cookidoo
cookidoo-api==0.11.2
cookidoo-api==0.12.2

# homeassistant.components.backup
# homeassistant.components.utility_meter
Expand Down Expand Up @@ -1134,7 +1134,7 @@ hole==0.8.0
holidays==0.64

# homeassistant.components.frontend
home-assistant-frontend==20250106.0
home-assistant-frontend==20250109.0

# homeassistant.components.conversation
home-assistant-intents==2025.1.1
Expand Down Expand Up @@ -2309,7 +2309,7 @@ pysqueezebox==0.10.0
pystiebeleltron==0.0.1.dev2

# homeassistant.components.suez_water
pysuezV2==2.0.1
pysuezV2==2.0.3

# homeassistant.components.switchbee
pyswitchbee==1.8.3
Expand Down
Loading

0 comments on commit bceccd8

Please sign in to comment.