Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion homeassistant/components/ambient_station/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# 1 -> 2: Unique ID format changed, so delete and re-import:
if version == 1:
dev_reg = dr.async_get(hass)
dev_reg.async_clear_config_entry(entry.entry_id)
dev_reg.async_clear_config_entry(entry.entry_id, entry.domain)

en_reg = er.async_get(hass)
en_reg.async_clear_config_entry(entry.entry_id)
Expand Down
20 changes: 10 additions & 10 deletions homeassistant/components/androidtv/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from typing import Any

import attr

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.components.diagnostics import (
async_redact_data,
device_entry_as_dict,
entity_entry_as_dict,
)
from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
Expand Down Expand Up @@ -40,7 +42,7 @@ async def async_get_config_entry_diagnostics(
return data

data["device"] = {
**async_redact_data(attr.asdict(hass_device), TO_REDACT_DEV),
**async_redact_data(device_entry_as_dict(hass_device), TO_REDACT_DEV),
"entities": {},
}

Expand All @@ -60,13 +62,11 @@ async def async_get_config_entry_diagnostics(
# The context doesn't provide useful information in this case.
state_dict.pop("context", None)

entity_dict = entity_entry_as_dict(entity_entry)
# The entity_id is already provided at root level (the key).
del entity_dict["entity_id"]
data["device"]["entities"][entity_entry.entity_id] = {
**async_redact_data(
attr.asdict(
entity_entry, filter=lambda attr, value: attr.name != "entity_id"
),
TO_REDACT,
),
**async_redact_data(entity_dict, TO_REDACT),
"state": state_dict,
}

Expand Down
20 changes: 10 additions & 10 deletions homeassistant/components/asuswrt/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from typing import Any

import attr

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.components.diagnostics import (
async_redact_data,
device_entry_as_dict,
entity_entry_as_dict,
)
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_IDENTIFIERS,
Expand Down Expand Up @@ -39,7 +41,7 @@ async def async_get_config_entry_diagnostics(
return data

data["device"] = {
**async_redact_data(attr.asdict(hass_device), TO_REDACT_DEV),
**async_redact_data(device_entry_as_dict(hass_device), TO_REDACT_DEV),
"entities": {},
"tracked_devices": [],
}
Expand All @@ -60,13 +62,11 @@ async def async_get_config_entry_diagnostics(
# The context doesn't provide useful information in this case.
state_dict.pop("context", None)

entity_dict = entity_entry_as_dict(entity_entry)
# The entity_id is already provided at root level (the key).
del entity_dict["entity_id"]
data["device"]["entities"][entity_entry.entity_id] = {
**async_redact_data(
attr.asdict(
entity_entry, filter=lambda attr, value: attr.name != "entity_id"
),
TO_REDACT,
),
**async_redact_data(entity_dict, TO_REDACT),
"state": state_dict,
}

Expand Down
37 changes: 37 additions & 0 deletions homeassistant/components/device_automation/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@
}


def _resolve_device_id(hass: HomeAssistant, device_id: str, domain: str) -> str:
"""Resolve a device automation device id, following a composite device id.

A device automation created when a device could be connected to more than one
config entry stores the id of the (now removed) composite device. When the
automation's domain owns one of the split devices' config entries, resolve to that
device - an integration may look the device up in its own registry, which only
knows the current device id, not the removed composite id.
"""
device_registry = dr.async_get(hass)
if device_id in device_registry.devices:
return device_id
if not (
split_devices := device_registry.async_get_devices_for_composite_device_id(
device_id
)
):
return device_id
# Resolve to the device owned by a config entry of the automation's domain
for split_device in split_devices:
entry = hass.config_entries.async_get_entry(split_device.config_entry_id)
if entry is not None and entry.domain == domain:
return split_device.id
return device_id


async def async_validate_device_automation_config(
hass: HomeAssistant,
config: ConfigType,
Expand All @@ -51,6 +77,17 @@ async def async_validate_device_automation_config(
) -> ConfigType:
"""Validate config."""
validated_config: ConfigType = automation_schema(config)

# A device automation may reference a pre-migration composite device id; resolve it
# to the split device for its domain so the device and its entities exist and the
# integration platform (validation and attach/call) receives a live device id
resolved_device_id = _resolve_device_id(
hass, validated_config[CONF_DEVICE_ID], validated_config[CONF_DOMAIN]
)
if resolved_device_id != validated_config[CONF_DEVICE_ID]:
config = {**config, CONF_DEVICE_ID: resolved_device_id}
validated_config = {**validated_config, CONF_DEVICE_ID: resolved_device_id}

platform = await async_get_device_automation_platform(
hass, validated_config[CONF_DOMAIN], automation_type
)
Expand Down
11 changes: 8 additions & 3 deletions homeassistant/components/diagnostics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@
from homeassistant.util.json import format_unserializable_data

from .const import DOMAIN, REDACTED, DiagnosticsSubType, DiagnosticsType
from .util import async_redact_data, entity_entry_as_dict

__all__ = ["REDACTED", "async_redact_data", "entity_entry_as_dict"]
from .util import async_redact_data, device_entry_as_dict, entity_entry_as_dict

__all__ = [
"REDACTED",
"async_redact_data",
"device_entry_as_dict",
"entity_entry_as_dict",
]

_LOGGER = logging.getLogger(__name__)

Expand Down
28 changes: 28 additions & 0 deletions homeassistant/components/diagnostics/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import attr

from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_registry import RegistryEntry

from .const import REDACTED
Expand Down Expand Up @@ -45,6 +46,33 @@ def async_redact_data[_T](data: _T, to_redact: Iterable[Any]) -> _T:
return cast(_T, redacted)


# DeviceEntry attributes that are internal bookkeeping and must not be exposed in
# diagnostics. Underscore attributes (_cache, _suggested_area, and the transient
# _pending_move / _composite_subentries) are excluded separately by _device_entry_filter.
# The composite-device migration attributes below can be removed in HA Core 2027.8.
_INTERNAL_DEVICE_ENTRY_ATTRIBUTES = (
"composite_device_id",
"composite_primary_config_entry",
"has_composite_identifiers",
"split_at",
)


def _device_entry_filter(a: attr.Attribute, _: Any) -> bool:
return (
not a.name.startswith("_") and a.name not in _INTERNAL_DEVICE_ENTRY_ATTRIBUTES
)


@callback
def device_entry_as_dict(entry: DeviceEntry) -> dict[str, Any]:
"""Convert a device registry entry to a dict for diagnostics.

This excludes internal fields that should not be exposed in diagnostics.
"""
return attr.asdict(entry, filter=_device_entry_filter)


def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
return a.name not in (
"_cache",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/dwd_weather_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ async def async_setup_entry(
"""Set up a config entry."""
device_registry = dr.async_get(hass)
if device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}):
device_registry.async_clear_config_entry(entry.entry_id)
device_registry.async_clear_config_entry(entry.entry_id, entry.domain)
coordinator = DwdWeatherWarningsCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()

Expand Down
12 changes: 6 additions & 6 deletions homeassistant/components/enphase_envoy/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
from typing import TYPE_CHECKING, Any

from aiohttp import ClientResponse
from attr import asdict
from pyenphase.envoy import Envoy
from pyenphase.exceptions import EnvoyError

from homeassistant.components.diagnostics import async_redact_data, entity_entry_as_dict
from homeassistant.components.diagnostics import (
async_redact_data,
device_entry_as_dict,
entity_entry_as_dict,
)
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
Expand Down Expand Up @@ -119,10 +122,7 @@ async def async_get_config_entry_diagnostics(
state_dict.pop("context", None)
entity_dict = entity_entry_as_dict(entity)
entities.append({"entity": entity_dict, "state": state_dict})
device_dict = asdict(device)
device_dict.pop("_cache", None)
# This can be removed when suggested_area is removed from DeviceEntry
device_dict.pop("_suggested_area")
device_dict = device_entry_as_dict(device)
device_entities.append({"device": device_dict, "entities": entities})

# remove envoy serial
Expand Down
9 changes: 5 additions & 4 deletions homeassistant/components/hassio/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from typing import Any

from attr import asdict

from homeassistant.components.diagnostics import entity_entry_as_dict
from homeassistant.components.diagnostics import (
device_entry_as_dict,
entity_entry_as_dict,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
Expand Down Expand Up @@ -53,7 +54,7 @@ async def async_get_config_entry_diagnostics(
{"entry": entity_entry_as_dict(entity_entry), "state": state_dict}
)

devices.append({"device": asdict(device), "entities": entities})
devices.append({"device": device_entry_as_dict(device), "entities": entities})

return {
"coordinator_data": coordinator.data.to_dict(),
Expand Down
10 changes: 6 additions & 4 deletions homeassistant/components/hunterdouglas_powerview/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from dataclasses import asdict
from typing import Any

import attr

from homeassistant.components.diagnostics import async_redact_data, entity_entry_as_dict
from homeassistant.components.diagnostics import (
async_redact_data,
device_entry_as_dict,
entity_entry_as_dict,
)
from homeassistant.const import ATTR_CONFIGURATION_URL, CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
Expand Down Expand Up @@ -75,7 +77,7 @@ def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str,
# Gather information how this device is represented in Home Assistant
entity_registry = er.async_get(hass)

data = async_redact_data(attr.asdict(device), REDACT_CONFIG)
data = async_redact_data(device_entry_as_dict(device), REDACT_CONFIG)
data["entities"] = []
entities: list[dict[str, Any]] = data["entities"]

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/intellifire/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["intellifire4py"],
"requirements": ["intellifire4py==4.4.0"]
"requirements": ["intellifire4py==4.5.0"]
}
10 changes: 9 additions & 1 deletion homeassistant/components/lg_thinq/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import logging
from typing import Any, override

from aiohttp import ClientError
from thinqconnect import ThinQAPIException
from thinqconnect.devices.const import Location
from thinqconnect.integration import PropertyState

from homeassistant.core import callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
Expand Down Expand Up @@ -112,3 +113,10 @@ async def async_call_api(
if on_fail_method:
on_fail_method()
raise ServiceValidationError(exc) from exc
except (TimeoutError, ClientError) as exc:
if on_fail_method:
on_fail_method()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
) from exc
3 changes: 3 additions & 0 deletions homeassistant/components/lg_thinq/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,9 @@
}
},
"exceptions": {
"connection_error": {
"message": "Failed to connect to the LG ThinQ cloud. Please try again later."
},
"failed_to_connect_mqtt": {
"message": "Failed to connect MQTT: {error}"
}
Expand Down
17 changes: 10 additions & 7 deletions homeassistant/components/nut/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from typing import Any

import attr

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.components.diagnostics import (
async_redact_data,
device_entry_as_dict,
entity_entry_as_dict,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
Expand Down Expand Up @@ -41,7 +43,7 @@ async def async_get_config_entry_diagnostics(
assert hass_device is not None

data["device"] = {
**attr.asdict(hass_device),
**device_entry_as_dict(hass_device),
"entities": {},
}

Expand All @@ -61,10 +63,11 @@ async def async_get_config_entry_diagnostics(
# The context doesn't provide useful information in this case.
state_dict.pop("context", None)

entity_dict = entity_entry_as_dict(entity_entry)
# The entity_id is already provided at root level (the key).
del entity_dict["entity_id"]
data["device"]["entities"][entity_entry.entity_id] = {
**attr.asdict(
entity_entry, filter=lambda attr, value: attr.name != "entity_id"
),
**entity_dict,
"state": state_dict,
}

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/samsungtv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async def async_migrate_entry(
# 1 -> 2: Unique ID format changed, so delete and re-import:
if version == 1:
dev_reg = dr.async_get(hass)
dev_reg.async_clear_config_entry(config_entry.entry_id)
dev_reg.async_clear_config_entry(config_entry.entry_id, config_entry.domain)

en_reg = er.async_get(hass)
en_reg.async_clear_config_entry(config_entry.entry_id)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/steam_online/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class PlayerData:
loccityid: int | None = None
gameextrainfo: str | None = None
gameid: str | None = None
lobbysteamid: str | None = None
level: int | None = None


Expand Down
Loading
Loading