mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +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 datetime import datetime, timedelta
|
||||
from enum import StrEnum
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Self, TypedDict
|
||||
|
||||
from cronsim import CronSim
|
||||
@ -28,6 +29,10 @@ if TYPE_CHECKING:
|
||||
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."""
|
||||
@ -329,6 +334,8 @@ class BackupSchedule:
|
||||
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
|
||||
)
|
||||
|
@ -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
|
||||
@ -26,6 +28,9 @@ from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT
|
||||
|
||||
_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:
|
||||
@ -138,13 +143,55 @@ class CloudBackupAgent(BackupAgent):
|
||||
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_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(
|
||||
self,
|
||||
*,
|
||||
@ -161,34 +208,34 @@ class CloudBackupAgent(BackupAgent):
|
||||
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
|
||||
|
||||
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,
|
||||
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(backup.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
|
||||
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,
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyflick"],
|
||||
"requirements": ["PyFlick==1.1.2"]
|
||||
"requirements": ["PyFlick==1.1.3"]
|
||||
}
|
||||
|
@ -51,19 +51,19 @@ class FlickPricingSensor(CoordinatorEntity[FlickElectricDataCoordinator], Sensor
|
||||
_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,
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"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.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
|
||||
@ -83,7 +84,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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(
|
||||
@ -128,8 +135,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ from typing import Any, cast
|
||||
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
|
||||
|
||||
@ -105,7 +105,7 @@ class OverkizExecutor:
|
||||
"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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pysuez", "regex"],
|
||||
"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],
|
||||
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
|
||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
@ -45,7 +45,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
|
||||
@ -189,7 +189,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
|
||||
@ -600,7 +600,7 @@ colorthief==0.2.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
|
||||
@ -963,7 +963,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
|
||||
@ -1875,7 +1875,7 @@ pyspeex-noise==1.0.2
|
||||
pysqueezebox==0.10.0
|
||||
|
||||
# homeassistant.components.suez_water
|
||||
pysuezV2==2.0.1
|
||||
pysuezV2==2.0.3
|
||||
|
||||
# homeassistant.components.switchbee
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
|
@ -389,6 +389,7 @@ async def test_agents_upload_fail_put(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
mock_get_upload_details: Mock,
|
||||
put_mock_kwargs: dict[str, Any],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test agent upload backup fails."""
|
||||
client = await hass_client()
|
||||
@ -417,6 +418,9 @@ async def test_agents_upload_fail_put(
|
||||
return_value=test_backup,
|
||||
),
|
||||
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""])
|
||||
fetch_backup.return_value = test_backup
|
||||
@ -426,6 +430,8 @@ async def test_agents_upload_fail_put(
|
||||
)
|
||||
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
|
||||
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
|
||||
assert len(store_backups) == 1
|
||||
@ -469,6 +475,7 @@ async def test_agents_upload_fail_cloud(
|
||||
return_value=test_backup,
|
||||
),
|
||||
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""])
|
||||
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