|
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
|
from typing import TYPE_CHECKING
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
from homeassistant.core import HomeAssistant, Event, callback
|
|
from homeassistant.core import HomeAssistant, Event, callback
|
|
|
-from homeassistant.helpers import entity_registry as er
|
|
|
|
|
|
|
+from homeassistant.helpers.event import async_track_state_change_event
|
|
|
from homeassistant.helpers.storage import Store
|
|
from homeassistant.helpers.storage import Store
|
|
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
|
|
|
|
|
@@ -14,9 +14,8 @@ from .const import (
|
|
|
STORAGE_KEY,
|
|
STORAGE_KEY,
|
|
|
STORAGE_VERSION,
|
|
STORAGE_VERSION,
|
|
|
CONF_LOCK_ENTITY_IDS,
|
|
CONF_LOCK_ENTITY_IDS,
|
|
|
- ZWAVE_JS_NOTIFICATION_EVENT,
|
|
|
|
|
- LOCK_OP_TYPE,
|
|
|
|
|
- EVENT_MAP,
|
|
|
|
|
|
|
+ ALARM_MAP,
|
|
|
|
|
+ UserSource,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
if TYPE_CHECKING:
|
|
@@ -38,6 +37,15 @@ class LockState:
|
|
|
last_user: str | None = None
|
|
last_user: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _alarm_entity(lock_entity_id: str, suffix: str) -> str:
|
|
|
|
|
+ """Derive alarm sensor entity ID from lock entity ID.
|
|
|
|
|
+
|
|
|
|
|
+ lock.living_room_front_lock -> sensor.living_room_front_lock_alarm_<suffix>
|
|
|
|
|
+ """
|
|
|
|
|
+ object_id = lock_entity_id.split(".")[-1]
|
|
|
|
|
+ return f"sensor.{object_id}_alarm_{suffix}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
|
"""Manages slot data, name storage, and event listening for all configured locks."""
|
|
"""Manages slot data, name storage, and event listening for all configured locks."""
|
|
|
|
|
|
|
@@ -47,7 +55,10 @@ class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
|
self._lock_entity_ids: list[str] = entry.data[CONF_LOCK_ENTITY_IDS]
|
|
self._lock_entity_ids: list[str] = entry.data[CONF_LOCK_ENTITY_IDS]
|
|
|
self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
|
self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
|
|
self._names: dict[str, str] = {} # "{entity_id}:{slot_num}" → name
|
|
self._names: dict[str, str] = {} # "{entity_id}:{slot_num}" → name
|
|
|
- self._device_to_entity: dict[str, str] = {} # device_id → lock_entity_id
|
|
|
|
|
|
|
+ # alarm_type entity_id → lock_entity_id
|
|
|
|
|
+ self._alarm_to_lock: dict[str, str] = {
|
|
|
|
|
+ _alarm_entity(eid, "type"): eid for eid in self._lock_entity_ids
|
|
|
|
|
+ }
|
|
|
self._unsub_event: callback | None = None
|
|
self._unsub_event: callback | None = None
|
|
|
self.data: dict[str, LockState] = {eid: LockState() for eid in self._lock_entity_ids}
|
|
self.data: dict[str, LockState] = {eid: LockState() for eid in self._lock_entity_ids}
|
|
|
|
|
|
|
@@ -56,11 +67,12 @@ class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
|
if stored:
|
|
if stored:
|
|
|
self._names = stored.get("names", {})
|
|
self._names = stored.get("names", {})
|
|
|
|
|
|
|
|
- self._build_device_map()
|
|
|
|
|
await self._async_fetch_all_slots()
|
|
await self._async_fetch_all_slots()
|
|
|
|
|
|
|
|
- self._unsub_event = self.hass.bus.async_listen(
|
|
|
|
|
- ZWAVE_JS_NOTIFICATION_EVENT, self._handle_zwave_event
|
|
|
|
|
|
|
+ self._unsub_event = async_track_state_change_event(
|
|
|
|
|
+ self.hass,
|
|
|
|
|
+ list(self._alarm_to_lock),
|
|
|
|
|
+ self._handle_alarm_state_change,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
def async_teardown(self) -> None:
|
|
def async_teardown(self) -> None:
|
|
@@ -68,13 +80,6 @@ class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
|
self._unsub_event()
|
|
self._unsub_event()
|
|
|
self._unsub_event = None
|
|
self._unsub_event = None
|
|
|
|
|
|
|
|
- def _build_device_map(self) -> None:
|
|
|
|
|
- er_obj = er.async_get(self.hass)
|
|
|
|
|
- for lock_entity_id in self._lock_entity_ids:
|
|
|
|
|
- entry = er_obj.async_get(lock_entity_id)
|
|
|
|
|
- if entry and entry.device_id:
|
|
|
|
|
- self._device_to_entity[entry.device_id] = lock_entity_id
|
|
|
|
|
-
|
|
|
|
|
async def _async_fetch_all_slots(self) -> None:
|
|
async def _async_fetch_all_slots(self) -> None:
|
|
|
for lock_entity_id in self._lock_entity_ids:
|
|
for lock_entity_id in self._lock_entity_ids:
|
|
|
slots = await self._fetch_slots(lock_entity_id)
|
|
slots = await self._fetch_slots(lock_entity_id)
|
|
@@ -104,33 +109,40 @@ class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
|
return slots
|
|
return slots
|
|
|
|
|
|
|
|
@callback
|
|
@callback
|
|
|
- def _handle_zwave_event(self, event: Event) -> None:
|
|
|
|
|
- data = event.data
|
|
|
|
|
- if data.get("type") != LOCK_OP_TYPE:
|
|
|
|
|
|
|
+ def _handle_alarm_state_change(self, event: Event) -> None:
|
|
|
|
|
+ entity_id: str = event.data["entity_id"]
|
|
|
|
|
+ lock_entity_id = self._alarm_to_lock.get(entity_id)
|
|
|
|
|
+ if not lock_entity_id:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- device_id = data.get("device_id")
|
|
|
|
|
- lock_entity_id = self._device_to_entity.get(device_id)
|
|
|
|
|
- if not lock_entity_id:
|
|
|
|
|
|
|
+ new_state = event.data.get("new_state")
|
|
|
|
|
+ if new_state is None:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ alarm_type = int(new_state.state)
|
|
|
|
|
+ except (ValueError, TypeError):
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- event_num = data.get("event")
|
|
|
|
|
- mapping = EVENT_MAP.get(event_num)
|
|
|
|
|
|
|
+ mapping = ALARM_MAP.get(alarm_type)
|
|
|
if not mapping:
|
|
if not mapping:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- action, user_template = mapping
|
|
|
|
|
- if user_template == "keypad":
|
|
|
|
|
- params = data.get("parameters", {})
|
|
|
|
|
- user_id = params.get("userId")
|
|
|
|
|
- if user_id is not None:
|
|
|
|
|
- user_str = self.get_name(lock_entity_id, int(user_id)) or str(user_id)
|
|
|
|
|
- else:
|
|
|
|
|
- user_str = "<Keypad>"
|
|
|
|
|
|
|
+ if mapping.user_source == UserSource.SLOT:
|
|
|
|
|
+ alarm_level_state = self.hass.states.get(_alarm_entity(lock_entity_id, "level"))
|
|
|
|
|
+ try:
|
|
|
|
|
+ slot_num = int(alarm_level_state.state)
|
|
|
|
|
+ user_str = self.get_name(lock_entity_id, slot_num) or str(slot_num)
|
|
|
|
|
+ except (AttributeError, ValueError, TypeError):
|
|
|
|
|
+ user_str = "<Unknown>"
|
|
|
|
|
+ elif mapping.user_source == UserSource.MANUAL:
|
|
|
|
|
+ user_str = "<Manual>"
|
|
|
|
|
+ elif mapping.user_source == UserSource.RADIO:
|
|
|
|
|
+ user_str = "<Radio>"
|
|
|
else:
|
|
else:
|
|
|
- user_str = user_template
|
|
|
|
|
|
|
+ user_str = "<Unknown>"
|
|
|
|
|
|
|
|
- self.data[lock_entity_id].last_action = action
|
|
|
|
|
|
|
+ self.data[lock_entity_id].last_action = mapping.action
|
|
|
self.data[lock_entity_id].last_user = user_str
|
|
self.data[lock_entity_id].last_user = user_str
|
|
|
self.async_update_listeners()
|
|
self.async_update_listeners()
|
|
|
|
|
|
|
@@ -145,7 +157,6 @@ class ZLockCoordinator(DataUpdateCoordinator[dict[str, LockState]]):
|
|
|
self._names.pop(key, None)
|
|
self._names.pop(key, None)
|
|
|
await self._store.async_save({"names": self._names})
|
|
await self._store.async_save({"names": self._names})
|
|
|
|
|
|
|
|
- # Refresh the name on the cached slot if it exists
|
|
|
|
|
state = self.data.get(lock_entity_id)
|
|
state = self.data.get(lock_entity_id)
|
|
|
if state and slot_num in state.slots:
|
|
if state and slot_num in state.slots:
|
|
|
state.slots[slot_num].name = name or None
|
|
state.slots[slot_num].name = name or None
|