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
20915b0
Update uv to 0.12.2 (#178495)
renovate[bot] Aug 13, 2026
24a15d3
Bump pnpm in E2E tests (#179063)
bramkragten Aug 13, 2026
ad96b06
Add child devices (#178666)
emontnemery Aug 13, 2026
b5b1cb8
Split user flow init and submission in bsblan config flow tests (#179…
liudger Aug 13, 2026
cd593d4
Split user flow init and data submission in blebox config flow tests …
bkobus-bbx Aug 13, 2026
fe086f5
Split user flow init and data submission in insteon config flow tests…
connorgallopo Aug 13, 2026
34ec404
Refactor config flow tests in HTML5 integration (#179018)
tr4nt0r Aug 13, 2026
b69d1b9
Improve TechnoVE config flow init tests (#179042)
Moustachauve Aug 13, 2026
ff9adf3
Simplify collision handling (#179005)
arturpragacz Aug 13, 2026
f3b7cf6
Fix wrong snapshots in devolo Home Network tests (#179071)
Shutgun Aug 13, 2026
99f9f23
Reduce FFmpeg probe latency for streaming WAV from TTS (#178709)
vroland Aug 13, 2026
d723bf9
Bump aiounifi to v92 (#179017)
Kane610 Aug 13, 2026
4a77f18
Use state_extended bulk polling in vizio when available (#176559)
raman325 Aug 13, 2026
fc9f328
Reject iZone airflow values that are not multiples of 5 (#178977)
Swamp-Ig Aug 13, 2026
38ef496
Bump tesla-fleet-api to 1.9.0 (#179054)
Bre77 Aug 13, 2026
e7dad09
Add backwards compatibility for custom integrations accessing child d…
emontnemery Aug 13, 2026
b7e75b7
Report cache read tokens as cached tokens in Anthropic (#177604)
jamesshannon Aug 13, 2026
8820f18
Add setpoint temperature option to Home Connect (#167980)
Diegorro98 Aug 13, 2026
80fd0c5
Send update_type of 'updated' when a persistent notification already …
davidlang42 Aug 13, 2026
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
12 changes: 12 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
"pip_requirements",
"pre-commit",
"dockerfile",
"npm",
"custom.regex",
"homeassistant-manifest"
],

"ignorePaths": ["**/node_modules/**"],

"pre-commit": {
"enabled": true
},
Expand Down Expand Up @@ -180,6 +183,15 @@
"enabled": true,
"labels": ["dependency"]
},
{
"description": "pnpm version pinned in the E2E tests packageManager field (allowlisted)",
"matchManagers": ["npm"],
"matchFileNames": ["tests/e2e/package.json"],
"matchDepTypes": ["packageManager"],
"matchPackageNames": ["pnpm"],
"enabled": true,
"labels": ["dependency"]
},
{
"description": "For types-* stubs, only allow patch updates. Major/minor bumps track the upstream runtime package version and must be manually coordinated with the corresponding pin.",
"matchPackageNames": ["/^types-/"],
Expand Down
14 changes: 11 additions & 3 deletions homeassistant/auth/permissions/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import voluptuous as vol

from homeassistant.helpers import device_registry as dr

from .const import POLICY_CONTROL, POLICY_EDIT, POLICY_READ, SUBCAT_ALL
from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType
Expand Down Expand Up @@ -58,12 +60,18 @@ def _lookup_area(
if entity_entry is None or entity_entry.device_id is None:
return None

device_entry = perm_lookup.device_registry.async_get(entity_entry.device_id)
device_registry = perm_lookup.device_registry
device_entry = device_registry.async_get(entity_entry.device_id)

if device_entry is None:
return None

area_id = dr.async_get_effective_area_id(device_registry.hass, device_entry)

if device_entry is None or device_entry.area_id is None:
if area_id is None:
return None

return area_dict.get(device_entry.area_id)
return area_dict.get(area_id)


def _lookup_device(
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/alexa_devices/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ def async_get_entry_id_for_service_call(
"""Get the entry ID related to a service call (by device ID)."""
device_registry = dr.async_get(call.hass)
device_id = call.data[ATTR_DEVICE_ID]
if (device_entry := device_registry.async_get(device_id)) is None:
if (
device_entry := device_registry.async_get(
device_id, include_child_devices=False
)
) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device_id",
Expand Down
54 changes: 38 additions & 16 deletions homeassistant/components/analytics/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,35 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()


def _device_payload(device_entry: dr.AnyDeviceEntry) -> dict[str, Any]:
"""Return the analytics payload for a device or child device."""
if isinstance(device_entry, dr.ChildDeviceEntry):
# A child device carries no hardware or firmware metadata of its own;
# it is reported with its parent referenced as via_device.
return {
"entry_type": None,
"has_configuration_url": False,
"hw_version": None,
"manufacturer": None,
"model": None,
"model_id": None,
"sw_version": None,
"via_device": device_entry.parent_device_id,
"entities": [],
}
return {
"entry_type": device_entry.entry_type,
"has_configuration_url": device_entry.configuration_url is not None,
"hw_version": device_entry.hw_version,
"manufacturer": device_entry.manufacturer,
"model": device_entry.model,
"model_id": device_entry.model_id,
"sw_version": device_entry.sw_version,
"via_device": device_entry.via_device_id,
"entities": [],
}


async def _async_snapshot_payload(hass: HomeAssistant) -> dict:
"""Return detailed information about entities and devices for a snapshot."""
dev_reg = dr.async_get(hass)
Expand All @@ -745,13 +774,17 @@ async def _async_snapshot_payload(hass: HomeAssistant) -> dict:
removed_devices: set[str] = set()

# Get device list
for device_entry in dev_reg.devices.values():
for device_entry in (*dev_reg.devices.values(), *dev_reg.child_devices.values()):
config_entry = hass.config_entries.async_get_entry(device_entry.config_entry_id)

if config_entry is None:
continue

if device_entry.entry_type is dr.DeviceEntryType.SERVICE:
# Only full devices can be service devices; child devices never are.
if (
isinstance(device_entry, dr.DeviceEntry)
and device_entry.entry_type is dr.DeviceEntryType.SERVICE
):
removed_devices.add(device_entry.id)
continue

Expand Down Expand Up @@ -849,23 +882,12 @@ async def _async_snapshot_payload(hass: HomeAssistant) -> dict:
removed_devices.add(device_id)
continue

device_entry = dev_reg.devices[device_id]
resolved_device = dev_reg.async_get(device_id)
assert resolved_device is not None

device_id_mapping[device_id] = (integration_domain, len(devices_info))

devices_info.append(
{
"entry_type": device_entry.entry_type,
"has_configuration_url": device_entry.configuration_url is not None,
"hw_version": device_entry.hw_version,
"manufacturer": device_entry.manufacturer,
"model": device_entry.model,
"model_id": device_entry.model_id,
"sw_version": device_entry.sw_version,
"via_device": device_entry.via_device_id,
"entities": [],
}
)
devices_info.append(_device_payload(resolved_device))

# Fill out via_device with new device ids
for integration_info in integrations_info.values():
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/anthropic/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ def _create_token_stats(
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
cached_input_tokens = input_usage.cache_read_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/assist_pipeline/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,7 @@ def _get_all_targets_in_satellite_area(
if device_entry is None:
return False

area_id = device_entry.area_id
area_id = dr.async_get_effective_area_id(self.hass, device_entry)
if area_id is None:
return False

Expand All @@ -1402,7 +1402,9 @@ def _get_all_targets_in_satellite_area(
if target_device_entry is None:
return False

target_area_id = target_device_entry.area_id
target_area_id = dr.async_get_effective_area_id(
self.hass, target_device_entry
)

if target_area_id != area_id:
return False
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/avea/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class AveaLight(LightEntity):
_attr_has_entity_name = True
_attr_name = None
_attr_supported_color_modes = {ColorMode.HS}
_attr_device_info: DeviceInfo | None = None

def __init__(self, light: avea.Bulb, address: str) -> None:
"""Initialize an AveaLight."""
Expand Down
9 changes: 7 additions & 2 deletions homeassistant/components/bluetooth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,14 @@ async def async_update_device(
hw_version=details.get(ADAPTER_HW_VERSION),
)
if via_device_id and (via_device_entry := device_registry.async_get(via_device_id)):
# The bluetooth scanner may be child device; link to its parent.
if isinstance(via_device_entry, dr.ChildDeviceEntry):
via_device_id = via_device_entry.parent_device_id
kwargs: dict[str, Any] = {"via_device_id": via_device_id}
if not device_entry.area_id and via_device_entry.area_id:
kwargs["area_id"] = via_device_entry.area_id
# The source device may be an area-inheriting child, so use its effective area.
via_area_id = dr.async_get_effective_area_id(hass, via_device_entry)
if not device_entry.area_id and via_area_id:
kwargs["area_id"] = via_area_id
device_registry.async_update_device(device_entry.id, **kwargs)


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/bsblan/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _resolve_config_entry(
device_id: str = service_call.data[ATTR_DEVICE_ID]

device_registry = dr.async_get(service_call.hass)
device_entry = device_registry.async_get(device_id)
device_entry = device_registry.async_get(device_id, include_child_devices=False)

if device_entry is None:
raise ServiceValidationError(
Expand Down
19 changes: 16 additions & 3 deletions homeassistant/components/cloud/google_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,12 +500,25 @@ def _handle_device_registry_updated(
if event.data["action"] != "update" or "area_id" not in event.data["changes"]:
return

device_id = event.data["device_id"]
ent_reg = er.async_get(self.hass)

# Children without an area of their own inherit the parent's area, so a
# parent area change also changes the effective area of their entities.
device_ids = [device_id]
device_ids.extend(
child.id
for child in dr.async_entries_for_parent_device(
dr.async_get(self.hass), device_id
)
if child.area_id is None
)

# Check if any exposed entity uses the device area
if not any(
entity_entry.area_id is None and self.should_expose(entity_entry.entity_id)
for entity_entry in er.async_entries_for_device(
er.async_get(self.hass), event.data["device_id"]
)
for check_device_id in device_ids
for entity_entry in er.async_entries_for_device(ent_reg, check_device_id)
):
return

Expand Down
24 changes: 19 additions & 5 deletions homeassistant/components/config/device_registry.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""HTTP views to interact with the device registry."""

import logging
from typing import Any, cast
from typing import Any

import voluptuous as vol

Expand All @@ -11,7 +11,7 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry, DeviceEntryDisabler
from homeassistant.helpers.device_registry import DeviceEntryDisabler

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -92,7 +92,8 @@ def websocket_list_devices(
inner = b",".join(
[
entry.json_repr
for entry in registry.devices.values()
for container in (registry.devices, registry.child_devices)
for entry in container.values()
if entry.json_repr is not None
]
)
Expand Down Expand Up @@ -127,10 +128,18 @@ def websocket_list_linked_devices(
)
return

# A child device is never linked: its identifiers share the parent's
# per-config-entry namespace, so matching them against other entries' main
# devices is not meaningful.
if isinstance(device, dr.ChildDeviceEntry):
connection.send_result(msg["id"], {"linked_devices": []})
return

linked_devices = [
entry.id
for entry in registry.async_get_devices(
identifiers=device.identifiers, connections=device.connections
identifiers=device.identifiers,
connections=device.connections,
)
if entry.id != device_id
]
Expand Down Expand Up @@ -170,7 +179,12 @@ def websocket_update_device(
# Convert labels to a set
msg["labels"] = set(msg["labels"])

entry = cast(DeviceEntry, registry.async_update_device(**msg))
entry: dr.AnyDeviceEntry | None
if msg["device_id"] in registry.child_devices:
entry = registry.async_update_child_device(**msg)
else:
entry = registry.async_update_device(**msg)
assert entry is not None

connection.send_message(websocket_api.result_message(msg_id, entry.dict_repr))

Expand Down
10 changes: 4 additions & 6 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1254,12 +1254,10 @@ def _get_satellite_area_and_device(
area_id = entity_entry.area_id
device_id = entity_entry.device_id

if (
area_id is None
and device_id is not None
and (device_entry := dr.async_get(hass).async_get(device_id)) is not None
):
area_id = device_entry.area_id
if area_id is None and device_id is not None:
device_registry = dr.async_get(hass)
if (device_entry := device_registry.async_get(device_id)) is not None:
area_id = dr.async_get_effective_area_id(hass, device_entry)

if area_id is None:
return None, device_id
Expand Down
17 changes: 13 additions & 4 deletions homeassistant/components/deconz/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,9 @@ async def async_validate_trigger_config(
config = TRIGGER_SCHEMA(config)

device_registry = dr.async_get(hass)
device = device_registry.async_get(config[CONF_DEVICE_ID])
device = device_registry.async_get(
config[CONF_DEVICE_ID], include_child_devices=False
)

trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])

Expand Down Expand Up @@ -731,7 +733,14 @@ async def async_attach_trigger(
event_data: dict[str, int | str] = {}

device_registry = dr.async_get(hass)
device = device_registry.devices[config[CONF_DEVICE_ID]]
device = device_registry.async_get(
config[CONF_DEVICE_ID], include_child_devices=False
)

if not device:
raise InvalidDeviceAutomationConfig(
f"deCONZ trigger device with ID {config[CONF_DEVICE_ID]} not found"
)

deconz_event = _get_deconz_event_from_device(hass, device)
if event_id := deconz_event.serial:
Expand Down Expand Up @@ -764,9 +773,9 @@ async def async_get_triggers(
Generate device trigger list.
"""
device_registry = dr.async_get(hass)
device = device_registry.devices[device_id]
device = device_registry.async_get(device_id, include_child_devices=False)

if device.model not in REMOTES:
if device is None or device.model not in REMOTES:
return []

triggers = []
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/derivative/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
)
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.device import async_entity_id_to_device
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.device_registry import AnyDeviceEntry
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
Expand Down Expand Up @@ -199,7 +199,7 @@ def __init__(
unit_time: UnitOfTime,
max_sub_interval: timedelta | None,
unique_id: str | None,
device: DeviceEntry | None = None,
device: AnyDeviceEntry | None = None,
) -> None:
"""Initialize the derivative sensor."""
self._attr_unique_id = unique_id
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/device_tracker/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,12 @@ def handle_device_event(ev: Event[EventDeviceRegistryUpdatedData]) -> None:
return

dev_reg = dr.async_get(hass)
device_entry = dev_reg.async_get(ev.data["device_id"])
device_entry = dev_reg.async_get(
ev.data["device_id"], include_child_devices=False
)

if device_entry is None:
# This should not happen, since the device was just created.
# A child device resolves to None here; it has no MAC to match.
return

# Check if device has a mac
Expand Down
Loading
Loading