mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Merge branch 'input_button_trigger' of github.com:home-assistant/core into button_being_held_trigger
This commit is contained in:
@@ -168,17 +168,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
||||
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
# Devices left in both the config entry and its subentry by Home Assistant Core
|
||||
# 2025.7.0b0-2025.7.0b1 are collapsed onto the subentry by the device registry
|
||||
# migration, so there's nothing to correct here.
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
|
||||
@@ -86,6 +86,8 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
||||
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
errors = {}
|
||||
|
||||
host = user_input[CONF_HOST]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_units": "Could not find any HVAC units in CoolMasterNet host."
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["energieleser==0.1.5"],
|
||||
"requirements": ["energieleser==0.1.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_stromleser._tcp.local."
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==45.6.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"esphome-dashboard-api==1.4.0",
|
||||
"bleak-esphome==3.9.7"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
|
||||
@@ -230,17 +230,9 @@ async def async_migrate_entry(
|
||||
),
|
||||
)
|
||||
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
# Devices left in both the config entry and its subentry by Home Assistant Core
|
||||
# 2025.7.0b0-2025.7.0b1 are collapsed onto the subentry by the device registry
|
||||
# migration, so there's nothing to correct here.
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
|
||||
@@ -123,6 +123,7 @@ async def async_setup_entry(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
manufacturer="Google",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
@@ -25,6 +25,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceEntryType,
|
||||
DeviceInfo,
|
||||
async_get_device_id_by_identifier,
|
||||
)
|
||||
@@ -356,6 +357,7 @@ class GoogleHealthSensor[_CoordinatorT: GoogleHealthDataUpdateCoordinator[Any]](
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{entry_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, entry_id)},
|
||||
manufacturer="Google",
|
||||
)
|
||||
|
||||
@@ -19,9 +19,11 @@ from aiohue.v2.controllers.sensors import (
|
||||
)
|
||||
from aiohue.v2.models.camera_motion import CameraMotion
|
||||
from aiohue.v2.models.contact import Contact, ContactState
|
||||
from aiohue.v2.models.convenience_area_motion import ConvenienceAreaMotion
|
||||
from aiohue.v2.models.entertainment_configuration import EntertainmentStatus
|
||||
from aiohue.v2.models.grouped_motion import GroupedMotion
|
||||
from aiohue.v2.models.motion import Motion
|
||||
from aiohue.v2.models.motion_area_configuration import MotionAreaHealth
|
||||
from aiohue.v2.models.resource import ResourceTypes
|
||||
from aiohue.v2.models.security_area_motion import SecurityAreaMotion
|
||||
from aiohue.v2.models.tamper import Tamper, TamperState
|
||||
@@ -181,11 +183,10 @@ class HueGroupedMotionSensor(HueMotionSensor):
|
||||
class HueMotionAwareSensor(HueMotionSensor):
|
||||
"""Representation of a Motion sensor based on Hue Motion Aware.
|
||||
|
||||
Note that we only create sensors for the SecurityAreaMotion resource
|
||||
and not for the ConvenienceAreaMotion resource, because the latter
|
||||
does not have a state when it's not directly controlling lights.
|
||||
The SecurityAreaMotion resource is always available with a state, allowing
|
||||
Home Assistant users to actually use it as a motion sensor in their HA automations.
|
||||
A MotionAware zone owns both a ConvenienceAreaMotion and a SecurityAreaMotion
|
||||
service, and which of the two carries the motion state depends on how the zone
|
||||
is set up in the Hue app. The source is therefore resolved from the zone, and
|
||||
the state is unknown while neither service reports a valid one.
|
||||
"""
|
||||
|
||||
controller: SecurityAreaMotionController
|
||||
@@ -201,6 +202,26 @@ class HueMotionAwareSensor(HueMotionSensor):
|
||||
"""Return sensor name."""
|
||||
return self.controller.get_motion_area_configuration(self.resource.id).name
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
zone = self._motion_area_configuration
|
||||
# switching a zone off leaves the `enabled` flag of its services untouched,
|
||||
# so the zone is the only reliable signal that it stopped reporting motion
|
||||
if not zone.enabled or zone.health == MotionAreaHealth.NOT_RUNNING:
|
||||
return None
|
||||
# either service can be the one reporting a zone, with the other left holding
|
||||
# a reading that no longer changes, so take the first that has a valid one.
|
||||
# The convenience service comes first because a zone that is bound to lights
|
||||
# reports on that service.
|
||||
for source in (self._convenience_service, self.resource):
|
||||
if source is None or not source.enabled or source.motion is None:
|
||||
continue
|
||||
if (value := source.motion.value) is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bridge: HueBridge,
|
||||
@@ -229,6 +250,32 @@ class HueMotionAwareSensor(HueMotionSensor):
|
||||
self._handle_event, self._motion_area_configuration.id
|
||||
)
|
||||
)
|
||||
# zones that are bound to lights report their motion on the convenience service
|
||||
if (service_id := self._convenience_service_id) is not None:
|
||||
self.async_on_remove(
|
||||
self.bridge.api.sensors.convenience_area_motion.subscribe(
|
||||
self._handle_event, service_id
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def _convenience_service_id(self) -> str | None:
|
||||
"""Return the id of this zone's ConvenienceAreaMotion service, if it has one."""
|
||||
return next(
|
||||
(
|
||||
service.rid
|
||||
for service in self._motion_area_configuration.services
|
||||
if service.rtype == ResourceTypes.CONVENIENCE_AREA_MOTION
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@property
|
||||
def _convenience_service(self) -> ConvenienceAreaMotion | None:
|
||||
"""Return this zone's ConvenienceAreaMotion resource, if the bridge has it."""
|
||||
if (service_id := self._convenience_service_id) is None:
|
||||
return None
|
||||
return self.bridge.api.sensors.convenience_area_motion.get(service_id)
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-enforce-class-module
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, override
|
||||
from pyinsteon import devices
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
@@ -88,6 +89,8 @@ class InsteonEntity(Entity):
|
||||
@override
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information."""
|
||||
config_entry = self.platform.config_entry
|
||||
assert config_entry
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, str(self._insteon_device.address))},
|
||||
manufacturer="SmartLabs, Inc",
|
||||
@@ -100,7 +103,11 @@ class InsteonEntity(Entity):
|
||||
f"{self._insteon_device.firmware:02x} Engine Version:"
|
||||
f" {self._insteon_device.engine_version}"
|
||||
),
|
||||
via_device=(DOMAIN, str(devices.modem.address)),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(DOMAIN, str(devices.modem.address)),
|
||||
config_entry_id=config_entry.entry_id,
|
||||
),
|
||||
configuration_url=f"homeassistant://insteon/device/config/{self._insteon_device.id}",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Base entity for Iskra devices."""
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -26,7 +27,11 @@ class IskraEntity(CoordinatorEntity[IskraDataUpdateCoordinator]):
|
||||
name=self.device.model,
|
||||
sw_version=self.device.fw_version,
|
||||
serial_number=self.device.serial,
|
||||
via_device=(DOMAIN, gateway.serial),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, gateway.serial),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
else:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -50,7 +51,11 @@ class JellyfinClientEntity(JellyfinEntity):
|
||||
model=self.client_name,
|
||||
name=self.device_name,
|
||||
sw_version=self.app_version,
|
||||
via_device=(DOMAIN, coordinator.server_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.server_id),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
self._attr_name = None
|
||||
else:
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import DEGREE, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import UNDEFINED, StateType, UndefinedType
|
||||
@@ -31,6 +32,10 @@ async def async_setup_entry(
|
||||
"2_ch_power_strip",
|
||||
)
|
||||
|
||||
via_device_id = dr.async_get_device_id_by_identifier(
|
||||
hass, (DOMAIN, "2_ch_power_strip"), config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
DemoSensor(
|
||||
@@ -42,7 +47,7 @@ async def async_setup_entry(
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
unit_of_measurement=UnitOfPower.WATT,
|
||||
via_device="2_ch_power_strip",
|
||||
via_device_id=via_device_id,
|
||||
),
|
||||
DemoSensor(
|
||||
device_unique_id="outlet_2",
|
||||
@@ -53,7 +58,7 @@ async def async_setup_entry(
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
unit_of_measurement=UnitOfPower.WATT,
|
||||
via_device="2_ch_power_strip",
|
||||
via_device_id=via_device_id,
|
||||
),
|
||||
DemoSensor(
|
||||
device_unique_id="statistics_issues",
|
||||
@@ -135,7 +140,7 @@ class DemoSensor(SensorEntity):
|
||||
device_class: SensorDeviceClass | None,
|
||||
state_class: SensorStateClass | None,
|
||||
unit_of_measurement: str | None,
|
||||
via_device: str | None = None,
|
||||
via_device_id: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._attr_device_class = device_class
|
||||
@@ -150,5 +155,5 @@ class DemoSensor(SensorEntity):
|
||||
identifiers={(DOMAIN, device_unique_id)},
|
||||
name=device_name,
|
||||
)
|
||||
if via_device:
|
||||
self._attr_device_info["via_device"] = (DOMAIN, via_device)
|
||||
if via_device_id:
|
||||
self._attr_device_info["via_device_id"] = via_device_id
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, override
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -27,6 +28,10 @@ async def async_setup_entry(
|
||||
"2_ch_power_strip",
|
||||
)
|
||||
|
||||
via_device_id = dr.async_get_device_id_by_identifier(
|
||||
hass, (DOMAIN, "2_ch_power_strip"), config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
DemoSwitch(
|
||||
@@ -35,7 +40,7 @@ async def async_setup_entry(
|
||||
entity_name=None,
|
||||
state=False,
|
||||
assumed=False,
|
||||
via_device="2_ch_power_strip",
|
||||
via_device_id=via_device_id,
|
||||
),
|
||||
DemoSwitch(
|
||||
unique_id="outlet_2",
|
||||
@@ -43,7 +48,7 @@ async def async_setup_entry(
|
||||
entity_name=None,
|
||||
state=True,
|
||||
assumed=False,
|
||||
via_device="2_ch_power_strip",
|
||||
via_device_id=via_device_id,
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -65,7 +70,7 @@ class DemoSwitch(SwitchEntity):
|
||||
assumed: bool,
|
||||
translation_key: str | None = None,
|
||||
device_class: SwitchDeviceClass | None = None,
|
||||
via_device: str | None = None,
|
||||
via_device_id: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Demo switch."""
|
||||
self._attr_assumed_state = assumed
|
||||
@@ -77,8 +82,8 @@ class DemoSwitch(SwitchEntity):
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
name=device_name,
|
||||
)
|
||||
if via_device:
|
||||
self._attr_device_info["via_device"] = (DOMAIN, via_device)
|
||||
if via_device_id:
|
||||
self._attr_device_info["via_device_id"] = via_device_id
|
||||
self._attr_name = entity_name
|
||||
|
||||
@override
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==3.17.0",
|
||||
"xknxproject==3.9.0",
|
||||
"xknx==3.18.0",
|
||||
"xknxproject==3.10.0",
|
||||
"knx-frontend==2026.7.23.145751",
|
||||
"knx-telegram-store[sqlite,postgres]==0.11.1"
|
||||
"knx-telegram-store[sqlite,postgres]==0.11.2"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -211,7 +211,9 @@ def register_lcn_address_devices(
|
||||
"""
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
host_identifiers = (DOMAIN, config_entry.entry_id)
|
||||
host_device_id = dr.async_get_device_id_by_identifier(
|
||||
hass, (DOMAIN, config_entry.entry_id), config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
for device_config in config_entry.data[CONF_DEVICES]:
|
||||
address = device_config[CONF_ADDRESS]
|
||||
@@ -233,7 +235,7 @@ def register_lcn_address_devices(
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers=identifiers,
|
||||
via_device=host_identifiers,
|
||||
via_device_id=host_device_id,
|
||||
manufacturer="Issendorff",
|
||||
sw_version=sw_version,
|
||||
name=device_name,
|
||||
|
||||
@@ -117,12 +117,12 @@ class LifXConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not (legacy_entry := async_get_legacy_entry(self.hass)):
|
||||
return False
|
||||
device_registry = dr.async_get(self.hass)
|
||||
existing_device = device_registry.async_get_device(
|
||||
existing_devices = device_registry.async_get_devices(
|
||||
identifiers={(DOMAIN, self.unique_id)}
|
||||
)
|
||||
return bool(
|
||||
existing_device is not None
|
||||
and legacy_entry.entry_id in existing_device.config_entries
|
||||
return any(
|
||||
device.config_entry_id == legacy_entry.entry_id
|
||||
for device in existing_devices
|
||||
)
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, override
|
||||
from livisi.const import CAPABILITY_MAP
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -59,7 +60,11 @@ class LivisiEntity(CoordinatorEntity[LivisiDataUpdateCoordinator]):
|
||||
model=device["type"],
|
||||
name=device_name,
|
||||
suggested_area=room_name,
|
||||
via_device=(DOMAIN, config_entry.entry_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, config_entry.entry_id),
|
||||
config_entry_id=config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
super().__init__(coordinator)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.light import (
|
||||
brightness_supported,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -97,9 +98,13 @@ class LunatoneLight(
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
name=self._device.name,
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
f"{self._config_entry_unique_id}-line{self._device.data.line}",
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(
|
||||
DOMAIN,
|
||||
f"{self._config_entry_unique_id}-line{self._device.data.line}",
|
||||
),
|
||||
config_entry_id=self.coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -261,7 +266,11 @@ class LunatoneLineBroadcastLight(
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
name=f"DALI Line {line}",
|
||||
via_device=(DOMAIN, config_entry_unique_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.coordinator.hass,
|
||||
(DOMAIN, config_entry_unique_id),
|
||||
config_entry_id=self.coordinator.config_entry.entry_id,
|
||||
),
|
||||
**extra_info,
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -135,7 +136,11 @@ class LunatoneSensor(
|
||||
f"DALI Line {self.sensor.data.dali_sensor_address.line}"
|
||||
f" - A{self.sensor.data.dali_sensor_address.address}\u00b2"
|
||||
),
|
||||
via_device=(DOMAIN, str(self._config_entry_unique_id)),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.coordinator.hass,
|
||||
(DOMAIN, str(self._config_entry_unique_id)),
|
||||
config_entry_id=self.coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
self._attr_device_info = device_info
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from pymelcloud.atw_device import Zone
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -38,6 +39,8 @@ type MelCloudConfigEntry = ConfigEntry[dict[str, list[MelCloudDeviceUpdateCoordi
|
||||
class MelCloudDeviceUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Per-device coordinator for MELCloud data updates."""
|
||||
|
||||
config_entry: MelCloudConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -110,7 +113,11 @@ class MelCloudDeviceUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
manufacturer="Mitsubishi Electric",
|
||||
model="ATW zone device",
|
||||
name=f"{self.device.name} {zone.name}",
|
||||
via_device=(DOMAIN, f"{dev.mac}-{dev.serial}"),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(DOMAIN, f"{dev.mac}-{dev.serial}"),
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,14 +4,8 @@ import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import (
|
||||
CONF_TRACK_HOME,
|
||||
DEFAULT_HOME_LATITUDE,
|
||||
DEFAULT_HOME_LONGITUDE,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_TRACK_HOME, DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE
|
||||
from .coordinator import MetDataUpdateCoordinator, MetWeatherConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.WEATHER]
|
||||
@@ -49,8 +43,6 @@ async def async_setup_entry(
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
await cleanup_old_device(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -59,12 +51,3 @@ async def async_unload_entry(
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
async def cleanup_old_device(hass: HomeAssistant) -> None:
|
||||
"""Cleanup device without proper device identifier."""
|
||||
device_reg = dr.async_get(hass)
|
||||
device = device_reg.async_get_device(identifiers={(DOMAIN,)}) # type: ignore[arg-type]
|
||||
if device:
|
||||
_LOGGER.debug("Removing improper device %s", device.name)
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
@@ -88,14 +88,15 @@ class MitsubishiComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
changed IP.
|
||||
"""
|
||||
mac = dr.format_mac(discovery_info.macaddress)
|
||||
device = dr.async_get(self.hass).async_get_device(
|
||||
devices = dr.async_get(self.hass).async_get_devices(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, mac)}
|
||||
)
|
||||
device_entry_ids = {device.config_entry_id for device in devices}
|
||||
entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in self._async_current_entries(include_ignore=False)
|
||||
if device is not None and entry.entry_id in device.config_entries
|
||||
if entry.entry_id in device_entry_ids
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1319,8 +1319,8 @@ def ensure_via_device_exists(
|
||||
if (
|
||||
device_info is None
|
||||
or CONF_VIA_DEVICE not in device_info
|
||||
or (device_registry := dr.async_get(hass)).async_get_device(
|
||||
identifiers={device_info["via_device"]}
|
||||
or (device_registry := dr.async_get(hass)).async_get_device_by_identifier(
|
||||
device_info["via_device"], config_entry.entry_id
|
||||
)
|
||||
):
|
||||
return
|
||||
|
||||
@@ -36,8 +36,8 @@ class MQTTDeviceEntryMigration(RepairsFlow):
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
if user_input is not None:
|
||||
device_registry = dr.async_get(self.hass)
|
||||
subentry_device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.subentry_id)}
|
||||
subentry_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, self.subentry_id), self.entry_id
|
||||
)
|
||||
entry = self.hass.config_entries.async_get_entry(self.entry_id)
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["nibe==2.22.0"]
|
||||
"requirements": ["nibe==2.24.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import override
|
||||
|
||||
from pynuki.device import NukiDevice
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -38,6 +39,10 @@ class NukiEntity[_NukiDeviceT: NukiDevice](CoordinatorEntity[NukiCoordinator]):
|
||||
manufacturer="Nuki Home Solutions GmbH",
|
||||
model=self._nuki_device.device_model_str.capitalize(),
|
||||
sw_version=self._nuki_device.firmware_version,
|
||||
via_device=(DOMAIN, self.coordinator.bridge_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.coordinator.hass,
|
||||
(DOMAIN, self.coordinator.bridge_id),
|
||||
config_entry_id=self.coordinator.config_entry.entry_id,
|
||||
),
|
||||
serial_number=parse_id(self._nuki_device.nuki_id),
|
||||
)
|
||||
|
||||
@@ -226,17 +226,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
||||
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
# Devices left in both the config entry and its subentry by Home Assistant Core
|
||||
# 2025.7.0b0-2025.7.0b1 are collapsed onto the subentry by the device registry
|
||||
# migration, so there's nothing to correct here.
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
|
||||
@@ -418,17 +418,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
|
||||
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
# Devices left in both the config entry and its subentry by Home Assistant Core
|
||||
# 2025.7.0b0-2025.7.0b1 are collapsed onto the subentry by the device registry
|
||||
# migration, so there's nothing to correct here.
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -99,7 +100,11 @@ class OpenRGBLight(CoordinatorEntity[OpenRGBCoordinator], LightEntity):
|
||||
model=f"{self.device.metadata.description} ({self.device.type.name})",
|
||||
sw_version=self.device.metadata.version,
|
||||
serial_number=self.device.metadata.serial,
|
||||
via_device=(DOMAIN, coordinator.entry_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.entry_id),
|
||||
config_entry_id=coordinator.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
modes = [mode.name for mode in self.device.modes]
|
||||
|
||||
@@ -7,4 +7,7 @@ CONF_ALLOW_NOTIFY = "allow_notify"
|
||||
CONST_APP_ID = "homeassistant.io"
|
||||
CONST_APP_NAME = "Home Assistant"
|
||||
|
||||
TV_STATE_OFF = "Off"
|
||||
TV_STATE_ON = "On"
|
||||
|
||||
TRIGGER_TYPE_TURN_ON = "turn_on"
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.trigger import PluggableAction
|
||||
|
||||
from . import LOGGER as _LOGGER
|
||||
from .const import TV_STATE_OFF
|
||||
from .coordinator import PhilipsTVConfigEntry, PhilipsTVDataUpdateCoordinator
|
||||
from .entity import PhilipsJsEntity
|
||||
from .helpers import async_get_turn_on_trigger
|
||||
@@ -458,7 +459,9 @@ class PhilipsTVMediaPlayer(PhilipsJsEntity, MediaPlayerEntity):
|
||||
@callback
|
||||
def _update_from_coordinator(self):
|
||||
if self._tv.on:
|
||||
if self._tv.powerstate in ("Standby", "StandbyKeep"):
|
||||
if self._tv.powerstate in ("Standby", "StandbyKeep") or (
|
||||
self._tv.powerstate is None and self._tv.screenstate == TV_STATE_OFF
|
||||
):
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
|
||||
@@ -6,12 +6,10 @@ from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import TV_STATE_OFF, TV_STATE_ON
|
||||
from .coordinator import PhilipsTVConfigEntry, PhilipsTVDataUpdateCoordinator
|
||||
from .entity import PhilipsJsEntity
|
||||
|
||||
HUE_POWER_OFF = "Off"
|
||||
HUE_POWER_ON = "On"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -50,23 +48,23 @@ class PhilipsTVScreenSwitch(PhilipsJsEntity, SwitchEntity):
|
||||
return False
|
||||
if not self.coordinator.api.on:
|
||||
return False
|
||||
return self.coordinator.api.powerstate == "On"
|
||||
return self.coordinator.api.powerstate in (TV_STATE_ON, None)
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self.coordinator.api.screenstate == "On"
|
||||
return self.coordinator.api.screenstate == TV_STATE_ON
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.coordinator.api.setScreenState("On")
|
||||
await self.coordinator.api.setScreenState(TV_STATE_ON)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.coordinator.api.setScreenState("Off")
|
||||
await self.coordinator.api.setScreenState(TV_STATE_OFF)
|
||||
|
||||
|
||||
class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity):
|
||||
@@ -92,22 +90,22 @@ class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity):
|
||||
return False
|
||||
if not self.coordinator.api.on:
|
||||
return False
|
||||
return self.coordinator.api.powerstate == "On"
|
||||
return self.coordinator.api.powerstate in (TV_STATE_ON, None)
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self.coordinator.api.huelamp_power == HUE_POWER_ON
|
||||
return self.coordinator.api.huelamp_power == TV_STATE_ON
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.coordinator.api.setHueLampPower(HUE_POWER_ON)
|
||||
await self.coordinator.api.setHueLampPower(TV_STATE_ON)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.coordinator.api.setHueLampPower(HUE_POWER_OFF)
|
||||
await self.coordinator.api.setHueLampPower(TV_STATE_OFF)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
import logging
|
||||
from typing import Any, Concatenate, cast, override
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, cast, override
|
||||
|
||||
from plexapi.client import PlexClient
|
||||
import plexapi.exceptions
|
||||
@@ -20,7 +20,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
@@ -557,6 +557,9 @@ class PlexMediaPlayer(MediaPlayerEntity):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
config_entry = self.platform.config_entry
|
||||
if TYPE_CHECKING:
|
||||
assert config_entry
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.machine_identifier)},
|
||||
manufacturer=self.device_platform or "Plex",
|
||||
@@ -566,7 +569,11 @@ class PlexMediaPlayer(MediaPlayerEntity):
|
||||
# name to None
|
||||
name=cast(str | None, self.name),
|
||||
sw_version=self.device_version,
|
||||
via_device=(DOMAIN, self.plex_server.machine_identifier),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(DOMAIN, self.plex_server.machine_identifier),
|
||||
config_entry_id=config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.21.7"]
|
||||
"requirements": ["reolink-aio==0.21.8"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import cast, override
|
||||
|
||||
from homeassistant.components.event import EventDeviceClass, EventEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -70,7 +71,11 @@ class RoonEventEntity(EventEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self._server.hass,
|
||||
(DOMAIN, self._entry_id),
|
||||
config_entry_id=self._entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
def _roonapi_volume_callback(
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
@@ -154,7 +155,9 @@ class RoonDevice(MediaPlayerEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass, (DOMAIN, self._entry_id), config_entry_id=self._entry_id
|
||||
),
|
||||
)
|
||||
|
||||
def update_data(self, player_data=None):
|
||||
|
||||
@@ -6,6 +6,7 @@ from satel_integra import AsyncSatel
|
||||
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -60,7 +61,11 @@ class SatelIntegraEntity[_CoordinatorT: SatelIntegraBaseCoordinator](
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=subentry.data[CONF_NAME],
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
via_device=(DOMAIN, config_entry_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, config_entry_id),
|
||||
config_entry_id=config_entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -17,6 +17,7 @@ from simplipy.websocket import (
|
||||
)
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -101,7 +102,11 @@ class SimpliSafeEntity(CoordinatorEntity[SimpliSafeDataUpdateCoordinator]):
|
||||
manufacturer="SimpliSafe",
|
||||
model=model,
|
||||
name=device_name,
|
||||
via_device=(DOMAIN, str(system.system_id)),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.coordinator.hass,
|
||||
(DOMAIN, str(system.system_id)),
|
||||
config_entry_id=self.coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
self._attr_unique_id = serial
|
||||
|
||||
@@ -162,7 +162,7 @@ async def async_setup_entry(
|
||||
model_id=model_id,
|
||||
hw_version=str(player.firmware) if player.firmware is not None else None,
|
||||
sw_version=sw_version,
|
||||
via_device=(DOMAIN, coordinator.server_uuid),
|
||||
via_device_id=server_device.id if server_device else None,
|
||||
)
|
||||
_LOGGER.debug("Creating / Updating player device %s", device)
|
||||
async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)])
|
||||
|
||||
@@ -93,10 +93,12 @@ async def coordinator_for_device(
|
||||
manageable_by_webhook: bool = False,
|
||||
) -> SwitchBotCoordinator:
|
||||
"""Instantiate coordinator and adds to list for gathering."""
|
||||
coordinator = coordinators_by_id.setdefault(
|
||||
device.device_id,
|
||||
SwitchBotCoordinator(hass, entry, api, device, manageable_by_webhook),
|
||||
)
|
||||
coordinator = coordinators_by_id.get(device.device_id)
|
||||
if coordinator is None:
|
||||
coordinator = SwitchBotCoordinator(
|
||||
hass, entry, api, device, manageable_by_webhook
|
||||
)
|
||||
coordinators_by_id[device.device_id] = coordinator
|
||||
|
||||
if coordinator.data is None:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -26,6 +26,8 @@ type TailwindConfigEntry = ConfigEntry[TailwindDataUpdateCoordinator]
|
||||
class TailwindDataUpdateCoordinator(DataUpdateCoordinator[TailwindDeviceStatus]):
|
||||
"""Class to manage fetching Tailwind data."""
|
||||
|
||||
config_entry: TailwindConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: TailwindConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.tailwind = Tailwind(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Base entity for the Tailwind integration."""
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -55,7 +56,11 @@ class TailwindDoorEntity(CoordinatorEntity[TailwindDataUpdateCoordinator]):
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{coordinator.data.device_id}-{door_id}")},
|
||||
via_device=(DOMAIN, coordinator.data.device_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.data.device_id),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
name=f"Door {coordinator.data.doors[door_id].index + 1}",
|
||||
manufacturer="Tailwind",
|
||||
model=coordinator.data.product,
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import override
|
||||
from aiotedee.models import TedeeLock
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -35,7 +36,11 @@ class TedeeEntity(CoordinatorEntity[TedeeApiCoordinator]):
|
||||
manufacturer="Tedee",
|
||||
model=lock.type_name,
|
||||
model_id=lock.type_name,
|
||||
via_device=(DOMAIN, coordinator.bridge.serial),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.bridge.serial),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,6 +8,7 @@ from tesla_fleet_api.tesla.energysite import EnergySite
|
||||
from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet
|
||||
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -205,7 +206,11 @@ class TeslaFleetWallConnectorEntity(
|
||||
identifiers={(DOMAIN, din)},
|
||||
manufacturer="Tesla",
|
||||
name="Wall Connector",
|
||||
via_device=(DOMAIN, str(data.id)),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
data.live_coordinator.hass,
|
||||
(DOMAIN, str(data.id)),
|
||||
config_entry_id=data.live_coordinator.config_entry.entry_id,
|
||||
),
|
||||
serial_number=din.rsplit("-", maxsplit=1)[-1],
|
||||
model=model,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from tesla_fleet_api.const import Scope
|
||||
from tesla_fleet_api.teslemetry import EnergySite, Vehicle
|
||||
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -224,7 +225,11 @@ class TeslemetryWallConnectorEntity(TeslemetryPollingEntity):
|
||||
manufacturer="Tesla",
|
||||
configuration_url="https://teslemetry.com/console",
|
||||
name="Wall Connector",
|
||||
via_device=(DOMAIN, str(data.id)),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
data.live_coordinator.hass,
|
||||
(DOMAIN, str(data.id)),
|
||||
config_entry_id=data.live_coordinator.config_entry.entry_id,
|
||||
),
|
||||
serial_number=din.rsplit("-", maxsplit=1)[-1],
|
||||
model=model,
|
||||
)
|
||||
|
||||
@@ -107,7 +107,11 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
"probe_number": str(probe_number),
|
||||
},
|
||||
identifiers={(DOMAIN, f"{self.address}_{probe_number}")},
|
||||
via_device=(DOMAIN, self.address),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
self.hass,
|
||||
(DOMAIN, self.address),
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import override
|
||||
|
||||
from pytouchlinesl import Zone
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -24,7 +25,11 @@ class TouchlineSLZoneEntity(CoordinatorEntity[TouchlineSLModuleCoordinator]):
|
||||
identifiers={(DOMAIN, f"{coordinator.data.module.id}-{zone_id}")},
|
||||
name=self.zone.name,
|
||||
manufacturer="Roth",
|
||||
via_device=(DOMAIN, coordinator.data.module.id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
coordinator.hass,
|
||||
(DOMAIN, coordinator.data.module.id),
|
||||
config_entry_id=coordinator.config_entry.entry_id,
|
||||
),
|
||||
model="zone",
|
||||
suggested_area=self.zone.name,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from pytradfri.device import Device
|
||||
from pytradfri.error import RequestError
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -61,7 +62,11 @@ class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]):
|
||||
model=info.model_number,
|
||||
name=self._device.name,
|
||||
sw_version=info.firmware_version,
|
||||
via_device=(DOMAIN, gateway_id),
|
||||
via_device_id=dr.async_get_device_id_by_identifier(
|
||||
device_coordinator.hass,
|
||||
(DOMAIN, gateway_id),
|
||||
config_entry_id=device_coordinator.config_entry.entry_id,
|
||||
),
|
||||
)
|
||||
self._attr_unique_id = f"{gateway_id}-{self._device_id}"
|
||||
|
||||
|
||||
@@ -116,22 +116,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool
|
||||
},
|
||||
)
|
||||
|
||||
identifiers = {(DOMAIN, device.usn)}
|
||||
identifiers = [(DOMAIN, device.usn)]
|
||||
if device.host:
|
||||
identifiers.add((IDENTIFIER_HOST, device.host))
|
||||
identifiers.append((IDENTIFIER_HOST, device.host))
|
||||
if device.serial_number:
|
||||
identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
|
||||
identifiers.append((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
|
||||
|
||||
connections = {(dr.CONNECTION_UPNP, discovery_info.ssdp_udn)}
|
||||
connections = [(dr.CONNECTION_UPNP, discovery_info.ssdp_udn)]
|
||||
if discovery_info.ssdp_udn != device.udn:
|
||||
connections.add((dr.CONNECTION_UPNP, device.udn))
|
||||
connections.append((dr.CONNECTION_UPNP, device.udn))
|
||||
if device_mac_address:
|
||||
connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address))
|
||||
connections.append((dr.CONNECTION_NETWORK_MAC, device_mac_address))
|
||||
|
||||
dev_registry = dr.async_get(hass)
|
||||
device_entry = dev_registry.async_get_device(
|
||||
identifiers=identifiers, connections=connections
|
||||
)
|
||||
device_entry = None
|
||||
for identifier in identifiers:
|
||||
if device_entry := dev_registry.async_get_device_by_identifier(
|
||||
identifier, entry.entry_id
|
||||
):
|
||||
break
|
||||
if device_entry is None:
|
||||
for connection in connections:
|
||||
if device_entry := dev_registry.async_get_device_by_connection(
|
||||
connection, entry.entry_id
|
||||
):
|
||||
break
|
||||
if device_entry:
|
||||
LOGGER.debug(
|
||||
"Found device using connections: %s, device_entry: %s",
|
||||
@@ -142,8 +151,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool
|
||||
# No device found, create new device entry.
|
||||
device_entry = dev_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
connections=connections,
|
||||
identifiers=identifiers,
|
||||
connections=set(connections),
|
||||
identifiers=set(identifiers),
|
||||
name=device.name,
|
||||
manufacturer=device.manufacturer,
|
||||
model=device.model_name,
|
||||
@@ -155,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool
|
||||
# Update identifier.
|
||||
device_entry = dev_registry.async_update_device(
|
||||
device_entry.id,
|
||||
new_identifiers=identifiers,
|
||||
new_identifiers=set(identifiers),
|
||||
)
|
||||
|
||||
assert device_entry
|
||||
|
||||
@@ -18,6 +18,7 @@ HAS_PORT = {
|
||||
MonitorType.RADIUS,
|
||||
MonitorType.SNMP,
|
||||
MonitorType.SMTP,
|
||||
MonitorType.NTP,
|
||||
}
|
||||
HAS_HOST = HAS_PORT | {
|
||||
MonitorType.PING,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pythonkuma"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pythonkuma==0.5.1"]
|
||||
"requirements": ["pythonkuma==0.5.2"]
|
||||
}
|
||||
|
||||
@@ -118,8 +118,10 @@
|
||||
"mongodb": "MongoDB",
|
||||
"mqtt": "MQTT",
|
||||
"mysql": "MySQL/MariaDB",
|
||||
"ntp": "NTP",
|
||||
"oracledb": "Oracle Database",
|
||||
"ping": "Ping",
|
||||
"pm2": "PM2 Process",
|
||||
"port": "TCP port",
|
||||
"postgres": "PostgreSQL",
|
||||
"push": "Push",
|
||||
|
||||
Generated
+8
-8
@@ -922,7 +922,7 @@ emoji==2.8.0
|
||||
emulated-roku==0.3.0
|
||||
|
||||
# homeassistant.components.energieleser
|
||||
energieleser==0.1.5
|
||||
energieleser==0.1.6
|
||||
|
||||
# homeassistant.components.huisbaasje
|
||||
energyflip-client==0.2.2
|
||||
@@ -961,7 +961,7 @@ eq3btsmart==2.3.0
|
||||
esios_api==4.4.0
|
||||
|
||||
# homeassistant.components.esphome
|
||||
esphome-dashboard-api==1.3.0
|
||||
esphome-dashboard-api==1.4.0
|
||||
|
||||
# homeassistant.components.essent
|
||||
essent-dynamic-pricing==0.3.1
|
||||
@@ -1438,7 +1438,7 @@ knocki==0.4.2
|
||||
knx-frontend==2026.7.23.145751
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-telegram-store[sqlite,postgres]==0.11.1
|
||||
knx-telegram-store[sqlite,postgres]==0.11.2
|
||||
|
||||
# homeassistant.components.kraken
|
||||
krakenex==2.2.2
|
||||
@@ -1688,7 +1688,7 @@ nextdns==5.0.1
|
||||
nhc==0.8.0
|
||||
|
||||
# homeassistant.components.nibe_heatpump
|
||||
nibe==2.22.0
|
||||
nibe==2.24.0
|
||||
|
||||
# homeassistant.components.nice_go
|
||||
nice-go==1.0.2
|
||||
@@ -2786,7 +2786,7 @@ python-xbox==0.2.0
|
||||
pythonegardia==1.0.52
|
||||
|
||||
# homeassistant.components.uptime_kuma
|
||||
pythonkuma==0.5.1
|
||||
pythonkuma==0.5.2
|
||||
|
||||
# homeassistant.components.tile
|
||||
pytile==2024.12.0
|
||||
@@ -2916,7 +2916,7 @@ renault-api==0.5.12
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.21.7
|
||||
reolink-aio==0.21.8
|
||||
|
||||
# homeassistant.components.radio_frequency
|
||||
rf-protocols==4.3.0
|
||||
@@ -3406,10 +3406,10 @@ wyoming==1.10.0
|
||||
xiaomi-ble==1.11.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.17.0
|
||||
xknx==3.18.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.9.0
|
||||
xknxproject==3.10.0
|
||||
|
||||
# homeassistant.components.fritz
|
||||
# homeassistant.components.rest
|
||||
|
||||
@@ -10,6 +10,8 @@ from homeassistant.components.coolmaster.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _flow_data(send_wakeup_prompt: bool = False) -> dict:
|
||||
options: dict = {"host": "1.1.1.1"}
|
||||
@@ -109,3 +111,26 @@ async def test_form_no_units(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "no_units"}
|
||||
|
||||
|
||||
async def test_form_duplicate_host(hass: HomeAssistant) -> None:
|
||||
"""Test we abort when a bridge on this host is already configured."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"host": "1.1.1.1",
|
||||
"port": 10102,
|
||||
"supported_modes": AVAILABLE_MODES,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], _flow_data()
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Philips Hue binary_sensor platform tests for V2 bridge/api."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.json import JsonArrayType
|
||||
@@ -9,6 +12,60 @@ from homeassistant.util.json import JsonArrayType
|
||||
from .conftest import setup_platform
|
||||
from .const import FAKE_BINARY_SENSOR, FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY
|
||||
|
||||
MOTION_AWARE_ENTITY_ID = "binary_sensor.test_room_test_room_motion_aware_sensor_1"
|
||||
MOTION_AREA_CONFIGURATION_ID = "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b"
|
||||
AREA_MOTION_SERVICE_IDS = {
|
||||
"convenience_area_motion": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345",
|
||||
"security_area_motion": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f",
|
||||
}
|
||||
|
||||
MOTION_DETECTED = {
|
||||
"motion": True,
|
||||
"motion_valid": True,
|
||||
"motion_report": {"changed": "2023-09-23T08:20:51.384Z", "motion": True},
|
||||
}
|
||||
MOTION_CLEARED = {
|
||||
"motion": False,
|
||||
"motion_valid": True,
|
||||
"motion_report": {"changed": "2023-09-23T08:13:42.394Z", "motion": False},
|
||||
}
|
||||
MOTION_INVALID = {
|
||||
"motion": False,
|
||||
"motion_valid": False,
|
||||
"motion_report": {"changed": "2023-09-23T05:54:08.166Z", "motion": False},
|
||||
}
|
||||
|
||||
|
||||
def area_motion_service(
|
||||
service_type: str,
|
||||
*,
|
||||
enabled: bool = True,
|
||||
motion: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a service of the MotionAware zone, without `motion` when none is given."""
|
||||
service = {
|
||||
"id": AREA_MOTION_SERVICE_IDS[service_type],
|
||||
"owner": {
|
||||
"rid": MOTION_AREA_CONFIGURATION_ID,
|
||||
"rtype": "motion_area_configuration",
|
||||
},
|
||||
"enabled": enabled,
|
||||
"type": service_type,
|
||||
}
|
||||
if motion is not None:
|
||||
service["motion"] = motion
|
||||
return service
|
||||
|
||||
|
||||
def replace_resources(
|
||||
data: JsonArrayType, resources: list[dict[str, Any]]
|
||||
) -> JsonArrayType:
|
||||
"""Return the test data with each resource of the same id replaced."""
|
||||
replacements = {resource["id"]: resource for resource in resources}
|
||||
missing = replacements.keys() - {resource["id"] for resource in data}
|
||||
assert not missing, f"resource id(s) not present in the test data: {missing}"
|
||||
return [replacements.get(resource["id"], resource) for resource in data]
|
||||
|
||||
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
|
||||
@@ -88,7 +145,7 @@ async def test_binary_sensors(
|
||||
assert sensor.attributes["device_class"] == "motion"
|
||||
|
||||
# test motion aware sensor
|
||||
sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1")
|
||||
sensor = hass.states.get(MOTION_AWARE_ENTITY_ID)
|
||||
assert sensor is not None
|
||||
assert sensor.state == "off"
|
||||
assert sensor.name == "Test Room Motion Aware Sensor 1"
|
||||
@@ -195,15 +252,17 @@ async def test_motion_aware_sensor(
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
# test motion aware sensor exists and has correct state
|
||||
sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1")
|
||||
sensor = hass.states.get(MOTION_AWARE_ENTITY_ID)
|
||||
assert sensor is not None
|
||||
assert sensor.state == "off"
|
||||
assert sensor.attributes["device_class"] == "motion"
|
||||
|
||||
# test update of motion aware sensor works on incoming event
|
||||
# the zone in the test data has its convenience service enabled, so that is the
|
||||
# service reporting its motion
|
||||
updated_sensor = {
|
||||
"id": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f",
|
||||
"type": "security_area_motion",
|
||||
"id": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345",
|
||||
"type": "convenience_area_motion",
|
||||
"motion": {
|
||||
"motion": True,
|
||||
"motion_valid": True,
|
||||
@@ -212,7 +271,7 @@ async def test_motion_aware_sensor(
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", updated_sensor)
|
||||
await hass.async_block_till_done()
|
||||
sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1")
|
||||
sensor = hass.states.get(MOTION_AWARE_ENTITY_ID)
|
||||
assert sensor.state == "on"
|
||||
|
||||
# test name update when motion area configuration name changes
|
||||
@@ -225,6 +284,191 @@ async def test_motion_aware_sensor(
|
||||
await hass.async_block_till_done()
|
||||
# The entity name is derived from the motion area configuration name
|
||||
# but the entity ID doesn't change - we just verify the sensor still exists
|
||||
sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1")
|
||||
sensor = hass.states.get(MOTION_AWARE_ENTITY_ID)
|
||||
assert sensor is not None
|
||||
assert sensor.name == "Test Room Updated Motion Area"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("services", "expected_state"),
|
||||
[
|
||||
pytest.param(
|
||||
[
|
||||
area_motion_service("security_area_motion", motion=MOTION_CLEARED),
|
||||
area_motion_service("convenience_area_motion", motion=MOTION_DETECTED),
|
||||
],
|
||||
"on",
|
||||
id="bound_to_lights_reads_convenience",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
area_motion_service("security_area_motion", motion=MOTION_CLEARED),
|
||||
area_motion_service(
|
||||
"convenience_area_motion", enabled=False, motion=MOTION_DETECTED
|
||||
),
|
||||
],
|
||||
"off",
|
||||
id="not_bound_to_lights_reads_security",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
area_motion_service("security_area_motion"),
|
||||
area_motion_service("convenience_area_motion", motion=MOTION_DETECTED),
|
||||
],
|
||||
"on",
|
||||
id="hue_secure_security_without_motion_reads_convenience",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
area_motion_service("security_area_motion", motion=MOTION_INVALID),
|
||||
area_motion_service(
|
||||
"convenience_area_motion", enabled=False, motion=MOTION_DETECTED
|
||||
),
|
||||
],
|
||||
"unknown",
|
||||
id="not_bound_to_lights_without_valid_reading",
|
||||
),
|
||||
# a real zone can have its convenience service enabled while only the security
|
||||
# service reports, so an enabled service without a reading must not win
|
||||
pytest.param(
|
||||
[
|
||||
area_motion_service("security_area_motion", motion=MOTION_DETECTED),
|
||||
area_motion_service("convenience_area_motion", motion=MOTION_INVALID),
|
||||
],
|
||||
"on",
|
||||
id="falls_back_to_security_when_convenience_has_no_reading",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_motion_aware_sensor_motion_source(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
services: list[dict[str, Any]],
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test the MotionAware sensor reads the zone service that reports motion."""
|
||||
# every case gives the service that must be ignored the opposite state, so
|
||||
# reading the wrong one results in a state other than the asserted one
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
replace_resources(v2_resources_test_data, services)
|
||||
)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == expected_state
|
||||
|
||||
|
||||
async def test_motion_aware_sensor_follows_convenience_service(
|
||||
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
|
||||
) -> None:
|
||||
"""Test the MotionAware sensor updates on events of the convenience service."""
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
replace_resources(
|
||||
v2_resources_test_data,
|
||||
[
|
||||
area_motion_service("security_area_motion"),
|
||||
area_motion_service("convenience_area_motion", motion=MOTION_CLEARED),
|
||||
],
|
||||
)
|
||||
)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off"
|
||||
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
area_motion_service("convenience_area_motion", motion=MOTION_DETECTED),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "on"
|
||||
|
||||
|
||||
async def test_motion_aware_sensor_follows_security_service(
|
||||
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
|
||||
) -> None:
|
||||
"""Test the MotionAware sensor updates on events of the security service."""
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
replace_resources(
|
||||
v2_resources_test_data,
|
||||
[
|
||||
area_motion_service("security_area_motion", motion=MOTION_CLEARED),
|
||||
area_motion_service(
|
||||
"convenience_area_motion", enabled=False, motion=MOTION_CLEARED
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off"
|
||||
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
area_motion_service("security_area_motion", motion=MOTION_DETECTED),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "on"
|
||||
|
||||
|
||||
async def test_motion_aware_sensor_without_convenience_resource(
|
||||
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
|
||||
) -> None:
|
||||
"""Test the MotionAware sensor works when the convenience service is missing."""
|
||||
# the zone still lists the service, but the bridge never delivered the resource
|
||||
data = replace_resources(
|
||||
v2_resources_test_data,
|
||||
[area_motion_service("security_area_motion", motion=MOTION_DETECTED)],
|
||||
)
|
||||
convenience_id = AREA_MOTION_SERVICE_IDS["convenience_area_motion"]
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
[resource for resource in data if resource["id"] != convenience_id]
|
||||
)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "on"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("zone_update", "zone_restore"),
|
||||
[
|
||||
pytest.param({"enabled": False}, {"enabled": True}, id="zone_switched_off"),
|
||||
pytest.param(
|
||||
{"health": "not_running"}, {"health": "healthy"}, id="zone_not_running"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_motion_aware_sensor_zone_not_reporting(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
zone_update: dict[str, Any],
|
||||
zone_restore: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the MotionAware sensor reports unknown while its zone is not reporting."""
|
||||
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off"
|
||||
|
||||
# the services keep reporting a valid state while the zone itself does not
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": MOTION_AREA_CONFIGURATION_ID,
|
||||
"type": "motion_area_configuration",
|
||||
**zone_update,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "unknown"
|
||||
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": MOTION_AREA_CONFIGURATION_ID,
|
||||
"type": "motion_area_configuration",
|
||||
**zone_restore,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off"
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components.met.const import (
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import init_integration
|
||||
|
||||
@@ -50,30 +49,3 @@ async def test_fail_default_home_entry(
|
||||
"Skip setting up met.no integration; No Home location has been set"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_removing_incorrect_devices(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_weather,
|
||||
) -> None:
|
||||
"""Test we remove incorrect devices."""
|
||||
entry = await init_integration(hass)
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
name="Forecast_legacy",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)},
|
||||
manufacturer="Met.no",
|
||||
model="Forecast",
|
||||
configuration_url="https://www.met.no/en",
|
||||
)
|
||||
|
||||
assert await hass.config_entries.async_reload(entry.entry_id)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
assert not device_registry.async_get_device(identifiers={(DOMAIN,)})
|
||||
assert device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
||||
assert "Removing improper device Forecast_legacy" in caplog.text
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Test Met weather entity."""
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.met import DOMAIN
|
||||
from homeassistant.components.met.const import DOMAIN
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_WEATHER_DEW_POINT,
|
||||
|
||||
@@ -53,7 +53,7 @@ from .common import (
|
||||
help_test_update_with_json_attrs_not_dict,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_mqtt_message
|
||||
from tests.common import async_fire_mqtt_message
|
||||
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -547,10 +547,9 @@ async def test_entity_device_info_with_hub(
|
||||
) -> None:
|
||||
"""Test MQTT event device registry integration."""
|
||||
await mqtt_mock_entry()
|
||||
other_config_entry = MockConfigEntry()
|
||||
other_config_entry.add_to_hass(hass)
|
||||
mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
hub = device_registry.async_get_or_create(
|
||||
config_entry_id=other_config_entry.entry_id,
|
||||
config_entry_id=mqtt_config_entry.entry_id,
|
||||
connections=set(),
|
||||
identifiers={("mqtt", "hub-id")},
|
||||
manufacturer="manufacturer",
|
||||
|
||||
@@ -76,7 +76,6 @@ from .common import (
|
||||
)
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_capture_events,
|
||||
async_fire_mqtt_message,
|
||||
async_fire_time_changed,
|
||||
@@ -1585,10 +1584,9 @@ async def test_entity_device_info_with_hub(
|
||||
) -> None:
|
||||
"""Test MQTT sensor device registry integration."""
|
||||
await mqtt_mock_entry()
|
||||
other_config_entry = MockConfigEntry()
|
||||
other_config_entry.add_to_hass(hass)
|
||||
mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
hub = device_registry.async_get_or_create(
|
||||
config_entry_id=other_config_entry.entry_id,
|
||||
config_entry_id=mqtt_config_entry.entry_id,
|
||||
connections=set(),
|
||||
identifiers={("mqtt", "hub-id")},
|
||||
manufacturer="manufacturer",
|
||||
|
||||
@@ -45,6 +45,7 @@ def mock_tv():
|
||||
tv.notify_change_supported = False
|
||||
tv.pairing_type = None
|
||||
tv.powerstate = None
|
||||
tv.screenstate = None
|
||||
tv.source_id = None
|
||||
tv.ambilight_current_configuration = None
|
||||
tv.ambilight_styles = {}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Tests for the Philips TV media player."""
|
||||
|
||||
from haphilipsjs import PhilipsTV
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.philips_js.const import TV_STATE_OFF, TV_STATE_ON
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MOCK_ENTITY_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("powerstate", "screenstate", "expected_state"),
|
||||
[
|
||||
pytest.param(TV_STATE_ON, TV_STATE_OFF, STATE_ON, id="powerstate-on"),
|
||||
pytest.param("Standby", TV_STATE_ON, STATE_OFF, id="powerstate-standby"),
|
||||
pytest.param(None, TV_STATE_ON, STATE_ON, id="screenstate-on"),
|
||||
pytest.param(None, TV_STATE_OFF, STATE_OFF, id="screenstate-off"),
|
||||
],
|
||||
)
|
||||
async def test_state(
|
||||
hass: HomeAssistant,
|
||||
mock_tv: PhilipsTV,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
powerstate: str | None,
|
||||
screenstate: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test the media player state."""
|
||||
mock_tv.json_feature_supported.return_value = False
|
||||
mock_tv.powerstate = powerstate
|
||||
mock_tv.screenstate = screenstate
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(MOCK_ENTITY_ID))
|
||||
assert state.state == expected_state
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Tests for the Philips TV switches."""
|
||||
|
||||
from haphilipsjs import PhilipsTV
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.philips_js.const import TV_STATE_OFF
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "screenstate", "huelamp_power"),
|
||||
[
|
||||
pytest.param(
|
||||
"switch.philips_tv_screen_state",
|
||||
TV_STATE_OFF,
|
||||
None,
|
||||
id="screen",
|
||||
),
|
||||
pytest.param(
|
||||
"switch.philips_tv_ambilight_hue",
|
||||
None,
|
||||
TV_STATE_OFF,
|
||||
id="ambilight-hue",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_available_without_powerstate(
|
||||
hass: HomeAssistant,
|
||||
mock_tv: PhilipsTV,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
screenstate: str | None,
|
||||
huelamp_power: str | None,
|
||||
) -> None:
|
||||
"""Test switches are available when the power state endpoint is absent."""
|
||||
mock_tv.json_feature_supported.return_value = True
|
||||
mock_tv.powerstate = None
|
||||
mock_tv.screenstate = screenstate
|
||||
mock_tv.huelamp_power = huelamp_power
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == STATE_OFF
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for the SwitchBot Cloud integration init."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@@ -20,6 +20,7 @@ from homeassistant.components.switchbot_cloud.const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.switchbot_cloud.coordinator import SwitchBotCoordinator
|
||||
from homeassistant.components.webhook import DOMAIN as WEBHOOK_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
@@ -744,3 +745,28 @@ async def test_remove_entry_with_cloud_unavailable(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.config_entries.async_entries("switchbot_cloud")
|
||||
|
||||
|
||||
async def test_single_coordinator_for_multi_platform_device(
|
||||
hass: HomeAssistant, mock_list_devices: AsyncMock, mock_get_status: AsyncMock
|
||||
) -> None:
|
||||
"""Test that a multi-platform device creates only one coordinator."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="relay-switch-pm-id-1",
|
||||
deviceName="relay-switch-pm-1",
|
||||
deviceType="Relay Switch 1PM",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
mock_get_status.return_value = {"switchStatus": 0}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot_cloud.SwitchBotCoordinator",
|
||||
wraps=SwitchBotCoordinator,
|
||||
) as coordinator_cls:
|
||||
entry = await configure_integration(hass)
|
||||
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert coordinator_cls.call_count == 1
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
'dns',
|
||||
'docker',
|
||||
'system_service',
|
||||
'pm2',
|
||||
'real_browser',
|
||||
'group',
|
||||
'push',
|
||||
@@ -78,6 +79,7 @@
|
||||
'json_query',
|
||||
'kafka_producer',
|
||||
'mqtt',
|
||||
'ntp',
|
||||
'rabbit_mq',
|
||||
'sip_options',
|
||||
'smtp',
|
||||
@@ -139,6 +141,7 @@
|
||||
'dns',
|
||||
'docker',
|
||||
'system_service',
|
||||
'pm2',
|
||||
'real_browser',
|
||||
'group',
|
||||
'push',
|
||||
@@ -148,6 +151,7 @@
|
||||
'json_query',
|
||||
'kafka_producer',
|
||||
'mqtt',
|
||||
'ntp',
|
||||
'rabbit_mq',
|
||||
'sip_options',
|
||||
'smtp',
|
||||
@@ -770,6 +774,7 @@
|
||||
'dns',
|
||||
'docker',
|
||||
'system_service',
|
||||
'pm2',
|
||||
'real_browser',
|
||||
'group',
|
||||
'push',
|
||||
@@ -779,6 +784,7 @@
|
||||
'json_query',
|
||||
'kafka_producer',
|
||||
'mqtt',
|
||||
'ntp',
|
||||
'rabbit_mq',
|
||||
'sip_options',
|
||||
'smtp',
|
||||
@@ -840,6 +846,7 @@
|
||||
'dns',
|
||||
'docker',
|
||||
'system_service',
|
||||
'pm2',
|
||||
'real_browser',
|
||||
'group',
|
||||
'push',
|
||||
@@ -849,6 +856,7 @@
|
||||
'json_query',
|
||||
'kafka_producer',
|
||||
'mqtt',
|
||||
'ntp',
|
||||
'rabbit_mq',
|
||||
'sip_options',
|
||||
'smtp',
|
||||
@@ -1576,6 +1584,7 @@
|
||||
'dns',
|
||||
'docker',
|
||||
'system_service',
|
||||
'pm2',
|
||||
'real_browser',
|
||||
'group',
|
||||
'push',
|
||||
@@ -1585,6 +1594,7 @@
|
||||
'json_query',
|
||||
'kafka_producer',
|
||||
'mqtt',
|
||||
'ntp',
|
||||
'rabbit_mq',
|
||||
'sip_options',
|
||||
'smtp',
|
||||
@@ -1646,6 +1656,7 @@
|
||||
'dns',
|
||||
'docker',
|
||||
'system_service',
|
||||
'pm2',
|
||||
'real_browser',
|
||||
'group',
|
||||
'push',
|
||||
@@ -1655,6 +1666,7 @@
|
||||
'json_query',
|
||||
'kafka_producer',
|
||||
'mqtt',
|
||||
'ntp',
|
||||
'rabbit_mq',
|
||||
'sip_options',
|
||||
'smtp',
|
||||
|
||||
Reference in New Issue
Block a user