mirror of
https://github.com/elisspace/core.git
synced 2026-10-05 16:56:19 +00:00
Cleanup device registration in Onewire (#58101)
* Add checks for device registry * Move registry checks to init.py * Run device registry check on disabled devices * Empty commit for testing * Register devices during initialisation * Adjust tests accordingly * Add via_device to device info * Adjust access to device registry Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .onewirehub import CannotConnect, OneWireHub
|
||||
@@ -25,31 +25,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = onewirehub
|
||||
|
||||
async def cleanup_registry() -> None:
|
||||
async def cleanup_registry(onewirehub: OneWireHub) -> None:
|
||||
# Get registries
|
||||
device_registry, entity_registry = await asyncio.gather(
|
||||
hass.helpers.device_registry.async_get_registry(),
|
||||
hass.helpers.entity_registry.async_get_registry(),
|
||||
)
|
||||
device_registry = dr.async_get(hass)
|
||||
# Generate list of all device entries
|
||||
registry_devices = [
|
||||
entry.id
|
||||
for entry in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
)
|
||||
]
|
||||
registry_devices = list(
|
||||
dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
)
|
||||
# Remove devices that don't belong to any entity
|
||||
for device_id in registry_devices:
|
||||
if not er.async_entries_for_device(
|
||||
entity_registry, device_id, include_disabled_entities=True
|
||||
):
|
||||
for device in registry_devices:
|
||||
if not onewirehub.has_device_in_cache(device):
|
||||
_LOGGER.debug(
|
||||
"Removing device `%s` because it does not have any entities",
|
||||
device_id,
|
||||
"Removing device `%s` because it is no longer available",
|
||||
device.id,
|
||||
)
|
||||
device_registry.async_remove_device(device_id)
|
||||
device_registry.async_remove_device(device.id)
|
||||
|
||||
async def start_platforms() -> None:
|
||||
async def start_platforms(onewirehub: OneWireHub) -> None:
|
||||
"""Start platforms and cleanup devices."""
|
||||
# wait until all required platforms are ready
|
||||
await asyncio.gather(
|
||||
@@ -58,9 +50,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
for platform in PLATFORMS
|
||||
)
|
||||
)
|
||||
await cleanup_registry()
|
||||
await cleanup_registry(onewirehub)
|
||||
|
||||
hass.async_create_task(start_platforms())
|
||||
hass.async_create_task(start_platforms(onewirehub))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -3,21 +3,16 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.onewire.model import OWServerDeviceDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_NAME,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.const import CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
@@ -89,24 +84,17 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]:
|
||||
return []
|
||||
|
||||
entities: list[BinarySensorEntity] = []
|
||||
|
||||
for device in onewirehub.devices:
|
||||
family = device["family"]
|
||||
device_type = device["type"]
|
||||
device_id = os.path.split(os.path.split(device["path"])[0])[1]
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(device, OWServerDeviceDescription)
|
||||
family = device.family
|
||||
device_id = device.id
|
||||
device_info = device.device_info
|
||||
|
||||
if family not in DEVICE_BINARY_SENSORS:
|
||||
continue
|
||||
device_info: DeviceInfo = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
|
||||
ATTR_MANUFACTURER: "Maxim Integrated",
|
||||
ATTR_MODEL: device_type,
|
||||
ATTR_NAME: device_id,
|
||||
}
|
||||
for description in DEVICE_BINARY_SENSORS[family]:
|
||||
device_file = os.path.join(
|
||||
os.path.split(device["path"])[0], description.key
|
||||
)
|
||||
device_file = os.path.join(os.path.split(device.path)[0], description.key)
|
||||
name = f"{device_id} {description.name}"
|
||||
entities.append(
|
||||
OneWireProxyBinarySensor(
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
"""Type definitions for 1-Wire integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pi1wire import OneWireInterface
|
||||
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
|
||||
class OWServerDeviceDescription(TypedDict):
|
||||
@dataclass
|
||||
class OWDeviceDescription:
|
||||
"""OWDeviceDescription device description class."""
|
||||
|
||||
device_info: DeviceInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class OWDirectDeviceDescription(OWDeviceDescription):
|
||||
"""SysBus device description class."""
|
||||
|
||||
interface: OneWireInterface
|
||||
|
||||
|
||||
@dataclass
|
||||
class OWServerDeviceDescription(OWDeviceDescription):
|
||||
"""OWServer device description class."""
|
||||
|
||||
path: str
|
||||
family: str
|
||||
id: str
|
||||
path: str
|
||||
type: str
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
"""Hub for communication with 1-Wire server or mount_dir."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pi1wire import Pi1Wire
|
||||
from pyownet import protocol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_NAME,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS
|
||||
from .model import OWServerDeviceDescription
|
||||
from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DOMAIN
|
||||
from .model import (
|
||||
OWDeviceDescription,
|
||||
OWDirectDeviceDescription,
|
||||
OWServerDeviceDescription,
|
||||
)
|
||||
|
||||
DEVICE_COUPLERS = {
|
||||
# Family : [branches]
|
||||
"1F": ["aux", "main"]
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OneWireHub:
|
||||
"""Hub to communicate with SysBus or OWServer."""
|
||||
@@ -29,7 +48,7 @@ class OneWireHub:
|
||||
self.type: str | None = None
|
||||
self.pi1proxy: Pi1Wire | None = None
|
||||
self.owproxy: protocol._Proxy | None = None
|
||||
self.devices: list | None = None
|
||||
self.devices: list[OWDeviceDescription] | None = None
|
||||
|
||||
async def connect(self, host: str, port: int) -> None:
|
||||
"""Connect to the owserver host."""
|
||||
@@ -56,42 +75,99 @@ class OneWireHub:
|
||||
port = config_entry.data[CONF_PORT]
|
||||
await self.connect(host, port)
|
||||
await self.discover_devices()
|
||||
if TYPE_CHECKING:
|
||||
assert self.devices
|
||||
# Register discovered devices on Hub
|
||||
device_registry = dr.async_get(self.hass)
|
||||
for device in self.devices:
|
||||
device_info: DeviceInfo = device.device_info
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers=device_info[ATTR_IDENTIFIERS],
|
||||
manufacturer=device_info[ATTR_MANUFACTURER],
|
||||
model=device_info[ATTR_MODEL],
|
||||
name=device_info[ATTR_NAME],
|
||||
via_device=device_info.get("via_device"),
|
||||
)
|
||||
|
||||
async def discover_devices(self) -> None:
|
||||
"""Discover all devices."""
|
||||
if self.devices is None:
|
||||
if self.type == CONF_TYPE_SYSBUS:
|
||||
assert self.pi1proxy
|
||||
self.devices = await self.hass.async_add_executor_job(
|
||||
self.pi1proxy.find_all_sensors
|
||||
self._discover_devices_sysbus
|
||||
)
|
||||
if self.type == CONF_TYPE_OWSERVER:
|
||||
self.devices = await self.hass.async_add_executor_job(
|
||||
self._discover_devices_owserver
|
||||
)
|
||||
|
||||
def _discover_devices_sysbus(self) -> list[OWDeviceDescription]:
|
||||
"""Discover all sysbus devices."""
|
||||
devices: list[OWDeviceDescription] = []
|
||||
assert self.pi1proxy
|
||||
for interface in self.pi1proxy.find_all_sensors():
|
||||
family = interface.mac_address[:2]
|
||||
device_id = f"{family}-{interface.mac_address[2:]}"
|
||||
device_info: DeviceInfo = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
|
||||
ATTR_MANUFACTURER: "Maxim Integrated",
|
||||
ATTR_MODEL: family,
|
||||
ATTR_NAME: device_id,
|
||||
}
|
||||
device = OWDirectDeviceDescription(
|
||||
device_info=device_info,
|
||||
interface=interface,
|
||||
)
|
||||
devices.append(device)
|
||||
return devices
|
||||
|
||||
def _discover_devices_owserver(
|
||||
self, path: str = "/"
|
||||
) -> list[OWServerDeviceDescription]:
|
||||
self, path: str = "/", parent_id: str | None = None
|
||||
) -> list[OWDeviceDescription]:
|
||||
"""Discover all owserver devices."""
|
||||
devices = []
|
||||
devices: list[OWDeviceDescription] = []
|
||||
assert self.owproxy
|
||||
for device_path in self.owproxy.dir(path):
|
||||
device_id = os.path.split(os.path.split(device_path)[0])[1]
|
||||
device_family = self.owproxy.read(f"{device_path}family").decode()
|
||||
_LOGGER.debug("read `%sfamily`: %s", device_path, device_family)
|
||||
device_type = self.owproxy.read(f"{device_path}type").decode()
|
||||
_LOGGER.debug("read `%stype`: %s", device_path, device_type)
|
||||
device_info: DeviceInfo = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
|
||||
ATTR_MANUFACTURER: "Maxim Integrated",
|
||||
ATTR_MODEL: device_type,
|
||||
ATTR_NAME: device_id,
|
||||
}
|
||||
if parent_id:
|
||||
device_info["via_device"] = (DOMAIN, parent_id)
|
||||
device = OWServerDeviceDescription(
|
||||
device_info=device_info,
|
||||
id=device_id,
|
||||
family=device_family,
|
||||
path=device_path,
|
||||
type=device_type,
|
||||
)
|
||||
devices.append(device)
|
||||
if device_branches := DEVICE_COUPLERS.get(device_family):
|
||||
for branch in device_branches:
|
||||
devices += self._discover_devices_owserver(f"{device_path}{branch}")
|
||||
else:
|
||||
devices.append(
|
||||
{
|
||||
"path": device_path,
|
||||
"family": device_family,
|
||||
"type": device_type,
|
||||
}
|
||||
)
|
||||
devices += self._discover_devices_owserver(
|
||||
f"{device_path}{branch}", device_id
|
||||
)
|
||||
|
||||
return devices
|
||||
|
||||
def has_device_in_cache(self, device: DeviceEntry) -> bool:
|
||||
"""Check if device was present in the cache."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.devices
|
||||
for internal_device in self.devices:
|
||||
for identifier in internal_device.device_info[ATTR_IDENTIFIERS]:
|
||||
if identifier in device.identifiers:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
@@ -7,10 +7,14 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
import os
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException
|
||||
|
||||
from homeassistant.components.onewire.model import (
|
||||
OWDirectDeviceDescription,
|
||||
OWServerDeviceDescription,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
@@ -19,10 +23,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_NAME,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
@@ -382,11 +382,14 @@ def get_entities(
|
||||
if conf_type == CONF_TYPE_OWSERVER:
|
||||
assert onewirehub.owproxy
|
||||
for device in onewirehub.devices:
|
||||
family = device["family"]
|
||||
device_type = device["type"]
|
||||
device_id = os.path.split(os.path.split(device["path"])[0])[1]
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(device, OWServerDeviceDescription)
|
||||
family = device.family
|
||||
device_type = device.type
|
||||
device_id = device.id
|
||||
device_info = device.device_info
|
||||
device_sub_type = "std"
|
||||
device_path = device["path"]
|
||||
device_path = device.path
|
||||
if "EF" in family:
|
||||
device_sub_type = "HobbyBoard"
|
||||
family = device_type
|
||||
@@ -401,12 +404,6 @@ def get_entities(
|
||||
device_id,
|
||||
)
|
||||
continue
|
||||
device_info: DeviceInfo = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
|
||||
ATTR_MANUFACTURER: "Maxim Integrated",
|
||||
ATTR_MODEL: device_type,
|
||||
ATTR_NAME: device_id,
|
||||
}
|
||||
for description in get_sensor_types(device_sub_type)[family]:
|
||||
if description.key.startswith("moisture/"):
|
||||
s_id = description.key.split(".")[1]
|
||||
@@ -421,7 +418,7 @@ def get_entities(
|
||||
description.native_unit_of_measurement = PERCENTAGE
|
||||
description.name = f"Wetness {s_id}"
|
||||
device_file = os.path.join(
|
||||
os.path.split(device["path"])[0], description.key
|
||||
os.path.split(device.path)[0], description.key
|
||||
)
|
||||
name = f"{device_names.get(device_id, device_id)} {description.name}"
|
||||
entities.append(
|
||||
@@ -439,9 +436,13 @@ def get_entities(
|
||||
elif conf_type == CONF_TYPE_SYSBUS:
|
||||
base_dir = config[CONF_MOUNT_DIR]
|
||||
_LOGGER.debug("Initializing using SysBus %s", base_dir)
|
||||
for p1sensor in onewirehub.devices:
|
||||
for device in onewirehub.devices:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(device, OWDirectDeviceDescription)
|
||||
p1sensor: OneWireInterface = device.interface
|
||||
family = p1sensor.mac_address[:2]
|
||||
device_id = f"{family}-{p1sensor.mac_address[2:]}"
|
||||
device_info = device.device_info
|
||||
if family not in DEVICE_SUPPORT_SYSBUS:
|
||||
_LOGGER.warning(
|
||||
"Ignoring unknown family (%s) of sensor found for device: %s",
|
||||
@@ -450,12 +451,6 @@ def get_entities(
|
||||
)
|
||||
continue
|
||||
|
||||
device_info = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
|
||||
ATTR_MANUFACTURER: "Maxim Integrated",
|
||||
ATTR_MODEL: family,
|
||||
ATTR_NAME: device_id,
|
||||
}
|
||||
description = SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION
|
||||
device_file = f"/sys/bus/w1/devices/{device_id}/w1_slave"
|
||||
name = f"{device_names.get(device_id, device_id)} {description.name}"
|
||||
|
||||
@@ -4,19 +4,13 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.onewire.model import OWServerDeviceDescription
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_NAME,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.const import CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
@@ -120,23 +114,16 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]:
|
||||
entities: list[SwitchEntity] = []
|
||||
|
||||
for device in onewirehub.devices:
|
||||
family = device["family"]
|
||||
device_type = device["type"]
|
||||
device_id = os.path.split(os.path.split(device["path"])[0])[1]
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(device, OWServerDeviceDescription)
|
||||
family = device.family
|
||||
device_id = device.id
|
||||
device_info = device.device_info
|
||||
|
||||
if family not in DEVICE_SWITCHES:
|
||||
continue
|
||||
|
||||
device_info: DeviceInfo = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
|
||||
ATTR_MANUFACTURER: "Maxim Integrated",
|
||||
ATTR_MODEL: device_type,
|
||||
ATTR_NAME: device_id,
|
||||
}
|
||||
for description in DEVICE_SWITCHES[family]:
|
||||
device_file = os.path.join(
|
||||
os.path.split(device["path"])[0], description.key
|
||||
)
|
||||
device_file = os.path.join(os.path.split(device.path)[0], description.key)
|
||||
name = f"{device_id} {description.name}"
|
||||
entities.append(
|
||||
OneWireProxySwitch(
|
||||
|
||||
Reference in New Issue
Block a user