|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
from homeassistant.components.sensor import SensorEntity
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
+from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
|
+from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
|
|
@@ -17,17 +19,73 @@ async def async_setup_entry(
|
|
|
) -> None:
|
|
|
coordinator: ZLockCoordinator = hass.data[DOMAIN][entry.entry_id]
|
|
|
entities = []
|
|
|
+ lock_device_ids: set[str] = set()
|
|
|
+
|
|
|
for lock_entity_id in entry.data[CONF_LOCK_ENTITY_IDS]:
|
|
|
- entities.append(ZLockLastActionSensor(coordinator, entry, lock_entity_id))
|
|
|
- entities.append(ZLockLastUserSensor(coordinator, entry, lock_entity_id))
|
|
|
- entities.append(ZLockSlotsSensor(coordinator, entry, lock_entity_id))
|
|
|
- for group in entry.options.get(CONF_GROUPS, []):
|
|
|
- group_name = group[CONF_GROUP_NAME]
|
|
|
+ device = _lock_device(hass, lock_entity_id)
|
|
|
+ # Identifiers-only DeviceInfo is a "link": our sensors join the lock's
|
|
|
+ # existing zwave_js device instead of creating a parallel one.
|
|
|
+ device_info = DeviceInfo(identifiers=device.identifiers) if device else None
|
|
|
+ if device:
|
|
|
+ lock_device_ids.add(device.id)
|
|
|
+ entities.append(ZLockLastActionSensor(coordinator, entry, lock_entity_id, device_info))
|
|
|
+ entities.append(ZLockLastUserSensor(coordinator, entry, lock_entity_id, device_info))
|
|
|
+ entities.append(ZLockSlotsSensor(coordinator, entry, lock_entity_id, device_info))
|
|
|
+
|
|
|
+ group_names = [g[CONF_GROUP_NAME] for g in entry.options.get(CONF_GROUPS, [])]
|
|
|
+ for group_name in group_names:
|
|
|
entities.append(ZLockGroupLastActionSensor(coordinator, entry, group_name))
|
|
|
entities.append(ZLockGroupLastUserSensor(coordinator, entry, group_name))
|
|
|
+
|
|
|
+ _async_cleanup_stale_registry_entries(hass, entry, entities, lock_device_ids, group_names)
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
|
|
|
+def _lock_device(hass: HomeAssistant, lock_entity_id: str) -> dr.DeviceEntry | None:
|
|
|
+ """Find the zwave_js device a lock entity belongs to."""
|
|
|
+ reg_entry = er.async_get(hass).async_get(lock_entity_id)
|
|
|
+ if not reg_entry or not reg_entry.device_id:
|
|
|
+ return None
|
|
|
+ device = dr.async_get(hass).async_get(reg_entry.device_id)
|
|
|
+ if device and device.identifiers:
|
|
|
+ return device
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _group_device_info(group_name: str) -> DeviceInfo:
|
|
|
+ return DeviceInfo(
|
|
|
+ identifiers={(DOMAIN, f"group_{group_name}")},
|
|
|
+ name=group_name,
|
|
|
+ entry_type=DeviceEntryType.SERVICE,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _async_cleanup_stale_registry_entries(
|
|
|
+ hass: HomeAssistant,
|
|
|
+ entry: ConfigEntry,
|
|
|
+ entities: list[SensorEntity],
|
|
|
+ lock_device_ids: set[str],
|
|
|
+ group_names: list[str],
|
|
|
+) -> None:
|
|
|
+ """Drop registry entities and device links left behind by removed locks/groups."""
|
|
|
+ valid_unique_ids = {e.unique_id for e in entities}
|
|
|
+ ent_reg = er.async_get(hass)
|
|
|
+ for reg_entry in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
|
|
|
+ if reg_entry.unique_id not in valid_unique_ids:
|
|
|
+ ent_reg.async_remove(reg_entry.entity_id)
|
|
|
+
|
|
|
+ dev_reg = dr.async_get(hass)
|
|
|
+ group_identifiers = {(DOMAIN, f"group_{name}") for name in group_names}
|
|
|
+ for device in dr.async_entries_for_config_entry(dev_reg, entry.entry_id):
|
|
|
+ own_identifiers = {i for i in device.identifiers if i[0] == DOMAIN}
|
|
|
+ keep = (
|
|
|
+ own_identifiers & group_identifiers
|
|
|
+ or (not own_identifiers and device.id in lock_device_ids)
|
|
|
+ )
|
|
|
+ if not keep:
|
|
|
+ dev_reg.async_update_device(device.id, remove_config_entry_id=entry.entry_id)
|
|
|
+
|
|
|
+
|
|
|
def _lock_object_id(lock_entity_id: str) -> str:
|
|
|
return lock_entity_id.split(".")[-1]
|
|
|
|
|
|
@@ -40,10 +98,20 @@ class _ZLockBaseSensor(CoordinatorEntity[ZLockCoordinator], SensorEntity):
|
|
|
coordinator: ZLockCoordinator,
|
|
|
entry: ConfigEntry,
|
|
|
lock_entity_id: str,
|
|
|
+ device_info: DeviceInfo | None = None,
|
|
|
) -> None:
|
|
|
super().__init__(coordinator)
|
|
|
self._lock_entity_id = lock_entity_id
|
|
|
self._entry = entry
|
|
|
+ if device_info:
|
|
|
+ self._attr_device_info = device_info
|
|
|
+
|
|
|
+ def _name(self, short: str) -> str:
|
|
|
+ # Attached to the lock's device, HA prefixes the device name; without a
|
|
|
+ # device the name must identify the lock on its own.
|
|
|
+ if self.device_info:
|
|
|
+ return short
|
|
|
+ return f"{_lock_object_id(self._lock_entity_id)} {short.lower()}"
|
|
|
|
|
|
@property
|
|
|
def _lock_state(self):
|
|
|
@@ -52,13 +120,13 @@ class _ZLockBaseSensor(CoordinatorEntity[ZLockCoordinator], SensorEntity):
|
|
|
|
|
|
class ZLockLastActionSensor(_ZLockBaseSensor):
|
|
|
|
|
|
- def __init__(self, coordinator, entry, lock_entity_id):
|
|
|
- super().__init__(coordinator, entry, lock_entity_id)
|
|
|
+ def __init__(self, coordinator, entry, lock_entity_id, device_info=None):
|
|
|
+ super().__init__(coordinator, entry, lock_entity_id, device_info)
|
|
|
self._attr_unique_id = f"{entry.entry_id}_{lock_entity_id}_last_action"
|
|
|
|
|
|
@property
|
|
|
def name(self) -> str:
|
|
|
- return f"{_lock_object_id(self._lock_entity_id)} last action"
|
|
|
+ return self._name("Last action")
|
|
|
|
|
|
@property
|
|
|
def native_value(self) -> str | None:
|
|
|
@@ -68,13 +136,13 @@ class ZLockLastActionSensor(_ZLockBaseSensor):
|
|
|
|
|
|
class ZLockLastUserSensor(_ZLockBaseSensor):
|
|
|
|
|
|
- def __init__(self, coordinator, entry, lock_entity_id):
|
|
|
- super().__init__(coordinator, entry, lock_entity_id)
|
|
|
+ def __init__(self, coordinator, entry, lock_entity_id, device_info=None):
|
|
|
+ super().__init__(coordinator, entry, lock_entity_id, device_info)
|
|
|
self._attr_unique_id = f"{entry.entry_id}_{lock_entity_id}_last_user"
|
|
|
|
|
|
@property
|
|
|
def name(self) -> str:
|
|
|
- return f"{_lock_object_id(self._lock_entity_id)} last user"
|
|
|
+ return self._name("Last user")
|
|
|
|
|
|
@property
|
|
|
def native_value(self) -> str | None:
|
|
|
@@ -84,13 +152,13 @@ class ZLockLastUserSensor(_ZLockBaseSensor):
|
|
|
|
|
|
class ZLockSlotsSensor(_ZLockBaseSensor):
|
|
|
|
|
|
- def __init__(self, coordinator, entry, lock_entity_id):
|
|
|
- super().__init__(coordinator, entry, lock_entity_id)
|
|
|
+ def __init__(self, coordinator, entry, lock_entity_id, device_info=None):
|
|
|
+ super().__init__(coordinator, entry, lock_entity_id, device_info)
|
|
|
self._attr_unique_id = f"{entry.entry_id}_{lock_entity_id}_slots"
|
|
|
|
|
|
@property
|
|
|
def name(self) -> str:
|
|
|
- return f"{_lock_object_id(self._lock_entity_id)} slots"
|
|
|
+ return self._name("Slots")
|
|
|
|
|
|
@property
|
|
|
def native_value(self) -> int:
|
|
|
@@ -126,6 +194,9 @@ class _ZLockGroupBaseSensor(CoordinatorEntity[ZLockCoordinator], SensorEntity):
|
|
|
super().__init__(coordinator)
|
|
|
self._group_name = group_name
|
|
|
self._entry = entry
|
|
|
+ # Groups have no physical device; give each a small virtual one so its
|
|
|
+ # sensors have a home in the UI. The device name supplies the group name.
|
|
|
+ self._attr_device_info = _group_device_info(group_name)
|
|
|
|
|
|
@property
|
|
|
def _group_state(self):
|
|
|
@@ -133,15 +204,12 @@ class _ZLockGroupBaseSensor(CoordinatorEntity[ZLockCoordinator], SensorEntity):
|
|
|
|
|
|
|
|
|
class ZLockGroupLastActionSensor(_ZLockGroupBaseSensor):
|
|
|
+ _attr_name = "Last action"
|
|
|
|
|
|
def __init__(self, coordinator, entry, group_name):
|
|
|
super().__init__(coordinator, entry, group_name)
|
|
|
self._attr_unique_id = f"{entry.entry_id}_group_{group_name}_last_action"
|
|
|
|
|
|
- @property
|
|
|
- def name(self) -> str:
|
|
|
- return f"{self._group_name} last action"
|
|
|
-
|
|
|
@property
|
|
|
def native_value(self) -> str | None:
|
|
|
state = self._group_state
|
|
|
@@ -149,15 +217,12 @@ class ZLockGroupLastActionSensor(_ZLockGroupBaseSensor):
|
|
|
|
|
|
|
|
|
class ZLockGroupLastUserSensor(_ZLockGroupBaseSensor):
|
|
|
+ _attr_name = "Last user"
|
|
|
|
|
|
def __init__(self, coordinator, entry, group_name):
|
|
|
super().__init__(coordinator, entry, group_name)
|
|
|
self._attr_unique_id = f"{entry.entry_id}_group_{group_name}_last_user"
|
|
|
|
|
|
- @property
|
|
|
- def name(self) -> str:
|
|
|
- return f"{self._group_name} last user"
|
|
|
-
|
|
|
@property
|
|
|
def native_value(self) -> str | None:
|
|
|
state = self._group_state
|