Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
91e7b75
Fix errors in legacy platform in PlayStation Network integration (#14…
tr4nt0r Jun 25, 2025
10d1aff
Migrate lyric to use runtime_data (#147475)
epenet Jun 25, 2025
2bcdc03
Migrate lupusec to use runtime_data (#147476)
epenet Jun 25, 2025
f22b623
Move luftdaten coordinator to separate module (#147477)
epenet Jun 25, 2025
51da1bc
Migrate loqed to use runtime_data (#147478)
epenet Jun 25, 2025
909d950
Migrate luftdaten to use runtime_data (#147480)
epenet Jun 25, 2025
69bf79d
Migrate local_calendar to use runtime_data (#147481)
epenet Jun 25, 2025
7031167
Set has entity name to True in Meater (#146954)
joostlek Jun 25, 2025
066e840
Migrate lookin to use runtime_data (#147479)
epenet Jun 25, 2025
51fb1ab
Refactor Meater availability (#146956)
joostlek Jun 25, 2025
85e9919
Add entity category option to entities set up via an MQTT subentry (#…
jbouwh Jun 25, 2025
d0b2d1d
Add evaporative humidifier for switchbot integration (#146235)
zerzhang Jun 25, 2025
f800248
Add more binary sensors to Alexa Devices (#146402)
chemelli74 Jun 25, 2025
f4b95ff
Ezviz battery camera work mode (#130478)
srescio Jun 25, 2025
33bd35b
Migrate Meater to use HassKey (#147485)
joostlek Jun 25, 2025
58e60fd
Bump hass-nabucasa from 0.103.0 to 0.104.0 (#147488)
ludeeus Jun 25, 2025
0a884c7
Add subdevices support to ESPHome (#147343)
bdraco Jun 25, 2025
0bbb168
Add Home Connect DHCP information (#147494)
Diegorro98 Jun 25, 2025
f897a72
Fix Google AI not using correct config options after subentries migra…
tronikos Jun 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions homeassistant/components/alexa_devices/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Final

from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import SENSOR_STATE_OFF

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
Expand All @@ -28,21 +29,58 @@
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Alexa Devices binary sensor entity description."""

is_on_fn: Callable[[AmazonDevice], bool]
is_on_fn: Callable[[AmazonDevice, str], bool]
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True


BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda _device: _device.online,
is_on_fn=lambda device, _: device.online,
),
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda _device: _device.bluetooth_state,
is_on_fn=lambda device, _: device.bluetooth_state,
),
AmazonBinarySensorEntityDescription(
key="babyCryDetectionState",
translation_key="baby_cry_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
AmazonBinarySensorEntityDescription(
key="beepingApplianceDetectionState",
translation_key="beeping_appliance_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
AmazonBinarySensorEntityDescription(
key="coughDetectionState",
translation_key="cough_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
AmazonBinarySensorEntityDescription(
key="dogBarkDetectionState",
translation_key="dog_bark_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
AmazonBinarySensorEntityDescription(
key="humanPresenceDetectionState",
device_class=BinarySensorDeviceClass.MOTION,
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
AmazonBinarySensorEntityDescription(
key="waterSoundsDetectionState",
translation_key="water_sounds_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
)

Expand All @@ -60,6 +98,7 @@ async def async_setup_entry(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in coordinator.data
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
)


Expand All @@ -71,4 +110,6 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return self.entity_description.is_on_fn(self.device)
return self.entity_description.is_on_fn(
self.device, self.entity_description.key
)
34 changes: 32 additions & 2 deletions homeassistant/components/alexa_devices/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,39 @@
"entity": {
"binary_sensor": {
"bluetooth": {
"default": "mdi:bluetooth",
"default": "mdi:bluetooth-off",
"state": {
"off": "mdi:bluetooth-off"
"on": "mdi:bluetooth"
}
},
"baby_cry_detection": {
"default": "mdi:account-voice-off",
"state": {
"on": "mdi:account-voice"
}
},
"beeping_appliance_detection": {
"default": "mdi:bell-off",
"state": {
"on": "mdi:bell-ring"
}
},
"cough_detection": {
"default": "mdi:blur-off",
"state": {
"on": "mdi:blur"
}
},
"dog_bark_detection": {
"default": "mdi:dog-side-off",
"state": {
"on": "mdi:dog-side"
}
},
"water_sounds_detection": {
"default": "mdi:water-pump-off",
"state": {
"on": "mdi:water-pump"
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/alexa_devices/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@
"binary_sensor": {
"bluetooth": {
"name": "Bluetooth"
},
"baby_cry_detection": {
"name": "Baby crying"
},
"beeping_appliance_detection": {
"name": "Beeping appliance"
},
"cough_detection": {
"name": "Coughing"
},
"dog_bark_detection": {
"name": "Dog barking"
},
"water_sounds_detection": {
"name": "Water sounds"
}
},
"notify": {
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.103.0"],
"requirements": ["hass-nabucasa==0.104.0"],
"single_config_entry": true
}
103 changes: 89 additions & 14 deletions homeassistant/components/esphome/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import Awaitable, Callable, Coroutine
import functools
import logging
import math
from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar, cast

Expand All @@ -13,7 +14,6 @@
EntityCategory as EsphomeEntityCategory,
EntityInfo,
EntityState,
build_unique_id,
)
import voluptuous as vol

Expand All @@ -24,6 +24,7 @@
config_validation as cv,
device_registry as dr,
entity_platform,
entity_registry as er,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
Expand All @@ -32,9 +33,11 @@
from .const import DOMAIN

# Import config flow so that it's added to the registry
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData, build_device_unique_id
from .enum_mapper import EsphomeEnumMapper

_LOGGER = logging.getLogger(__name__)

_InfoT = TypeVar("_InfoT", bound=EntityInfo)
_EntityT = TypeVar("_EntityT", bound="EsphomeEntity[Any,Any]")
_StateT = TypeVar("_StateT", bound=EntityState)
Expand All @@ -53,21 +56,74 @@ def async_static_info_updated(
) -> None:
"""Update entities of this platform when entities are listed."""
current_infos = entry_data.info[info_type]
device_info = entry_data.device_info
if TYPE_CHECKING:
assert device_info is not None
new_infos: dict[int, EntityInfo] = {}
add_entities: list[_EntityT] = []

ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)

for info in infos:
if not current_infos.pop(info.key, None):
# Create new entity
new_infos[info.key] = info

# Create new entity if it doesn't exist
if not (old_info := current_infos.pop(info.key, None)):
entity = entity_type(entry_data, platform.domain, info, state_type)
add_entities.append(entity)
new_infos[info.key] = info
continue

# Entity exists - check if device_id has changed
if old_info.device_id == info.device_id:
continue

# Entity has switched devices, need to migrate unique_id
old_unique_id = build_device_unique_id(device_info.mac_address, old_info)
entity_id = ent_reg.async_get_entity_id(platform.domain, DOMAIN, old_unique_id)

# If entity not found in registry, re-add it
# This happens when the device_id changed and the old device was deleted
if entity_id is None:
_LOGGER.info(
"Entity with old unique_id %s not found in registry after device_id "
"changed from %s to %s, re-adding entity",
old_unique_id,
old_info.device_id,
info.device_id,
)
entity = entity_type(entry_data, platform.domain, info, state_type)
add_entities.append(entity)
continue

updates: dict[str, Any] = {}
new_unique_id = build_device_unique_id(device_info.mac_address, info)

# Update unique_id if it changed
if old_unique_id != new_unique_id:
updates["new_unique_id"] = new_unique_id

# Update device assignment
if info.device_id:
# Entity now belongs to a sub device
new_device = dev_reg.async_get_device(
identifiers={(DOMAIN, f"{device_info.mac_address}_{info.device_id}")}
)
else:
# Entity now belongs to the main device
new_device = dev_reg.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
)

if new_device:
updates["device_id"] = new_device.id

# Apply all updates at once
if updates:
ent_reg.async_update_entity(entity_id, **updates)

# Anything still in current_infos is now gone
if current_infos:
device_info = entry_data.device_info
if TYPE_CHECKING:
assert device_info is not None
entry_data.async_remove_entities(
hass, current_infos.values(), device_info.mac_address
)
Expand Down Expand Up @@ -244,19 +300,36 @@ def __init__(
self._key = entity_info.key
self._state_type = state_type
self._on_static_info_update(entity_info)
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
)

device_name = device_info.name
# Determine the device connection based on whether this entity belongs to a sub device
if entity_info.device_id:
# Entity belongs to a sub device
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{device_info.mac_address}_{entity_info.device_id}")
}
)
# Use the pre-computed device_id_to_name mapping for O(1) lookup
device_name = entry_data.device_id_to_name.get(
entity_info.device_id, device_info.name
)
else:
# Entity belongs to the main device
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
)

if entity_info.name:
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
self.entity_id = f"{domain}.{device_name}_{entity_info.object_id}"
else:
# https://github.com/home-assistant/core/issues/132532
# If name is not set, ESPHome will use the sanitized friendly name
# as the name, however we want to use the original object_id
# as the entity_id before it is sanitized since the sanitizer
# is not utf-8 aware. In this case, its always going to be
# an empty string so we drop the object_id.
self.entity_id = f"{domain}.{device_info.name}"
self.entity_id = f"{domain}.{device_name}"

async def async_added_to_hass(self) -> None:
"""Register callbacks."""
Expand Down Expand Up @@ -290,7 +363,9 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None:
static_info = cast(_InfoT, static_info)
assert device_info
self._static_info = static_info
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
self._attr_unique_id = build_device_unique_id(
device_info.mac_address, static_info
)
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
# https://github.com/home-assistant/core/issues/132532
# If the name is "", we need to set it to None since otherwise
Expand Down
24 changes: 22 additions & 2 deletions homeassistant/components/esphome/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@
}


def build_device_unique_id(mac: str, entity_info: EntityInfo) -> str:
"""Build unique ID for entity, appending @device_id if it belongs to a sub-device.

This wrapper around build_unique_id ensures that entities belonging to sub-devices
have their device_id appended to the unique_id to handle proper migration when
entities move between devices.
"""
base_unique_id = build_unique_id(mac, entity_info)

# If entity belongs to a sub-device, append @device_id
if entity_info.device_id:
return f"{base_unique_id}@{entity_info.device_id}"

return base_unique_id


class StoreData(TypedDict, total=False):
"""ESPHome storage data."""

Expand Down Expand Up @@ -160,6 +176,7 @@ class RuntimeEntryData:
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
default_factory=list
)
device_id_to_name: dict[int, str] = field(default_factory=dict)

@property
def name(self) -> str:
Expand Down Expand Up @@ -222,7 +239,9 @@ def async_remove_entities(
ent_reg = er.async_get(hass)
for info in static_infos:
if entry := ent_reg.async_get_entity_id(
INFO_TYPE_TO_PLATFORM[type(info)], DOMAIN, build_unique_id(mac, info)
INFO_TYPE_TO_PLATFORM[type(info)],
DOMAIN,
build_device_unique_id(mac, info),
):
ent_reg.async_remove(entry)

Expand Down Expand Up @@ -278,7 +297,8 @@ async def async_update_static_infos(
if (
(old_unique_id := info.unique_id)
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
and (new_unique_id := build_unique_id(mac, info)) != old_unique_id
and (new_unique_id := build_device_unique_id(mac, info))
!= old_unique_id
and not registry_get_entity(platform, DOMAIN, new_unique_id)
):
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
Expand Down
Loading
Loading