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:
epenet
2021-10-22 11:45:40 +02:00
committed by GitHub
co-authored by epenet
parent 766a693514
commit c00a5fad8f
11 changed files with 317 additions and 253 deletions
+14 -22
View File
@@ -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(
+23 -3
View File
@@ -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
+94 -18
View File
@@ -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."""
+18 -23
View File
@@ -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}"
+9 -22
View File
@@ -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(