mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 22:57:17 +00:00
2025.1.2 (#135241)
This commit is contained in:
commit
bceccd85ee
@ -7,6 +7,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
import random
|
||||||
from typing import TYPE_CHECKING, Self, TypedDict
|
from typing import TYPE_CHECKING, Self, TypedDict
|
||||||
|
|
||||||
from cronsim import CronSim
|
from cronsim import CronSim
|
||||||
@ -28,6 +29,10 @@ if TYPE_CHECKING:
|
|||||||
CRON_PATTERN_DAILY = "45 4 * * *"
|
CRON_PATTERN_DAILY = "45 4 * * *"
|
||||||
CRON_PATTERN_WEEKLY = "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):
|
class StoredBackupConfig(TypedDict):
|
||||||
"""Represent the stored backup config."""
|
"""Represent the stored backup config."""
|
||||||
@ -329,6 +334,8 @@ class BackupSchedule:
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
LOGGER.exception("Unexpected error creating automatic backup")
|
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.remove_next_backup_event = async_track_point_in_time(
|
||||||
manager.hass, _create_backup, next_time
|
manager.hass, _create_backup, next_time
|
||||||
)
|
)
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
from aiohttp import ClientError, ClientTimeout, StreamReader
|
from aiohttp import ClientError, ClientTimeout, StreamReader
|
||||||
@ -26,6 +28,9 @@ from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_STORAGE_BACKUP = "backup"
|
_STORAGE_BACKUP = "backup"
|
||||||
|
_RETRY_LIMIT = 5
|
||||||
|
_RETRY_SECONDS_MIN = 60
|
||||||
|
_RETRY_SECONDS_MAX = 600
|
||||||
|
|
||||||
|
|
||||||
async def _b64md5(stream: AsyncIterator[bytes]) -> str:
|
async def _b64md5(stream: AsyncIterator[bytes]) -> str:
|
||||||
@ -138,13 +143,55 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
raise BackupAgentError("Failed to get download details") from err
|
raise BackupAgentError("Failed to get download details") from err
|
||||||
|
|
||||||
try:
|
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()
|
resp.raise_for_status()
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
raise BackupAgentError("Failed to download backup") from err
|
raise BackupAgentError("Failed to download backup") from err
|
||||||
|
|
||||||
return ChunkAsyncStreamIterator(resp.content)
|
return ChunkAsyncStreamIterator(resp.content)
|
||||||
|
|
||||||
|
async def _async_do_upload_backup(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||||
|
filename: str,
|
||||||
|
base64md5hash: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
size: int,
|
||||||
|
) -> None:
|
||||||
|
"""Upload a backup."""
|
||||||
|
try:
|
||||||
|
details = await async_files_upload_details(
|
||||||
|
self._cloud,
|
||||||
|
storage_type=_STORAGE_BACKUP,
|
||||||
|
filename=filename,
|
||||||
|
metadata=metadata,
|
||||||
|
size=size,
|
||||||
|
base64md5hash=base64md5hash,
|
||||||
|
)
|
||||||
|
except (ClientError, CloudError) as err:
|
||||||
|
raise BackupAgentError("Failed to get upload details") from err
|
||||||
|
|
||||||
|
try:
|
||||||
|
upload_status = await self._cloud.websession.put(
|
||||||
|
details["url"],
|
||||||
|
data=await open_stream(),
|
||||||
|
headers=details["headers"] | {"content-length": str(size)},
|
||||||
|
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
|
||||||
|
)
|
||||||
|
_LOGGER.log(
|
||||||
|
logging.DEBUG if upload_status.status < 400 else logging.WARNING,
|
||||||
|
"Backup upload status: %s",
|
||||||
|
upload_status.status,
|
||||||
|
)
|
||||||
|
upload_status.raise_for_status()
|
||||||
|
except (TimeoutError, ClientError) as err:
|
||||||
|
raise BackupAgentError("Failed to upload backup") from err
|
||||||
|
|
||||||
async def async_upload_backup(
|
async def async_upload_backup(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@ -161,34 +208,34 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
raise BackupAgentError("Cloud backups must be protected")
|
raise BackupAgentError("Cloud backups must be protected")
|
||||||
|
|
||||||
base64md5hash = await _b64md5(await open_stream())
|
base64md5hash = await _b64md5(await open_stream())
|
||||||
|
filename = self._get_backup_filename()
|
||||||
|
metadata = backup.as_dict()
|
||||||
|
size = backup.size
|
||||||
|
|
||||||
try:
|
tries = 1
|
||||||
details = await async_files_upload_details(
|
while tries <= _RETRY_LIMIT:
|
||||||
self._cloud,
|
try:
|
||||||
storage_type=_STORAGE_BACKUP,
|
await self._async_do_upload_backup(
|
||||||
filename=self._get_backup_filename(),
|
open_stream=open_stream,
|
||||||
metadata=backup.as_dict(),
|
filename=filename,
|
||||||
size=backup.size,
|
base64md5hash=base64md5hash,
|
||||||
base64md5hash=base64md5hash,
|
metadata=metadata,
|
||||||
)
|
size=size,
|
||||||
except (ClientError, CloudError) as err:
|
)
|
||||||
raise BackupAgentError("Failed to get upload details") from err
|
break
|
||||||
|
except BackupAgentError as err:
|
||||||
try:
|
if tries == _RETRY_LIMIT:
|
||||||
upload_status = await self._cloud.websession.put(
|
raise
|
||||||
details["url"],
|
tries += 1
|
||||||
data=await open_stream(),
|
retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX)
|
||||||
headers=details["headers"] | {"content-length": str(backup.size)},
|
_LOGGER.info(
|
||||||
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
|
"Failed to upload backup, retrying (%s/%s) in %ss: %s",
|
||||||
)
|
tries,
|
||||||
_LOGGER.log(
|
_RETRY_LIMIT,
|
||||||
logging.DEBUG if upload_status.status < 400 else logging.WARNING,
|
retry_timer,
|
||||||
"Backup upload status: %s",
|
err,
|
||||||
upload_status.status,
|
)
|
||||||
)
|
await asyncio.sleep(retry_timer)
|
||||||
upload_status.raise_for_status()
|
|
||||||
except (TimeoutError, ClientError) as err:
|
|
||||||
raise BackupAgentError("Failed to upload backup") from err
|
|
||||||
|
|
||||||
async def async_delete_backup(
|
async def async_delete_backup(
|
||||||
self,
|
self,
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["cookidoo_api"],
|
"loggers": ["cookidoo_api"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["cookidoo-api==0.11.2"]
|
"requirements": ["cookidoo-api==0.12.2"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyflick"],
|
"loggers": ["pyflick"],
|
||||||
"requirements": ["PyFlick==1.1.2"]
|
"requirements": ["PyFlick==1.1.3"]
|
||||||
}
|
}
|
||||||
|
@ -51,19 +51,19 @@ class FlickPricingSensor(CoordinatorEntity[FlickElectricDataCoordinator], Sensor
|
|||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Unexpected quantity for unit price: %s", self.coordinator.data
|
"Unexpected quantity for unit price: %s", self.coordinator.data
|
||||||
)
|
)
|
||||||
return self.coordinator.data.cost
|
return self.coordinator.data.cost * 100
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
components: dict[str, Decimal] = {}
|
components: dict[str, float] = {}
|
||||||
|
|
||||||
for component in self.coordinator.data.components:
|
for component in self.coordinator.data.components:
|
||||||
if component.charge_setter not in ATTR_COMPONENTS:
|
if component.charge_setter not in ATTR_COMPONENTS:
|
||||||
_LOGGER.warning("Found unknown component: %s", component.charge_setter)
|
_LOGGER.warning("Found unknown component: %s", component.charge_setter)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
components[component.charge_setter] = component.value
|
components[component.charge_setter] = float(component.value * 100)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_START_AT: self.coordinator.data.start_at,
|
ATTR_START_AT: self.coordinator.data.start_at,
|
||||||
|
@ -21,5 +21,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250106.0"]
|
"requirements": ["home-assistant-frontend==20250109.0"]
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioautomower"],
|
"loggers": ["aioautomower"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioautomower==2024.12.0"]
|
"requirements": ["aioautomower==2025.1.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import logging
|
|||||||
from meteofrance_api.client import MeteoFranceClient
|
from meteofrance_api.client import MeteoFranceClient
|
||||||
from meteofrance_api.helpers import is_valid_warning_department
|
from meteofrance_api.helpers import is_valid_warning_department
|
||||||
from meteofrance_api.model import CurrentPhenomenons, Forecast, Rain
|
from meteofrance_api.model import CurrentPhenomenons, Forecast, Rain
|
||||||
|
from requests import RequestException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -83,7 +84,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
update_method=_async_update_data_rain,
|
update_method=_async_update_data_rain,
|
||||||
update_interval=SCAN_INTERVAL_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")
|
department = coordinator_forecast.data.position.get("dept")
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@ -128,8 +135,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.data[DOMAIN][entry.entry_id] = {
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
UNDO_UPDATE_LISTENER: undo_listener,
|
UNDO_UPDATE_LISTENER: undo_listener,
|
||||||
COORDINATOR_FORECAST: coordinator_forecast,
|
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:
|
if coordinator_alert and coordinator_alert.last_update_success:
|
||||||
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
|
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ async def async_setup_entry(
|
|||||||
"""Set up the Meteo-France sensor platform."""
|
"""Set up the Meteo-France sensor platform."""
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
|
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: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
|
||||||
COORDINATOR_ALERT
|
COORDINATOR_ALERT
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ from typing import Any, cast
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pyoverkiz.enums import OverkizCommand, Protocol
|
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.models import Command, Device, StateDefinition
|
||||||
from pyoverkiz.types import StateType as OverkizStateType
|
from pyoverkiz.types import StateType as OverkizStateType
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ class OverkizExecutor:
|
|||||||
"Home Assistant",
|
"Home Assistant",
|
||||||
)
|
)
|
||||||
# Catch Overkiz exceptions to support `continue_on_error` functionality
|
# Catch Overkiz exceptions to support `continue_on_error` functionality
|
||||||
except OverkizException as exception:
|
except BaseOverkizException as exception:
|
||||||
raise HomeAssistantError(exception) from exception
|
raise HomeAssistantError(exception) from exception
|
||||||
|
|
||||||
# ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here
|
# ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here
|
||||||
|
@ -82,7 +82,8 @@ def get_device_uid_and_ch(
|
|||||||
ch = int(device_uid[1][5:])
|
ch = int(device_uid[1][5:])
|
||||||
is_chime = True
|
is_chime = True
|
||||||
else:
|
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)
|
return (device_uid, ch, is_chime)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pysuez", "regex"],
|
"loggers": ["pysuez", "regex"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pysuezV2==2.0.1"]
|
"requirements": ["pysuezV2==2.0.3"]
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
|
|||||||
manufacturer=zha_device_info[ATTR_MANUFACTURER],
|
manufacturer=zha_device_info[ATTR_MANUFACTURER],
|
||||||
model=zha_device_info[ATTR_MODEL],
|
model=zha_device_info[ATTR_MODEL],
|
||||||
name=zha_device_info[ATTR_NAME],
|
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
|
@callback
|
||||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2025
|
MAJOR_VERSION: Final = 2025
|
||||||
MINOR_VERSION: Final = 1
|
MINOR_VERSION: Final = 1
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||||
|
@ -35,7 +35,7 @@ habluetooth==3.7.0
|
|||||||
hass-nabucasa==0.87.0
|
hass-nabucasa==0.87.0
|
||||||
hassil==2.1.0
|
hassil==2.1.0
|
||||||
home-assistant-bluetooth==1.13.0
|
home-assistant-bluetooth==1.13.0
|
||||||
home-assistant-frontend==20250106.0
|
home-assistant-frontend==20250109.0
|
||||||
home-assistant-intents==2025.1.1
|
home-assistant-intents==2025.1.1
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2025.1.1"
|
version = "2025.1.2"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -48,7 +48,7 @@ ProgettiHWSW==0.1.3
|
|||||||
PyChromecast==14.0.5
|
PyChromecast==14.0.5
|
||||||
|
|
||||||
# homeassistant.components.flick_electric
|
# homeassistant.components.flick_electric
|
||||||
PyFlick==1.1.2
|
PyFlick==1.1.3
|
||||||
|
|
||||||
# homeassistant.components.flume
|
# homeassistant.components.flume
|
||||||
PyFlume==0.6.5
|
PyFlume==0.6.5
|
||||||
@ -201,7 +201,7 @@ aioaseko==1.0.0
|
|||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.husqvarna_automower
|
# homeassistant.components.husqvarna_automower
|
||||||
aioautomower==2024.12.0
|
aioautomower==2025.1.0
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==2.2.1
|
aioazuredevops==2.2.1
|
||||||
@ -704,7 +704,7 @@ connect-box==0.3.1
|
|||||||
construct==2.10.68
|
construct==2.10.68
|
||||||
|
|
||||||
# homeassistant.components.cookidoo
|
# homeassistant.components.cookidoo
|
||||||
cookidoo-api==0.11.2
|
cookidoo-api==0.12.2
|
||||||
|
|
||||||
# homeassistant.components.backup
|
# homeassistant.components.backup
|
||||||
# homeassistant.components.utility_meter
|
# homeassistant.components.utility_meter
|
||||||
@ -1134,7 +1134,7 @@ hole==0.8.0
|
|||||||
holidays==0.64
|
holidays==0.64
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250106.0
|
home-assistant-frontend==20250109.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.1.1
|
home-assistant-intents==2025.1.1
|
||||||
@ -2309,7 +2309,7 @@ pysqueezebox==0.10.0
|
|||||||
pystiebeleltron==0.0.1.dev2
|
pystiebeleltron==0.0.1.dev2
|
||||||
|
|
||||||
# homeassistant.components.suez_water
|
# homeassistant.components.suez_water
|
||||||
pysuezV2==2.0.1
|
pysuezV2==2.0.3
|
||||||
|
|
||||||
# homeassistant.components.switchbee
|
# homeassistant.components.switchbee
|
||||||
pyswitchbee==1.8.3
|
pyswitchbee==1.8.3
|
||||||
|
@ -45,7 +45,7 @@ ProgettiHWSW==0.1.3
|
|||||||
PyChromecast==14.0.5
|
PyChromecast==14.0.5
|
||||||
|
|
||||||
# homeassistant.components.flick_electric
|
# homeassistant.components.flick_electric
|
||||||
PyFlick==1.1.2
|
PyFlick==1.1.3
|
||||||
|
|
||||||
# homeassistant.components.flume
|
# homeassistant.components.flume
|
||||||
PyFlume==0.6.5
|
PyFlume==0.6.5
|
||||||
@ -189,7 +189,7 @@ aioaseko==1.0.0
|
|||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.husqvarna_automower
|
# homeassistant.components.husqvarna_automower
|
||||||
aioautomower==2024.12.0
|
aioautomower==2025.1.0
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==2.2.1
|
aioazuredevops==2.2.1
|
||||||
@ -600,7 +600,7 @@ colorthief==0.2.1
|
|||||||
construct==2.10.68
|
construct==2.10.68
|
||||||
|
|
||||||
# homeassistant.components.cookidoo
|
# homeassistant.components.cookidoo
|
||||||
cookidoo-api==0.11.2
|
cookidoo-api==0.12.2
|
||||||
|
|
||||||
# homeassistant.components.backup
|
# homeassistant.components.backup
|
||||||
# homeassistant.components.utility_meter
|
# homeassistant.components.utility_meter
|
||||||
@ -963,7 +963,7 @@ hole==0.8.0
|
|||||||
holidays==0.64
|
holidays==0.64
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250106.0
|
home-assistant-frontend==20250109.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.1.1
|
home-assistant-intents==2025.1.1
|
||||||
@ -1875,7 +1875,7 @@ pyspeex-noise==1.0.2
|
|||||||
pysqueezebox==0.10.0
|
pysqueezebox==0.10.0
|
||||||
|
|
||||||
# homeassistant.components.suez_water
|
# homeassistant.components.suez_water
|
||||||
pysuezV2==2.0.1
|
pysuezV2==2.0.3
|
||||||
|
|
||||||
# homeassistant.components.switchbee
|
# homeassistant.components.switchbee
|
||||||
pyswitchbee==1.8.3
|
pyswitchbee==1.8.3
|
||||||
|
@ -1345,6 +1345,7 @@ async def test_config_update_errors(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@patch("homeassistant.components.backup.config.BACKUP_START_TIME_JITTER", 0)
|
||||||
async def test_config_schedule_logic(
|
async def test_config_schedule_logic(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
@ -1787,6 +1788,7 @@ async def test_config_schedule_logic(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@patch("homeassistant.components.backup.config.BACKUP_START_TIME_JITTER", 0)
|
||||||
async def test_config_retention_copies_logic(
|
async def test_config_retention_copies_logic(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
@ -389,6 +389,7 @@ async def test_agents_upload_fail_put(
|
|||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
mock_get_upload_details: Mock,
|
mock_get_upload_details: Mock,
|
||||||
put_mock_kwargs: dict[str, Any],
|
put_mock_kwargs: dict[str, Any],
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test agent upload backup fails."""
|
"""Test agent upload backup fails."""
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@ -417,6 +418,9 @@ async def test_agents_upload_fail_put(
|
|||||||
return_value=test_backup,
|
return_value=test_backup,
|
||||||
),
|
),
|
||||||
patch("pathlib.Path.open") as mocked_open,
|
patch("pathlib.Path.open") as mocked_open,
|
||||||
|
patch("homeassistant.components.cloud.backup.asyncio.sleep"),
|
||||||
|
patch("homeassistant.components.cloud.backup.random.randint", return_value=60),
|
||||||
|
patch("homeassistant.components.cloud.backup._RETRY_LIMIT", 2),
|
||||||
):
|
):
|
||||||
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
||||||
fetch_backup.return_value = test_backup
|
fetch_backup.return_value = test_backup
|
||||||
@ -426,6 +430,8 @@ async def test_agents_upload_fail_put(
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 2
|
||||||
|
assert "Failed to upload backup, retrying (2/2) in 60s" in caplog.text
|
||||||
assert resp.status == 201
|
assert resp.status == 201
|
||||||
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
|
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
|
||||||
assert len(store_backups) == 1
|
assert len(store_backups) == 1
|
||||||
@ -469,6 +475,7 @@ async def test_agents_upload_fail_cloud(
|
|||||||
return_value=test_backup,
|
return_value=test_backup,
|
||||||
),
|
),
|
||||||
patch("pathlib.Path.open") as mocked_open,
|
patch("pathlib.Path.open") as mocked_open,
|
||||||
|
patch("homeassistant.components.cloud.backup.asyncio.sleep"),
|
||||||
):
|
):
|
||||||
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
||||||
fetch_backup.return_value = test_backup
|
fetch_backup.return_value = test_backup
|
||||||
|
47
tests/components/zha/test_entity.py
Normal file
47
tests/components/zha/test_entity.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Test ZHA entities."""
|
||||||
|
|
||||||
|
from zigpy.profiles import zha
|
||||||
|
from zigpy.zcl.clusters import general
|
||||||
|
|
||||||
|
from homeassistant.components.zha.helpers import get_zha_gateway
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_registry_via_device(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_zha,
|
||||||
|
zigpy_device_mock,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test ZHA `via_device` is set correctly."""
|
||||||
|
|
||||||
|
await setup_zha()
|
||||||
|
gateway = get_zha_gateway(hass)
|
||||||
|
|
||||||
|
zigpy_device = zigpy_device_mock(
|
||||||
|
{
|
||||||
|
1: {
|
||||||
|
SIG_EP_INPUT: [general.Basic.cluster_id],
|
||||||
|
SIG_EP_OUTPUT: [],
|
||||||
|
SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
|
||||||
|
SIG_EP_PROFILE: zha.PROFILE_ID,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
zha_device = gateway.get_or_create_device(zigpy_device)
|
||||||
|
await gateway.async_device_initialized(zigpy_device)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
reg_coordinator_device = device_registry.async_get_device(
|
||||||
|
identifiers={("zha", str(gateway.state.node_info.ieee))}
|
||||||
|
)
|
||||||
|
|
||||||
|
reg_device = device_registry.async_get_device(
|
||||||
|
identifiers={("zha", str(zha_device.ieee))}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert reg_device.via_device_id == reg_coordinator_device.id
|
Loading…
x
Reference in New Issue
Block a user