Skip to content

Commit

Permalink
Add property to link entity to coordinator data
Browse files Browse the repository at this point in the history
  • Loading branch information
Snuffy2 committed Nov 13, 2024
1 parent ecf33e6 commit 65c9cc6
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 224 deletions.
2 changes: 1 addition & 1 deletion custom_components/keymaster/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""keymaster Integration."""

import asyncio
import logging
from collections.abc import Mapping
import logging

from homeassistant.components.persistent_notification import async_create, async_dismiss
from homeassistant.config_entries import ConfigEntry
Expand Down
52 changes: 21 additions & 31 deletions custom_components/keymaster/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
"""Sensor for keymaster."""

from dataclasses import dataclass
import logging

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady

from .const import CONF_LOCK_NAME, COORDINATOR, DOMAIN
from .coordinator import KeymasterCoordinator
from .entity import KeymasterEntity
from .entity import KeymasterEntity, KeymasterEntityDescription
from .helpers import async_using_zwave_js

try:
Expand All @@ -22,12 +21,6 @@
pass

_LOGGER = logging.getLogger(__name__)
ENTITY_NAME = "Network"


def generate_binary_sensor_name(lock_name: str) -> str:
"""Generate unique ID for network ready sensor."""
return f"{lock_name}: {ENTITY_NAME}"


async def async_setup_entry(hass, config_entry, async_add_entities):
Expand All @@ -36,9 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
lock = await coordinator.get_lock_by_name(config_entry.data.get(CONF_LOCK_NAME))
if async_using_zwave_js(hass=hass, lock=lock):
entity = ZwaveJSNetworkReadySensor(
hass=hass,
config_entry=config_entry,
coordinator=coordinator,
entity_description=KeymasterBinarySensorEntityDescription(
key="binary_sensor.connected",
name="Network",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_registry_enabled_default=True,
hass=hass,
config_entry=config_entry,
coordinator=coordinator,
),
)
else:
_LOGGER.error("Z-Wave integration not found")
Expand All @@ -48,28 +47,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
return True


@dataclass(kw_only=True)
class KeymasterBinarySensorEntityDescription(
KeymasterEntityDescription, BinarySensorEntityDescription
):
pass


class BaseNetworkReadySensor(KeymasterEntity, BinarySensorEntity):
"""Base binary sensor to indicate whether or not Z-Wave network is ready."""

def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
coordinator: KeymasterCoordinator,
entity_description: BinarySensorEntityDescription,
entity_description: KeymasterBinarySensorEntityDescription,
integration_name: str,
) -> None:
"""Initialize binary sensor."""
super().__init__(
hass=hass,
config_entry=config_entry,
coordinator=coordinator,
entity_description=entity_description,
)
self.integration_name = integration_name
self._attr_is_on = False
self._attr_name = "Network"
self._attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
self._attr_should_poll = False

@callback
Expand Down Expand Up @@ -103,23 +101,15 @@ class ZwaveJSNetworkReadySensor(BaseNetworkReadySensor):
"""Binary sensor to indicate whether or not `zwave_js` network is ready."""

def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
coordinator: KeymasterCoordinator,
entity_description: BinarySensorEntityDescription
self, entity_description: KeymasterBinarySensorEntityDescription
) -> None:
"""Initialize sensor."""
super().__init__(
hass=hass,
config_entry=config_entry,
coordinator=coordinator,
integration_name=ZWAVE_JS_DOMAIN,
entity_description=entity_description,
)
self.lock_config_entry_id = None
self._lock_found = True
# self.ent_reg = None
self._attr_should_poll = True

# async def async_update(self) -> None:
Expand Down
106 changes: 50 additions & 56 deletions custom_components/keymaster/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
"""keymaster Integration."""

import asyncio
import functools
import logging
from collections.abc import Mapping
from datetime import timedelta
import functools
import logging
from typing import Any

from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, STATE_ON
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import slugify
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import ATTR_CODE_SLOT, DOMAIN
from .exceptions import (
NoNodeSpecifiedError,
ZWaveIntegrationNotConfiguredError,
ZWaveNetworkNotReady,
)
from .exceptions import (
NotFoundError as NativeNotFoundError,
)
from .exceptions import (
NotSupportedError as NativeNotSupportedError,
)
from .helpers import (
async_using_zwave_js,
handle_zwave_js_event,
Expand All @@ -34,14 +20,15 @@
from .lock import KeymasterLock

try:
from homeassistant.components.zwave_js import ZWAVE_JS_NOTIFICATION_EVENT
from zwave_js_server.const.command_class.lock import (
ATTR_IN_USE,
ATTR_NAME,
ATTR_USERCODE,
)
from zwave_js_server.model.node import Node as ZwaveJSNode
from zwave_js_server.util.lock import get_usercode_from_node, get_usercodes

from homeassistant.components.zwave_js import ZWAVE_JS_NOTIFICATION_EVENT
except (ModuleNotFoundError, ImportError):
pass

Expand Down Expand Up @@ -299,52 +286,58 @@ async def get_lock_by_device_id(
def sync_get_lock_by_device_id(
self, keymaster_device_id: str
) -> KeymasterLock | None:
return asyncio.run_coroutine_threadsafe(
self.get_lock_by_device_id(keymaster_device_id),
self.hass.loop,
).result()
_LOGGER.debug(
f"[sync_get_lock_by_device_id] keymaster_device_id: {keymaster_device_id} ({type(keymaster_device_id)})"
)
if keymaster_device_id not in self.locks:
return None
return self.locks[keymaster_device_id]

async def _check_lock_connection(self, lock) -> bool:
# TODO: redo this to use lock.connected
self.network_sensor = self._entity_registry.async_get_entity_id(
"binary_sensor",
DOMAIN,
slugify(generate_binary_sensor_name(lock.lock_name)),
)
if self.network_sensor is None:
return False
try:
network_ready = self.hass.states.get(self.network_sensor)
if not network_ready:
# We may need to get a new entity ID
self.network_sensor = None
raise ZWaveNetworkNotReady

if network_ready.state != STATE_ON:
raise ZWaveNetworkNotReady

return True
except (
NativeNotFoundError,
NativeNotSupportedError,
NoNodeSpecifiedError,
ZWaveIntegrationNotConfiguredError,
ZWaveNetworkNotReady,
):
return False

async def _async_setup(self):
pass
return lock.connected

# self.network_sensor = self._entity_registry.async_get_entity_id(
# "binary_sensor",
# DOMAIN,
# slugify(generate_binary_sensor_name(lock.lock_name)),
# )
# if self.network_sensor is None:
# return False
# try:
# network_ready = self.hass.states.get(self.network_sensor)
# if not network_ready:
# # We may need to get a new entity ID
# self.network_sensor = None
# raise ZWaveNetworkNotReady

# if network_ready.state != STATE_ON:
# raise ZWaveNetworkNotReady

# return True
# except (
# NativeNotFoundError,
# NativeNotSupportedError,
# NoNodeSpecifiedError,
# ZWaveIntegrationNotConfiguredError,
# ZWaveNetworkNotReady,
# ):
# return False

async def _async_update_data(self) -> Mapping[str, Any]:
_LOGGER.debug(f"[Coordinator] self.locks: {self.locks}")
for lock in self.locks.values():
if not await self._check_lock_connection(lock):
raise UpdateFailed()
_LOGGER.error(f"[Coordinator] {lock.lock_name} not Connected")
continue

if async_using_zwave_js(hass=self.hass, lock=lock):
node: ZwaveJSNode = lock.zwave_js_lock_node
if node is None:
raise NativeNotFoundError
_LOGGER.debug(
f"[Coordinator] {lock.lock_name} Z-Wave Node not defined"
)
continue

for slot in get_usercodes(node):
code_slot = int(slot[ATTR_CODE_SLOT])
Expand Down Expand Up @@ -375,6 +368,7 @@ async def _async_update_data(self) -> Mapping[str, Any]:
# TODO: What if there are child locks?

else:
raise ZWaveIntegrationNotConfiguredError
_LOGGER.error(f"[Coordinator] {lock.lock_name} not using Z-Wave")
continue

return self.locks
52 changes: 35 additions & 17 deletions custom_components/keymaster/entity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry
Expand All @@ -11,37 +11,55 @@

from .const import DOMAIN
from .coordinator import KeymasterCoordinator
from .lock import KeymasterLock

_LOGGER = logging.getLogger(__name__)

# Naming convention for EntityDescription key (Property) for all entities: <Platform>.<Property>.<SubProperty>.<SubProperty>:<Slot Number>
# Not all items will exist for a property
# Items cannot contain . or : in their names


class KeymasterEntity(CoordinatorEntity[KeymasterCoordinator]):
"""Base entity for Keymaster"""

def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
coordinator: KeymasterCoordinator,
entity_description: EntityDescription
) -> None:
self.hass: HomeAssistant = hass
self.coordinator: KeymasterCoordinator = coordinator
self._entity_description = entity_description
self._property = entity_description.key
self._keymaster_device_id = hass.data[DOMAIN][config_entry.entry_id]
self._attr_unique_id = f"{self._keymaster_device_id}_{slugify(self._property)}"
_attr_available = True

def __init__(self, entity_description: EntityDescription) -> None:
self.hass: HomeAssistant = entity_description.hass
self.coordinator: KeymasterCoordinator = entity_description.coordinator
self._config_entry = entity_description.config_entry
self.entity_description: EntityDescription = entity_description
self._property: str = entity_description.key
self._keymaster_device_id: str = self.hass.data[DOMAIN][
self._config_entry.entry_id
]
lock: KeymasterLock = self.coordinator.sync_get_lock_by_device_id(
self._keymaster_device_id
)
self._attr_name: str = f"{lock.lock_name} {self.entity_description.name}"
_LOGGER.debug(
f"[Entity init] entity_description.name: {self.entity_description.name}, name: {self.name}"
)
self._attr_unique_id: str = (
f"{self._keymaster_device_id}_{slugify(self._property)}"
)
_LOGGER.debug(
f"[Entity init] self._property: {self._property}, unique_id: {self.unique_id}"
)
self._attr_extra_state_attributes: Mapping[str, Any] = {}
self._attr_device_info: Mapping[str, Any] = {
"identifiers": {(DOMAIN, config_entry.entry_id)},
"identifiers": {(DOMAIN, entity_description.config_entry.entry_id)},
}
super().__init__(self.coordinator, self._attr_unique_id)

@property
def available(self) -> bool:
return self._attr_available


@dataclass
@dataclass(kw_only=True)
class KeymasterEntityDescription(EntityDescription):
hass: HomeAssistant
config_entry: ConfigEntry
coordinator: KeymasterCoordinator
keymaster_device_id: str
Loading

0 comments on commit 65c9cc6

Please sign in to comment.