mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 10:16:02 -05:00
203 lines
6.7 KiB
Python
203 lines
6.7 KiB
Python
"""Device registry helpers for the Netatmo integration."""
|
|
|
|
from collections.abc import Iterable, Iterator
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pyatmo
|
|
from pyatmo.modules.device_types import DEVICE_DESCRIPTION_MAP
|
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers import device_registry as dr
|
|
|
|
from .const import CONF_URL_CONTROL, DOMAIN, MANUFACTURER
|
|
|
|
if TYPE_CHECKING:
|
|
from .coordinator import NetatmoConfigEntry
|
|
|
|
|
|
def _bridged_children(home: pyatmo.Home) -> Iterator[tuple[str, str]]:
|
|
"""Yield (module id, parent id) for every child a module lists as bridged."""
|
|
return (
|
|
(child_id, module.entity_id)
|
|
for module in home.modules.values()
|
|
for child_id in module.modules or ()
|
|
if child_id in home.modules and child_id != module.entity_id
|
|
)
|
|
|
|
|
|
def _declared_bridges(home: pyatmo.Home) -> Iterator[tuple[str, str]]:
|
|
"""Yield (module id, parent id) for every module that names its bridge."""
|
|
return (
|
|
(module.entity_id, bridge)
|
|
for module in home.modules.values()
|
|
if (bridge := module.bridge)
|
|
and bridge in home.modules
|
|
and bridge != module.entity_id
|
|
)
|
|
|
|
|
|
def netatmo_module_parents(account: pyatmo.AsyncAccount) -> dict[str, str]:
|
|
"""Map each module id to the id of the module it reports through.
|
|
|
|
The API records the relationship from both ends and neither end is
|
|
complete: a station lists bridged children that never name it back, and a
|
|
module can name a bridge that does not list it. `bridge` is single-valued,
|
|
so it wins wherever the two disagree.
|
|
"""
|
|
parents: dict[str, str] = {}
|
|
for home in account.homes.values():
|
|
for child_id, parent_id in _bridged_children(home):
|
|
parents.setdefault(child_id, parent_id)
|
|
parents.update(_declared_bridges(home))
|
|
|
|
return parents
|
|
|
|
|
|
def _register_bridge(
|
|
device_registry: dr.DeviceRegistry,
|
|
entry: NetatmoConfigEntry,
|
|
home: pyatmo.Home,
|
|
module_id: str,
|
|
module_parents: dict[str, str],
|
|
parent_device_ids: dict[str, str],
|
|
seen: set[str],
|
|
) -> str:
|
|
"""Register a bridging module after its own bridge and return its device id."""
|
|
if (device_id := parent_device_ids.get(module_id)) is not None:
|
|
return device_id
|
|
|
|
seen.add(module_id)
|
|
via_device_id = parent_device_ids[home.entity_id]
|
|
parent_id = module_parents.get(module_id)
|
|
# `seen` bounds the walk; a cycle in unvalidated API data would not terminate
|
|
if parent_id is not None and parent_id not in seen:
|
|
via_device_id = _register_bridge(
|
|
device_registry,
|
|
entry,
|
|
home,
|
|
parent_id,
|
|
module_parents,
|
|
parent_device_ids,
|
|
seen,
|
|
)
|
|
|
|
module = home.modules[module_id]
|
|
manufacturer, model = DEVICE_DESCRIPTION_MAP.get(
|
|
module.device_type, (MANUFACTURER, module.device_type.value)
|
|
)
|
|
device_entry = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
identifiers={(DOMAIN, module_id)},
|
|
manufacturer=manufacturer,
|
|
model=model,
|
|
name=module.name,
|
|
via_device_id=via_device_id,
|
|
)
|
|
parent_device_ids[module_id] = device_entry.id
|
|
return device_entry.id
|
|
|
|
|
|
@callback
|
|
def async_disabled_netatmo_ids(
|
|
hass: HomeAssistant, entry: NetatmoConfigEntry
|
|
) -> list[str]:
|
|
"""Return the Netatmo ids of every disabled device.
|
|
|
|
A superset of the disabled home ids. Module ids never match a home id, so
|
|
passing them through to pyatmo's denylist is harmless and avoids having to
|
|
know which devices are homes before the topology has been fetched.
|
|
"""
|
|
return [
|
|
identifier[1]
|
|
for device in dr.async_entries_for_config_entry(
|
|
dr.async_get(hass), entry.entry_id
|
|
)
|
|
if device.disabled_by
|
|
for identifier in device.identifiers
|
|
if identifier[0] == DOMAIN
|
|
]
|
|
|
|
|
|
@callback
|
|
def async_register_parent_devices(
|
|
hass: HomeAssistant,
|
|
entry: NetatmoConfigEntry,
|
|
account: pyatmo.AsyncAccount,
|
|
module_parents: dict[str, str],
|
|
) -> dict[str, str]:
|
|
"""Register a device per home and per bridging module.
|
|
|
|
Maps Netatmo ids to device registry ids.
|
|
"""
|
|
device_registry = dr.async_get(hass)
|
|
parent_device_ids: dict[str, str] = {}
|
|
|
|
for home_id, home_name in account.all_home_names.items():
|
|
device_entry = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
identifiers={(DOMAIN, home_id)},
|
|
manufacturer=MANUFACTURER,
|
|
model="Home",
|
|
name=home_name,
|
|
configuration_url=CONF_URL_CONTROL,
|
|
)
|
|
parent_device_ids[home_id] = device_entry.id
|
|
|
|
for home in account.homes.values():
|
|
bridges = {
|
|
module_parents[module_id]
|
|
for module_id in home.modules
|
|
if module_id in module_parents
|
|
}
|
|
for module_id in home.modules:
|
|
if module_id in bridges:
|
|
_register_bridge(
|
|
device_registry,
|
|
entry,
|
|
home,
|
|
module_id,
|
|
module_parents,
|
|
parent_device_ids,
|
|
set(),
|
|
)
|
|
|
|
return parent_device_ids
|
|
|
|
|
|
@callback
|
|
def async_sync_home_disabled_state(
|
|
hass: HomeAssistant, entry: NetatmoConfigEntry, home_device_ids: Iterable[str]
|
|
) -> None:
|
|
"""Mirror each home device's disabled state onto its descendants.
|
|
|
|
Devices disabled by the user are left alone in both directions, so toggling a
|
|
home never undoes a manual choice.
|
|
"""
|
|
device_registry = dr.async_get(hass)
|
|
|
|
children: dict[str, list[dr.DeviceEntry]] = {}
|
|
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
|
|
if device.via_device_id:
|
|
children.setdefault(device.via_device_id, []).append(device)
|
|
|
|
for home_device_id in home_device_ids:
|
|
home_device = device_registry.async_get(home_device_id)
|
|
assert home_device
|
|
|
|
disabled = home_device.disabled_by is not None
|
|
# Walk the whole subtree; a module can be a grandchild via its gateway
|
|
stack = list(children.get(home_device_id, []))
|
|
while stack:
|
|
device = stack.pop()
|
|
stack.extend(children.get(device.id, []))
|
|
|
|
if disabled and device.disabled_by is None:
|
|
device_registry.async_update_device(
|
|
device.id, disabled_by=dr.DeviceEntryDisabler.INTEGRATION
|
|
)
|
|
elif (
|
|
not disabled
|
|
and device.disabled_by is dr.DeviceEntryDisabler.INTEGRATION
|
|
):
|
|
device_registry.async_update_device(device.id, disabled_by=None)
|