mirror of
https://github.com/home-assistant/core.git
synced 2026-06-16 17:02:57 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f202c10b6 |
@@ -193,7 +193,7 @@ jobs:
|
||||
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder/actions/build-image@4de35182ce1e329181bffcbcc84d33db5e2c7e10 # 2026.06.0
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
build-args: |
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build machine image
|
||||
uses: home-assistant/builder/actions/build-image@4de35182ce1e329181bffcbcc84d33db5e2c7e10 # 2026.06.0
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
build-args: |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.4.8"],
|
||||
"requirements": ["aioairq==0.4.7"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"properties": {
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.const import CONF_MAC, CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -87,12 +87,9 @@ class AnthemAVR(MediaPlayerEntity):
|
||||
via_device=(DOMAIN, mac_address),
|
||||
)
|
||||
else:
|
||||
# Zone 1 is the physical receiver that owns the network MAC; higher
|
||||
# zones are via_device children and carry no connection.
|
||||
self._attr_unique_id = mac_address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, mac_address)},
|
||||
connections={(CONNECTION_NETWORK_MAC, mac_address)},
|
||||
name=name,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=model,
|
||||
|
||||
@@ -52,7 +52,10 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Service integration, no discovery.
|
||||
docs-data-update: done
|
||||
docs-data-update:
|
||||
status: exempt
|
||||
comment: |
|
||||
No data updates.
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
|
||||
@@ -193,7 +193,6 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
|
||||
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, data[Attribute.MAC_ADDRESS])},
|
||||
name=self.create_device_name(data),
|
||||
manufacturer="Aprilaire",
|
||||
)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aten_pe",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["atenpdu==0.3.6"]
|
||||
"requirements": ["atenpdu==0.3.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["webexpythonsdk"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["webexpythonsdk==2.0.6"]
|
||||
"requirements": ["webexpythonsdk==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==3.0.12"],
|
||||
"requirements": ["DoorBirdPy==3.0.11"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"properties": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pdunehd"],
|
||||
"requirements": ["pdunehd==1.3.3"]
|
||||
"requirements": ["pdunehd==1.3.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhomematic"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyhomematic==0.1.78"]
|
||||
"requirements": ["pyhomematic==0.1.77"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -41,7 +41,6 @@ class IAlarmPanel(
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.mac)},
|
||||
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)},
|
||||
manufacturer="Antifurto365 - Meian",
|
||||
name="iAlarm",
|
||||
)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/influxdb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["influxdb", "influxdb_client"],
|
||||
"requirements": ["influxdb==5.3.2", "influxdb-client==1.50.0"],
|
||||
"requirements": ["influxdb==5.3.1", "influxdb-client==1.50.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["messagebird"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["messagebird==1.2.1"]
|
||||
"requirements": ["messagebird==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["config", "omnilogic"],
|
||||
"requirements": ["omnilogic==0.4.9"],
|
||||
"requirements": ["omnilogic==0.4.5"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["hole"],
|
||||
"requirements": ["hole==0.9.2"]
|
||||
"requirements": ["hole==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -352,8 +352,6 @@ class PS4Device(MediaPlayerEntity):
|
||||
for device in d_registry.devices.get_devices_for_config_entry_id(
|
||||
self._entry_id
|
||||
):
|
||||
# Rebuilt from the existing device entry, which already carries
|
||||
# the network MAC connection added by the live-status branch.
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers=device.identifiers,
|
||||
manufacturer=device.manufacturer,
|
||||
@@ -367,9 +365,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
_sw_version = status["system-version"]
|
||||
_sw_version = _sw_version[1:4]
|
||||
sw_version = f"{_sw_version[0]}.{_sw_version[1:]}"
|
||||
# status["host-id"] is the console's network MAC address.
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, status["host-id"])},
|
||||
identifiers={(DOMAIN, status["host-id"])},
|
||||
manufacturer="Sony Interactive Entertainment Inc.",
|
||||
model="PlayStation 4",
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
from rabbitair import Model
|
||||
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -36,7 +36,6 @@ class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
|
||||
self._attr_unique_id = entry.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.data[CONF_MAC])},
|
||||
connections={(CONNECTION_NETWORK_MAC, entry.data[CONF_MAC])},
|
||||
manufacturer="Rabbit Air",
|
||||
model=MODELS.get(coordinator.data.model),
|
||||
name=entry.title,
|
||||
|
||||
@@ -13,10 +13,9 @@ from pyrainbird.async_client import (
|
||||
)
|
||||
from pyrainbird.data import ModelAndVersion, Schedule
|
||||
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS
|
||||
@@ -105,18 +104,13 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
||||
"""Return information about the device."""
|
||||
if self._unique_id is None:
|
||||
return None
|
||||
device_info = DeviceInfo(
|
||||
return DeviceInfo(
|
||||
name=self.device_name,
|
||||
identifiers={(DOMAIN, self._unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=self._model_info.model_name,
|
||||
sw_version=f"{self._model_info.major}.{self._model_info.minor}",
|
||||
)
|
||||
# The unique id is the formatted MAC for current config entries, but was
|
||||
# historically the serial number, so derive the connection from the MAC.
|
||||
if mac_address := self.config_entry.data.get(CONF_MAC):
|
||||
device_info["connections"] = {(CONNECTION_NETWORK_MAC, mac_address)}
|
||||
return device_info
|
||||
|
||||
async def _async_update_data(self) -> RainbirdDeviceState:
|
||||
"""Fetch data from Rain Bird device."""
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["paho_mqtt", "roombapy"],
|
||||
"requirements": ["roombapy==1.9.1"],
|
||||
"requirements": ["roombapy==1.9.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "irobot-*",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "assumed_state",
|
||||
"loggers": ["tellcore"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["tellcore-net==0.4", "tellcore-py==1.1.3"]
|
||||
"requirements": ["tellcore-net==0.4", "tellcore-py==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -160,11 +160,7 @@ class UnifiAccessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data=merged_input,
|
||||
)
|
||||
|
||||
name = (
|
||||
discovery_info.get("name")
|
||||
or discovery_info.get("hostname")
|
||||
or discovery_info.get("product_name")
|
||||
)
|
||||
name = discovery_info.get("hostname") or discovery_info.get("platform")
|
||||
if not name:
|
||||
short_mac = discovery_info["hw_addr"].replace(":", "").upper()[-6:]
|
||||
name = f"Access {short_mac}"
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"protect_api_key": "This API key is associated with UniFi Protect, not UniFi Access. Please generate a new API key from the UniFi Access application settings.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"flow_title": "{name} ({ip_address})",
|
||||
"step": {
|
||||
"discovery_confirm": {
|
||||
"data": {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""Diagnostics support for the Yoto integration."""
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import YotoConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"mac",
|
||||
"network_ssid",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: YotoConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"players": async_redact_data(
|
||||
{
|
||||
player_id: asdict(player)
|
||||
for player_id, player in coordinator.data.items()
|
||||
},
|
||||
TO_REDACT,
|
||||
),
|
||||
}
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["yoto_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["yoto-api==4.3.0"]
|
||||
"requirements": ["yoto-api==4.2.1"]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: The integration supports local DHCP discovery (via hostname pattern), but does not implement a separate discovery update handling flow.
|
||||
|
||||
@@ -115,6 +115,9 @@ from .trace import (
|
||||
)
|
||||
from .typing import UNDEFINED, ConfigType, TemplateVarsType, UndefinedType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.recorder import Recorder
|
||||
|
||||
ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
|
||||
FROM_CONFIG_FORMAT = "{}_from_config"
|
||||
VALIDATE_CONFIG_FORMAT = "{}_validate_config"
|
||||
@@ -201,6 +204,7 @@ async def async_setup(hass: HomeAssistant) -> None:
|
||||
hass.data[CONDITION_DISABLED_CONDITIONS] = set()
|
||||
hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS] = []
|
||||
hass.data[CONDITIONS] = {}
|
||||
hass.data[_DATA_HISTORY_PRIMING_MANAGER] = _HistoryPrimingManager(hass)
|
||||
|
||||
async def new_triggers_conditions_listener(
|
||||
_event_data: labs.EventLabsUpdatedData,
|
||||
@@ -469,6 +473,79 @@ ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
_DATA_HISTORY_PRIMING_MANAGER: HassKey[_HistoryPrimingManager] = HassKey(
|
||||
"condition_history_priming_manager"
|
||||
)
|
||||
|
||||
|
||||
class _HistoryPrimingManager:
|
||||
"""Serialize and coalesce the recorder reads that prime condition durations.
|
||||
|
||||
At startup many conditions may prime at once. Letting each hit the recorder
|
||||
independently would force a separate commit per condition and run every read
|
||||
on the shared DB executor in parallel — a flood. So the reads run one at a
|
||||
time, and a single commit flush is shared by each "generation" of conditions
|
||||
that arrive while the previous flush is running.
|
||||
|
||||
The flush a condition relies on must begin after that condition started
|
||||
tracking its entities, or the read could miss a change still queued in the
|
||||
recorder and compute too generous an anchor. A condition therefore never
|
||||
rides a flush that was already running when it arrived (the lobby); it waits
|
||||
that one out and joins the next.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the manager."""
|
||||
self._hass = hass
|
||||
self._flush_condition = asyncio.Condition()
|
||||
self._flushing = False
|
||||
self._query_lock = asyncio.Lock()
|
||||
|
||||
async def async_prime[_T](
|
||||
self, job: Callable[[Recorder], Coroutine[Any, Any, _T]]
|
||||
) -> _T:
|
||||
"""Flush the recorder, then run `job`, coordinated with other primings."""
|
||||
await self._async_flush()
|
||||
async with self._query_lock:
|
||||
return await job(get_instance(self._hass))
|
||||
|
||||
async def _async_flush(self) -> None:
|
||||
"""Return once a recorder flush that began no earlier than this call ends.
|
||||
|
||||
The first condition of a generation performs the flush; the rest ride it.
|
||||
"""
|
||||
async with self._flush_condition:
|
||||
# Lobby: a flush already running began before we arrived, so it may
|
||||
# not capture our entity's queued changes. Wait it out, don't ride it.
|
||||
if self._flushing:
|
||||
await self._flush_condition.wait()
|
||||
|
||||
do_flush = False
|
||||
while True:
|
||||
async with self._flush_condition:
|
||||
if not self._flushing:
|
||||
# First past the lobby this generation: we run the flush.
|
||||
self._flushing = True
|
||||
do_flush = True
|
||||
break
|
||||
# A peer began a fresh flush after we cleared the lobby; it
|
||||
# covers us too, so wait for it and ride it.
|
||||
await self._flush_condition.wait()
|
||||
break
|
||||
|
||||
if not do_flush:
|
||||
return
|
||||
|
||||
instance = get_instance(self._hass)
|
||||
try:
|
||||
if (commit_future := instance.async_get_commit_future()) is not None:
|
||||
await commit_future
|
||||
finally:
|
||||
async with self._flush_condition:
|
||||
self._flushing = False
|
||||
self._flush_condition.notify_all()
|
||||
|
||||
|
||||
class EntityConditionBase(Condition):
|
||||
"""Base class for entity conditions."""
|
||||
|
||||
@@ -668,28 +745,34 @@ class EntityConditionBase(Condition):
|
||||
assert self._duration is not None
|
||||
lookback = min(self._duration, MAX_HISTORY_PRIMING_LOOKBACK)
|
||||
start_time = dt_util.utcnow() - lookback
|
||||
instance = get_instance(self._hass)
|
||||
try:
|
||||
async with asyncio.timeout(HISTORY_PRIMING_TIMEOUT):
|
||||
# The history query only sees committed rows. Wait for the
|
||||
# recorder to flush its queue first.
|
||||
if (commit_future := instance.async_get_commit_future()) is not None:
|
||||
await commit_future
|
||||
historical_states = await instance.async_add_executor_job(
|
||||
ft.partial(
|
||||
history.get_significant_states,
|
||||
self._hass,
|
||||
start_time,
|
||||
entity_ids=list(anchors),
|
||||
include_start_time_state=True,
|
||||
# Mandatory: the default (True) drops attribute-only
|
||||
# changes for entities outside SIGNIFICANT_DOMAINS, which
|
||||
# are exactly the transitions attribute-based conditions
|
||||
# depend on.
|
||||
significant_changes_only=False,
|
||||
minimal_response=False,
|
||||
)
|
||||
|
||||
async def _read_history(
|
||||
instance: Recorder,
|
||||
) -> dict[str, list[State | dict[str, Any]]]:
|
||||
# The history query only sees committed rows; the priming manager
|
||||
# flushes the recorder queue before running this.
|
||||
return await instance.async_add_executor_job(
|
||||
ft.partial(
|
||||
history.get_significant_states,
|
||||
self._hass,
|
||||
start_time,
|
||||
entity_ids=list(anchors),
|
||||
include_start_time_state=True,
|
||||
# Mandatory: the default (True) drops attribute-only changes
|
||||
# for entities outside SIGNIFICANT_DOMAINS, which are exactly
|
||||
# the transitions attribute-based conditions depend on.
|
||||
significant_changes_only=False,
|
||||
minimal_response=False,
|
||||
)
|
||||
)
|
||||
|
||||
manager = self._hass.data[_DATA_HISTORY_PRIMING_MANAGER]
|
||||
try:
|
||||
# The timeout also covers waiting for our turn, so under a flood of
|
||||
# primings a condition falls back to its conservative anchor rather
|
||||
# than blocking on the queue indefinitely.
|
||||
async with asyncio.timeout(HISTORY_PRIMING_TIMEOUT):
|
||||
historical_states = await manager.async_prime(_read_history)
|
||||
except (SQLAlchemyError, TimeoutError) as err:
|
||||
# Best effort: keep the conservative anchors rather than failing.
|
||||
_LOGGER.debug("Error priming condition durations from history: %s", err)
|
||||
|
||||
Generated
+13
-13
@@ -13,7 +13,7 @@ AIOSomecomfort==0.0.35
|
||||
Adax-local==0.3.0
|
||||
|
||||
# homeassistant.components.doorbird
|
||||
DoorBirdPy==3.0.12
|
||||
DoorBirdPy==3.0.11
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==5.0.0
|
||||
@@ -181,7 +181,7 @@ aio-ownet==0.0.5
|
||||
aioacaia==0.1.18
|
||||
|
||||
# homeassistant.components.airq
|
||||
aioairq==0.4.8
|
||||
aioairq==0.4.7
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.7.2
|
||||
@@ -584,7 +584,7 @@ asyncsleepiq==1.7.1
|
||||
asyncssh==2.21.0
|
||||
|
||||
# homeassistant.components.aten_pe
|
||||
# atenpdu==0.3.6
|
||||
# atenpdu==0.3.2
|
||||
|
||||
# homeassistant.components.aurora
|
||||
auroranoaa==0.0.5
|
||||
@@ -1268,7 +1268,7 @@ hko==0.3.2
|
||||
hlk-sw16==0.0.9
|
||||
|
||||
# homeassistant.components.pi_hole
|
||||
hole==0.9.2
|
||||
hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
@@ -1365,7 +1365,7 @@ indevolt-api==1.8.5
|
||||
influxdb-client==1.50.0
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb==5.3.2
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.infrared
|
||||
infrared-protocols==6.0.1
|
||||
@@ -1559,7 +1559,7 @@ medcom-ble==0.1.1
|
||||
melnor-bluetooth==0.0.25
|
||||
|
||||
# homeassistant.components.message_bird
|
||||
messagebird==1.2.1
|
||||
messagebird==1.2.0
|
||||
|
||||
# homeassistant.components.meteo_lt
|
||||
meteo-lt-pkg==0.2.4
|
||||
@@ -1739,7 +1739,7 @@ ohme==1.9.1
|
||||
ollama==0.6.2
|
||||
|
||||
# homeassistant.components.omnilogic
|
||||
omnilogic==0.4.9
|
||||
omnilogic==0.4.5
|
||||
|
||||
# homeassistant.components.ondilo_ico
|
||||
ondilo==0.5.0
|
||||
@@ -1824,7 +1824,7 @@ panacotta==0.2
|
||||
panasonic-viera==0.4.4
|
||||
|
||||
# homeassistant.components.dunehd
|
||||
pdunehd==1.3.3
|
||||
pdunehd==1.3.2
|
||||
|
||||
# homeassistant.components.peblar
|
||||
peblar==0.5.1
|
||||
@@ -2229,7 +2229,7 @@ pyheos==1.0.6
|
||||
pyhive-integration==1.0.9
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.78
|
||||
pyhomematic==0.1.77
|
||||
|
||||
# homeassistant.components.homeworks
|
||||
pyhomeworks==1.1.2
|
||||
@@ -2932,7 +2932,7 @@ rokuecp==0.19.5
|
||||
romy==0.0.10
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.9.1
|
||||
roombapy==1.9.0
|
||||
|
||||
# homeassistant.components.roon
|
||||
roonapi==0.1.6
|
||||
@@ -3142,7 +3142,7 @@ tapsaff==0.2.1
|
||||
tellcore-net==0.4
|
||||
|
||||
# homeassistant.components.tellstick
|
||||
tellcore-py==1.1.3
|
||||
tellcore-py==1.1.2
|
||||
|
||||
# homeassistant.components.tellduslive
|
||||
tellduslive==0.10.12
|
||||
@@ -3360,7 +3360,7 @@ watergate-local-api==2025.1.0
|
||||
weatherflow4py==1.5.4
|
||||
|
||||
# homeassistant.components.cisco_webex_teams
|
||||
webexpythonsdk==2.0.6
|
||||
webexpythonsdk==2.0.1
|
||||
|
||||
# homeassistant.components.nasweb
|
||||
webio-api==0.1.12
|
||||
@@ -3439,7 +3439,7 @@ yeelightsunflower==0.0.10
|
||||
yolink-api==0.6.5
|
||||
|
||||
# homeassistant.components.yoto
|
||||
yoto-api==4.3.0
|
||||
yoto-api==4.2.1
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==2.2.0
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_registry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'00:00:00:00:00:01',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'anthemav',
|
||||
'00:00:00:00:00:01',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Anthem',
|
||||
'model': 'MRX 520',
|
||||
'model_id': None,
|
||||
'name': 'Anthem AV',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -5,13 +5,10 @@ from unittest.mock import ANY, AsyncMock, patch
|
||||
|
||||
from anthemav.device_error import DeviceError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.anthemav.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -37,19 +34,6 @@ async def test_load_unload_config_entry(
|
||||
mock_anthemav.close.assert_called_once()
|
||||
|
||||
|
||||
async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the device registry entry, including the network MAC connection."""
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "00:00:00:00:00:01")}
|
||||
)
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error", [OSError, DeviceError])
|
||||
async def test_config_entry_not_ready_when_oserror(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, error: Exception
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_registry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'12:34:56:78:90:ab',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': 'Rev. B',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'aprilaire',
|
||||
'12:34:56:78:90:ab',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Aprilaire',
|
||||
'model': '8476W',
|
||||
'model_id': None,
|
||||
'name': 'Aprilaire',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': '1.05',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -1,52 +0,0 @@
|
||||
"""Tests for the Aprilaire integration setup."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyaprilaire.const import Attribute
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.aprilaire.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the device registry entry, including the network MAC connection."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="12:34:56:78:90:ab",
|
||||
data={CONF_HOST: "localhost", CONF_PORT: 7000},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
client = AsyncMock()
|
||||
client.data = {
|
||||
Attribute.MAC_ADDRESS: "1234567890ab",
|
||||
Attribute.NAME: "Aprilaire",
|
||||
Attribute.MODEL_NUMBER: 0,
|
||||
Attribute.HARDWARE_REVISION: ord("B"),
|
||||
Attribute.FIRMWARE_MAJOR_REVISION: 1,
|
||||
Attribute.FIRMWARE_MINOR_REVISION: 5,
|
||||
Attribute.THERMOSTAT_MODES: 0,
|
||||
Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS: 0,
|
||||
Attribute.CONNECTED: True,
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aprilaire.coordinator.pyaprilaire.client.AprilaireClient",
|
||||
return_value=client,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "12:34:56:78:90:ab")}
|
||||
)
|
||||
assert device_entry == snapshot
|
||||
@@ -1,36 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_registry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'00:00:54:12:34:56',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'ialarm',
|
||||
'00:00:54:12:34:56',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Antifurto365 - Meian',
|
||||
'model': None,
|
||||
'model_id': None,
|
||||
'name': 'iAlarm',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -1,16 +1,14 @@
|
||||
"""Test the Antifurto365 iAlarm init."""
|
||||
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import Mock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.ialarm.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -56,26 +54,6 @@ async def test_setup_not_ready(
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
ialarm_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the device registry entry, including the network MAC connection."""
|
||||
ialarm_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56")
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "00:00:54:12:34:56")}
|
||||
)
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant, ialarm_api, mock_config_entry) -> None:
|
||||
"""Test being able to unload an entry."""
|
||||
ialarm_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56")
|
||||
|
||||
@@ -232,7 +232,7 @@ def _create_mocked_hole(
|
||||
incorrect_app_password or password not in ["newkey", "apikey"]
|
||||
):
|
||||
raise HoleError("Authentication failed: Invalid password")
|
||||
raise HoleConnectionError("Connection error")
|
||||
raise HoleConnectionError
|
||||
|
||||
async def get_data_side_effect(*_args, **_kwargs):
|
||||
"""Return data based on the mocked Hole instance state."""
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_registry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'a0:00:0a:0a:a0:00',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'ps4',
|
||||
'A0000A0AA000',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Sony Interactive Entertainment Inc.',
|
||||
'model': 'PlayStation 4',
|
||||
'model_id': None,
|
||||
'name': 'Fake PS4',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': '9.87',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -6,7 +6,6 @@ from unittest.mock import MagicMock, patch
|
||||
from pyps4_2ndscreen.credential import get_ddp_message
|
||||
from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT
|
||||
from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import ps4
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -324,24 +323,6 @@ async def test_device_info_is_set_from_status_correctly(
|
||||
assert mock_entry.identifiers == {(DOMAIN, MOCK_HOST_ID)}
|
||||
|
||||
|
||||
async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
patch_get_status: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the device registry entry, including the network MAC connection."""
|
||||
patch_get_status.return_value = MOCK_STATUS_STANDBY
|
||||
await setup_mock_component(hass)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, MOCK_HOST_ID)}
|
||||
)
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
async def test_device_info_is_assummed(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_registry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'01:23:45:67:89:ab',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0.4',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'rabbitair',
|
||||
'01:23:45:67:89:AB',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Rabbit Air',
|
||||
'model': 'A3',
|
||||
'model_id': None,
|
||||
'name': 'Rabbit Air',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': '2.3.17',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Test Rabbit Air integration setup."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from rabbitair import Mode, Model, Speed
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.rabbitair.const import DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_HOST = "1.1.1.1"
|
||||
TEST_TOKEN = "0123456789abcdef0123456789abcdef"
|
||||
TEST_MAC = "01:23:45:67:89:AB"
|
||||
TEST_FIRMWARE = "2.3.17"
|
||||
TEST_HARDWARE = "1.0.0.4"
|
||||
TEST_TITLE = "Rabbit Air"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None:
|
||||
"""Mock zeroconf in all tests."""
|
||||
|
||||
|
||||
def get_mock_state() -> Mock:
|
||||
"""Return a mock device state instance."""
|
||||
mock_state = Mock()
|
||||
mock_state.model = Model.A3
|
||||
mock_state.main_firmware = TEST_HARDWARE
|
||||
mock_state.power = True
|
||||
mock_state.mode = Mode.Auto
|
||||
mock_state.speed = Speed.Low
|
||||
mock_state.wifi_firmware = TEST_FIRMWARE
|
||||
return mock_state
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rabbitair_connect() -> Generator[None]:
|
||||
"""Mock connection."""
|
||||
with patch("rabbitair.UdpClient.get_state", return_value=get_mock_state()):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("rabbitair_connect")
|
||||
async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the device registry entry, including the network MAC connection."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=TEST_TITLE,
|
||||
unique_id=format_mac(TEST_MAC),
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
CONF_MAC: TEST_MAC,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, TEST_MAC)})
|
||||
assert device_entry == snapshot
|
||||
@@ -1,36 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_registry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'4c:a1:61:00:11:22',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'rainbird',
|
||||
'4c:a1:61:00:11:22',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Rain Bird',
|
||||
'model': 'ESP-TM2',
|
||||
'model_id': None,
|
||||
'name': 'Rain Bird Controller',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': '9.12',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.rainbird.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_MAC, Platform
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
@@ -44,24 +42,6 @@ async def test_init_success(
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the controller device registry entry, including the network MAC connection."""
|
||||
# Load a platform so the controller device is registered.
|
||||
with patch("homeassistant.components.rainbird.PLATFORMS", [Platform.SENSOR]):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, MAC_ADDRESS_UNIQUE_ID)}
|
||||
)
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_data", "responses", "config_entry_state", "config_flow_steps"),
|
||||
[
|
||||
|
||||
@@ -820,40 +820,3 @@ async def test_discovery_fallback_name_from_mac(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
assert result["description_placeholders"]["name"] == "Access DDEEFF"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
@pytest.mark.parametrize(
|
||||
("extra_info", "expected_name"),
|
||||
[
|
||||
(
|
||||
{"name": "Front Gate", "hostname": "unvr", "product_name": "UNVR"},
|
||||
"Front Gate",
|
||||
),
|
||||
({"hostname": "unvr", "product_name": "UNVR"}, "unvr"),
|
||||
({"product_name": "UniFi Dream Machine"}, "UniFi Dream Machine"),
|
||||
],
|
||||
ids=["console-name", "hostname", "product-name"],
|
||||
)
|
||||
async def test_discovery_name_resolution(
|
||||
hass: HomeAssistant,
|
||||
mock_client: MagicMock,
|
||||
extra_info: dict[str, str],
|
||||
expected_name: str,
|
||||
) -> None:
|
||||
"""Test the discovered-device name prefers the console name over raw codes."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
"source_ip": "10.0.0.5",
|
||||
"hw_addr": "aa:bb:cc:dd:ee:ff",
|
||||
"services": {"Access": True},
|
||||
"direct_connect_domain": "x.ui.direct",
|
||||
**extra_info,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
assert result["description_placeholders"]["name"] == expected_name
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_entry_diagnostics
|
||||
dict({
|
||||
'entry': dict({
|
||||
'data': dict({
|
||||
'auth_implementation': 'yoto',
|
||||
'token': dict({
|
||||
'access_token': '**REDACTED**',
|
||||
'expires_in': 3600,
|
||||
'refresh_token': '**REDACTED**',
|
||||
'scope': 'offline_access family:view family:devices:view family:devices:control family:devices:manage family:library:view user:content:view user:icons:manage',
|
||||
'token_type': 'Bearer',
|
||||
}),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'discovery_keys': dict({
|
||||
}),
|
||||
'domain': 'yoto',
|
||||
'minor_version': 1,
|
||||
'options': dict({
|
||||
}),
|
||||
'pref_disable_new_entities': False,
|
||||
'pref_disable_polling': False,
|
||||
'source': 'user',
|
||||
'subentries': list([
|
||||
]),
|
||||
'title': 'Yoto',
|
||||
'unique_id': 'auth0|user-test',
|
||||
'version': 1,
|
||||
}),
|
||||
'players': dict({
|
||||
'player-test': dict({
|
||||
'device': dict({
|
||||
'description': None,
|
||||
'device_family': 'v3',
|
||||
'device_group': None,
|
||||
'device_id': 'player-test',
|
||||
'device_type': 'v3',
|
||||
'form_factor': None,
|
||||
'generation': 'gen3',
|
||||
'has_user_given_name': False,
|
||||
'name': 'Nursery Yoto',
|
||||
'release_channel': None,
|
||||
}),
|
||||
'devices_refreshed_at': '2026-05-08T12:00:00+00:00',
|
||||
'extended_status': dict({
|
||||
'active_card': None,
|
||||
'ambient_light_sensor_reading': None,
|
||||
'average_download_speed_bytes_second': None,
|
||||
'battery_level_percentage': None,
|
||||
'battery_level_raw': None,
|
||||
'battery_profile': None,
|
||||
'battery_temperature': None,
|
||||
'battery_voltage_mv': None,
|
||||
'card_insertion_state': None,
|
||||
'current_display_brightness': None,
|
||||
'day_mode': None,
|
||||
'free_disk_space_bytes': None,
|
||||
'is_audio_device_connected': None,
|
||||
'is_background_download_active': None,
|
||||
'is_bluetooth_audio_connected': None,
|
||||
'is_charging': None,
|
||||
'network_ssid': None,
|
||||
'nightlight_mode': None,
|
||||
'power_source': None,
|
||||
'system_volume_percentage': None,
|
||||
'temperature_celcius': None,
|
||||
'total_disk_space_bytes': None,
|
||||
'updated_at': None,
|
||||
'uptime': None,
|
||||
'user_volume_percentage': None,
|
||||
'utc_offset_seconds': None,
|
||||
'utc_time': None,
|
||||
'wifi_strength': None,
|
||||
}),
|
||||
'info': dict({
|
||||
'activation_pop_code': None,
|
||||
'config': dict({
|
||||
'alarms': list([
|
||||
]),
|
||||
'bluetooth_enabled': None,
|
||||
'bt_headphones_enabled': None,
|
||||
'clock_face': None,
|
||||
'day_ambient_colour': None,
|
||||
'day_display_brightness': None,
|
||||
'day_display_brightness_auto': None,
|
||||
'day_max_volume_limit': None,
|
||||
'day_sounds_off': None,
|
||||
'day_time': dict({
|
||||
'__type': "<class 'datetime.time'>",
|
||||
'isoformat': '07:00:00',
|
||||
}),
|
||||
'day_yoto_daily': None,
|
||||
'day_yoto_radio': None,
|
||||
'display_dim_brightness': None,
|
||||
'display_dim_timeout': None,
|
||||
'headphones_volume_limited': None,
|
||||
'hour_format': None,
|
||||
'locale': None,
|
||||
'log_level': None,
|
||||
'night_ambient_colour': None,
|
||||
'night_display_brightness': None,
|
||||
'night_display_brightness_auto': None,
|
||||
'night_max_volume_limit': None,
|
||||
'night_sounds_off': None,
|
||||
'night_time': dict({
|
||||
'__type': "<class 'datetime.time'>",
|
||||
'isoformat': '19:00:00',
|
||||
}),
|
||||
'night_yoto_daily': None,
|
||||
'night_yoto_radio': None,
|
||||
'pause_power_button': None,
|
||||
'pause_volume_down': None,
|
||||
'repeat_all': None,
|
||||
'show_diagnostics': None,
|
||||
'shutdown_timeout': None,
|
||||
'system_volume': None,
|
||||
'timezone': None,
|
||||
'volume_level': None,
|
||||
}),
|
||||
'device_family': None,
|
||||
'device_group': None,
|
||||
'device_type': None,
|
||||
'error_code': None,
|
||||
'firmware_version': 'v2.17.5',
|
||||
'geo_timezone': None,
|
||||
'mac': '**REDACTED**',
|
||||
'name': None,
|
||||
'pop_code': None,
|
||||
'release_channel_id': None,
|
||||
}),
|
||||
'info_refreshed_at': '2026-05-08T12:00:00+00:00',
|
||||
'is_online': True,
|
||||
'last_event': dict({
|
||||
'card_id': 'card-test',
|
||||
'chapter_key': '01',
|
||||
'chapter_title': 'Chapter 1',
|
||||
'event_utc': None,
|
||||
'playback_status': 'playing',
|
||||
'playback_wait': None,
|
||||
'player_id': 'player-test',
|
||||
'position': 120,
|
||||
'repeat_all': None,
|
||||
'request_id': None,
|
||||
'sleep_timer_active': None,
|
||||
'sleep_timer_seconds': None,
|
||||
'source': None,
|
||||
'streaming': None,
|
||||
'track_key': '01-INT',
|
||||
'track_length': 300,
|
||||
'track_title': 'Introduction',
|
||||
'volume': 8,
|
||||
'volume_max': 16,
|
||||
}),
|
||||
'last_event_received_at': '2026-05-08T12:00:00+00:00',
|
||||
'online_refreshed_at': None,
|
||||
'status': dict({
|
||||
'active_card': None,
|
||||
'ambient_light_sensor_reading': None,
|
||||
'battery_level_percentage': 75,
|
||||
'card_insertion_state': 1,
|
||||
'current_display_brightness': None,
|
||||
'day_mode': 1,
|
||||
'free_disk_space_bytes': None,
|
||||
'is_audio_device_connected': False,
|
||||
'is_bluetooth_audio_connected': False,
|
||||
'is_charging': True,
|
||||
'nightlight_mode': None,
|
||||
'system_volume_percentage': None,
|
||||
'updated_at': None,
|
||||
'user_volume_percentage': None,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
@@ -1,29 +0,0 @@
|
||||
"""Tests for the diagnostics data provided by the Yoto integration."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("setup_credentials", "mock_yoto_client")
|
||||
|
||||
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
) == snapshot(exclude=props("entry_id", "created_at", "modified_at", "expires_at"))
|
||||
@@ -59,6 +59,7 @@ from homeassistant.helpers.automation import (
|
||||
move_top_level_schema_fields_to_options,
|
||||
)
|
||||
from homeassistant.helpers.condition import (
|
||||
_DATA_HISTORY_PRIMING_MANAGER,
|
||||
ATTR_BEHAVIOR,
|
||||
BEHAVIOR_ALL,
|
||||
BEHAVIOR_ANY,
|
||||
@@ -69,6 +70,7 @@ from homeassistant.helpers.condition import (
|
||||
EntityConditionBase,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
_async_get_condition_platform,
|
||||
_HistoryPrimingManager,
|
||||
async_validate_condition_config,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_numerical_condition_with_unit,
|
||||
@@ -5862,6 +5864,152 @@ async def test_state_condition_attr_duration_history_flushes_before_query(
|
||||
assert call_order == ["flush", "query"]
|
||||
|
||||
|
||||
async def test_async_setup_creates_history_priming_manager(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""The priming manager is created during condition setup, not on demand."""
|
||||
# condition.async_setup runs as part of the test hass fixture.
|
||||
assert isinstance(hass.data[_DATA_HISTORY_PRIMING_MANAGER], _HistoryPrimingManager)
|
||||
|
||||
|
||||
async def test_history_priming_manager_serializes_queries(
|
||||
recorder_mock: Recorder, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Queries run one at a time even when many conditions prime together."""
|
||||
manager = _HistoryPrimingManager(hass)
|
||||
instance = get_instance(hass)
|
||||
|
||||
running = 0
|
||||
max_running = 0
|
||||
release = asyncio.Event()
|
||||
started = asyncio.Event()
|
||||
|
||||
async def _job(_recorder: Recorder) -> str:
|
||||
nonlocal running, max_running
|
||||
running += 1
|
||||
max_running = max(max_running, running)
|
||||
started.set()
|
||||
await release.wait()
|
||||
running -= 1
|
||||
return "ok"
|
||||
|
||||
# No pending commit, so flushing is instant and only query serialization
|
||||
# is exercised.
|
||||
with patch.object(instance, "async_get_commit_future", return_value=None):
|
||||
tasks = [asyncio.create_task(manager.async_prime(_job)) for _ in range(5)]
|
||||
# The first job holds the query lock; the rest must queue behind it.
|
||||
await started.wait()
|
||||
await asyncio.sleep(0)
|
||||
assert running == 1
|
||||
release.set()
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
assert results == ["ok"] * 5
|
||||
assert max_running == 1
|
||||
|
||||
|
||||
async def test_history_priming_manager_does_not_ride_in_flight_flush(
|
||||
recorder_mock: Recorder, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""A priming never rides a flush that began before it arrived (the lobby).
|
||||
|
||||
The flush commits changes still queued in the recorder so the history read
|
||||
sees them. A condition that started tracking after an in-flight flush began
|
||||
could miss its own just-queued change if it rode that flush, so it waits the
|
||||
flush out and a fresh one is performed for it. Without the lobby step this
|
||||
test fails: the late arrivals would ride the first flush (one flush total)
|
||||
instead of sharing a second, fresh one.
|
||||
"""
|
||||
manager = _HistoryPrimingManager(hass)
|
||||
instance = get_instance(hass)
|
||||
|
||||
flush_futures: list[asyncio.Future[None]] = []
|
||||
|
||||
def _spy_commit_future() -> asyncio.Future[None]:
|
||||
fut = hass.loop.create_future()
|
||||
flush_futures.append(fut)
|
||||
return fut
|
||||
|
||||
async def _job(_recorder: Recorder) -> str:
|
||||
return "done"
|
||||
|
||||
with patch.object(instance, "async_get_commit_future", _spy_commit_future):
|
||||
# C0 claims the flush and is mid-flush (its commit future is pending).
|
||||
c0 = asyncio.create_task(manager.async_prime(_job))
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(0)
|
||||
if flush_futures:
|
||||
break
|
||||
assert len(flush_futures) == 1
|
||||
|
||||
# Two conditions arrive while C0's flush runs; they must not ride it.
|
||||
c1 = asyncio.create_task(manager.async_prime(_job))
|
||||
c2 = asyncio.create_task(manager.async_prime(_job))
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0)
|
||||
# Parked in the lobby: no new flush yet, none finished.
|
||||
assert len(flush_futures) == 1
|
||||
assert not c1.done()
|
||||
assert not c2.done()
|
||||
|
||||
# C0's flush completes; C1 now performs a fresh flush and C2 rides it.
|
||||
flush_futures[0].set_result(None)
|
||||
assert await c0 == "done"
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(0)
|
||||
# Exactly one fresh flush is shared by C1 and C2, not one each: this is
|
||||
# the assertion that fails without the lobby (it would stay 1).
|
||||
assert len(flush_futures) == 2
|
||||
flush_futures[1].set_result(None)
|
||||
assert await asyncio.gather(c1, c2) == ["done", "done"]
|
||||
|
||||
|
||||
async def test_history_priming_manager_cancelled_lobby_waiter(
|
||||
recorder_mock: Recorder, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""A priming cancelled while waiting in the lobby doesn't wedge later ones.
|
||||
|
||||
A condition whose timeout fires while it waits for an in-flight flush is
|
||||
cancelled. That must leave the manager able to flush for the next priming.
|
||||
"""
|
||||
manager = _HistoryPrimingManager(hass)
|
||||
instance = get_instance(hass)
|
||||
|
||||
flush_futures: list[asyncio.Future[None]] = []
|
||||
|
||||
def _spy_commit_future() -> asyncio.Future[None]:
|
||||
fut = hass.loop.create_future()
|
||||
flush_futures.append(fut)
|
||||
return fut
|
||||
|
||||
async def _job(_recorder: Recorder) -> str:
|
||||
return "done"
|
||||
|
||||
with patch.object(instance, "async_get_commit_future", _spy_commit_future):
|
||||
c0 = asyncio.create_task(manager.async_prime(_job))
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(0)
|
||||
if flush_futures:
|
||||
break
|
||||
# A second priming parks in the lobby, then its timeout cancels it.
|
||||
waiter = asyncio.create_task(manager.async_prime(_job))
|
||||
for _ in range(3):
|
||||
await asyncio.sleep(0)
|
||||
waiter.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await waiter
|
||||
|
||||
# C0 finishes; a later priming still flushes and completes normally.
|
||||
flush_futures[0].set_result(None)
|
||||
assert await c0 == "done"
|
||||
later = asyncio.create_task(manager.async_prime(_job))
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(0)
|
||||
assert len(flush_futures) == 2
|
||||
flush_futures[1].set_result(None)
|
||||
assert await later == "done"
|
||||
|
||||
|
||||
async def test_state_condition_multi_state_duration_uses_history(
|
||||
recorder_mock: Recorder, hass: HomeAssistant
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user