Skip to content

Commit

Permalink
Additional Changes (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
raman325 authored Feb 4, 2024
1 parent 52a836f commit 8dd94b3
Show file tree
Hide file tree
Showing 31 changed files with 1,049 additions and 476 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Example minimal dashboard configuration using the view configuration:
views:
- strategy:
type: custom:lock-code-manager
config_entry_name: House Locks
config_entry_title: House Locks
```
## Inspiration
Expand Down
15 changes: 7 additions & 8 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
Dev:
- Add lock list as attribute to binary sensor and add cards to UI to list locks and slots
- Add event entity to make it easy to see what happened last
- add repair issue if calendar isn’t found - solution is to pick a new calendar
- add repair issue if something else isn’t found (e.g. because someone renamed a lock_code_manager entity)
- Reevaluate logging
- Track state updates to locks in event and sensor entities to update availability
- Figure out how to handle builds and releases
- Use HACS websocket commands to check whether the dependent components are installed, and if not, install them.
- https://github.com/hacs/integration/blob/main/custom_components/hacs/websocket/repository.py#L19
- https://github.com/hacs/integration/blob/main/custom_components/hacs/websocket/repository.py#L211
- Figure out how to use HACS from integration
- Use HACS websocket commands to check whether the dependent components are installed, and if not, install them.
- https://github.com/hacs/integration/blob/main/custom_components/hacs/websocket/repository.py#L19
- https://github.com/hacs/integration/blob/main/custom_components/hacs/websocket/repository.py#L211
- Reevaluate logging
Test:
- Test enabling and disabling calendar/number of uses, adding and removing locks, etc.
- Test allowing two different config entries to use the same lock if they don't use overlapping slots
Expand All @@ -17,3 +15,4 @@ Test:
- Test strategy
Docs:
- Document how to use the strategy, including the additional custom card dependencies
- Document strategy configuration options (use_fold_entity_row and include_code_slot_sensors)
99 changes: 71 additions & 28 deletions custom_components/lock_code_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import asyncio
from collections import defaultdict
import copy
import logging
from pathlib import Path
from typing import Any
Expand All @@ -14,13 +13,19 @@
from homeassistant.components.lovelace.const import DOMAIN as LOVELACE_DOMAIN
from homeassistant.components.lovelace.resources import ResourceStorageCollection
from homeassistant.config_entries import ConfigEntry, ConfigEntryError
from homeassistant.const import ATTR_ENTITY_ID, CONF_ENABLED, CONF_NAME, CONF_PIN
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_ENABLED,
CONF_NAME,
CONF_PIN,
)
from homeassistant.core import Config, HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send

Expand All @@ -30,6 +35,9 @@
CONF_SLOTS,
COORDINATORS,
DOMAIN,
EVENT_PIN_USED,
FOLD_ENTITY_ROW_FILENAME,
HACS_DOMAIN,
PLATFORM_MAP,
PLATFORMS,
STRATEGY_FILENAME,
Expand All @@ -39,6 +47,7 @@
from .coordinator import LockUsercodeUpdateCoordinator
from .helpers import async_create_lock_instance, get_lock_from_entity_id
from .providers import BaseLock
from .websocket import async_setup as async_websocket_setup

_LOGGER = logging.getLogger(__name__)

Expand All @@ -56,13 +65,45 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool:

resources: ResourceStorageCollection
if resources := hass.data[LOVELACE_DOMAIN].get("resources"):
hass.http.register_static_path(
STRATEGY_PATH, Path(__file__).parent / "www" / STRATEGY_FILENAME
)
data = await resources.async_create_item(
{"res_type": "module", "url": STRATEGY_PATH}
)
_LOGGER.debug("Registered strategy module (resource ID %s)", data["id"])
# Load resources if needed
if not resources.loaded:
await resources.async_load()
resources.loaded = True

try:
next(
res
for res in resources.async_items()
if FOLD_ENTITY_ROW_FILENAME in res["url"]
)
except StopIteration:
_LOGGER.warning("fold-entity-row.js not found in Dashboard resources.")
ir.async_create_issue(
hass,
DOMAIN,
"fold_entity_row_js_not_found",
is_fixable=bool(HACS_DOMAIN in hass.config.components),
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
translation_key="fold_entity_row_js_not_found",
)
else:
ir.async_delete_issue(hass, DOMAIN, "fold_entity_row_js_not_found")
finally:
# Expose strategy javascript
hass.http.register_static_path(
STRATEGY_PATH, Path(__file__).parent / "www" / STRATEGY_FILENAME
)

# Register strategy module
data = await resources.async_create_item(
{"res_type": "module", "url": STRATEGY_PATH}
)
_LOGGER.debug("Registered strategy module (resource ID %s)", data["id"])

# Set up websocket API
await async_websocket_setup(hass)
_LOGGER.debug("Finished setting up websocket API")

# Hard refresh usercodes
async def _hard_refresh_usercodes(service: ServiceCall) -> None:
Expand Down Expand Up @@ -180,10 +221,10 @@ async def async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry)
ATTR_SETUP_TASKS
]

curr_slots: dict[int, Any] = copy.deepcopy(config_entry.data.get(CONF_SLOTS, {}))
new_slots: dict[int, Any] = copy.deepcopy(config_entry.options[CONF_SLOTS])
curr_locks: list[str] = copy.deepcopy(config_entry.data.get(CONF_LOCKS, []))
new_locks: list[str] = copy.deepcopy(config_entry.options[CONF_LOCKS])
curr_slots: dict[int, Any] = {**config_entry.data.get(CONF_SLOTS, {})}
new_slots: dict[int, Any] = {**config_entry.options.get(CONF_SLOTS, {})}
curr_locks: list[str] = [*config_entry.data.get(CONF_LOCKS, [])]
new_locks: list[str] = [*config_entry.options.get(CONF_LOCKS, [])]

# Set up any platforms that the new slot configs need that haven't already been
# setup
Expand Down Expand Up @@ -234,14 +275,14 @@ async def async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry)
)
async_dispatcher_send(hass, f"{DOMAIN}_{entry_id}_add_locks", locks_to_add)
for lock_entity_id in locks_to_add:
lock = hass.data[DOMAIN][entry_id][CONF_LOCKS][
lock_entity_id
] = async_create_lock_instance(
hass,
dr.async_get(hass),
er.async_get(hass),
config_entry,
lock_entity_id,
lock = hass.data[DOMAIN][entry_id][CONF_LOCKS][lock_entity_id] = (
async_create_lock_instance(
hass,
dr.async_get(hass),
er.async_get(hass),
config_entry,
lock_entity_id,
)
)
await lock.async_setup()

Expand All @@ -265,20 +306,20 @@ async def async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry)
_LOGGER.debug(
"%s (%s): Creating coordinator for lock %s", entry_id, entry_title, lock
)
coordinator = hass.data[DOMAIN][entry_id][COORDINATORS][
lock_entity_id
] = LockUsercodeUpdateCoordinator(hass, lock)
coordinator = hass.data[DOMAIN][entry_id][COORDINATORS][lock_entity_id] = (
LockUsercodeUpdateCoordinator(hass, lock)
)
await coordinator.async_config_entry_first_refresh()
for slot_num in new_slots:
_LOGGER.debug(
"%s (%s): Adding lock %s slot %s sensor",
"%s (%s): Adding lock %s slot %s sensor and event entity",
entry_id,
entry_title,
lock_entity_id,
slot_num,
)
async_dispatcher_send(
hass, f"{DOMAIN}_{entry_id}_add_lock_slot_sensor", lock, slot_num
hass, f"{DOMAIN}_{entry_id}_add_lock_slot", lock, slot_num
)

# Remove slot sensors that are no longer in the config
Expand All @@ -297,7 +338,9 @@ async def async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry)
][slot_num]
# First we store the set of entities we are adding so we can track when they are
# done
entities_to_add.update({CONF_ENABLED: True, CONF_NAME: True, CONF_PIN: True})
entities_to_add.update(
{CONF_ENABLED: True, CONF_NAME: True, CONF_PIN: True, EVENT_PIN_USED: True}
)
for lock_entity_id, lock in hass.data[DOMAIN][entry_id][CONF_LOCKS].items():
if lock_entity_id in locks_to_add:
continue
Expand All @@ -309,7 +352,7 @@ async def async_update_listener(hass: HomeAssistant, config_entry: ConfigEntry)
slot_num,
)
async_dispatcher_send(
hass, f"{DOMAIN}_{entry_id}_add_lock_slot_sensor", lock, slot_num
hass, f"{DOMAIN}_{entry_id}_add_lock_slot", lock, slot_num
)

# Check if we need to add a number of uses entity
Expand Down
49 changes: 34 additions & 15 deletions custom_components/lock_code_manager/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_PIN, STATE_ON
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
Expand All @@ -22,6 +23,7 @@
CONF_LOCKS,
CONF_NUMBER_OF_USES,
DOMAIN,
EVENT_PIN_USED,
PLATFORM_MAP,
)
from .entity import BaseLockCodeManagerEntity
Expand All @@ -44,7 +46,7 @@ def add_pin_enabled_entity(slot_num: int) -> None:
hass.data[DOMAIN][config_entry.entry_id][CONF_LOCKS].values()
)
async_add_entities(
[LockCodeManagerPINEnabledEntity(config_entry, locks, slot_num)],
[LockCodeManagerPINSyncedEntity(config_entry, locks, slot_num)],
True,
)

Expand All @@ -56,8 +58,8 @@ def add_pin_enabled_entity(slot_num: int) -> None:
return True


class LockCodeManagerPINEnabledEntity(BaseLockCodeManagerEntity, BinarySensorEntity):
"""PIN enabled binary sensor entity for lock code manager."""
class LockCodeManagerPINSyncedEntity(BaseLockCodeManagerEntity, BinarySensorEntity):
"""PIN synced to locks binary sensor entity for lock code manager."""

_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_should_poll = False
Expand All @@ -74,6 +76,7 @@ def __init__(
)
self._entity_id_map: dict[str, str] = {}
self._update_usercodes_task: asyncio.Task | None = None
self._issue_reg: ir.IssueRegistry | None = None

async def async_update_usercodes(self) -> None:
"""Update usercodes on locks based on state change."""
Expand Down Expand Up @@ -115,7 +118,7 @@ async def async_update_usercodes(self) -> None:
self.config_entry.async_create_task(
self.hass,
lock.async_set_usercode(
self.slot_num, pin_state.state, name_state.state
int(self.slot_num), pin_state.state, name_state.state
),
f"async_set_usercode_{lock.lock.entity_id}_{self.slot_num}",
)
Expand All @@ -135,7 +138,7 @@ async def async_update_usercodes(self) -> None:

self.config_entry.async_create_task(
self.hass,
lock.async_clear_usercode(self.slot_num),
lock.async_clear_usercode(int(self.slot_num)),
f"async_clear_usercode_{lock.lock.entity_id}_{self.slot_num}",
)

Expand Down Expand Up @@ -164,16 +167,29 @@ def _update_state(self) -> None:
# If there is a calendar entity, we need to check its state as well
if calendar_entity_id := self.config_entry.data.get(CONF_CALENDAR):
entity_id_map[CONF_CALENDAR] = calendar_entity_id
self._attr_extra_state_attributes = {"calendar": calendar_entity_id}
else:
self._attr_extra_state_attributes = {}

states = {
key: state.state
for key, entity_id in entity_id_map.items()
if key not in (CONF_NAME, CONF_PIN)
and (state := self.hass.states.get(entity_id))
}
states = {}
for key, entity_id in entity_id_map.items():
if key in (EVENT_PIN_USED, CONF_NAME, CONF_PIN):
continue
issue_id = f"{self.config_entry.entry_id}_{self.slot_num}_no_{key}"
if not (state := self.hass.states.get(entity_id)):
ir.async_create_issue(
self.hass,
DOMAIN,
issue_id,
translation_key="no_state",
translation_placeholders={
"entity_id": entity_id,
"entry_title": self.config_entry.title,
"key": key,
},
severity=ir.IssueSeverity.ERROR,
)
continue
else:
ir.async_delete_issue(self.hass, DOMAIN, issue_id)
states[key] = state.state

# For the binary sensor to be on, all states must be 'on', or for the number
# of uses, greater than 0
Expand Down Expand Up @@ -222,7 +238,7 @@ def _handle_state_changes(self, entity_id: str, _: State, __: State) -> None:
if any(
entity_id == key_entity_id
for key, key_entity_id in entity_id_map.items()
if key not in (CONF_NAME, CONF_PIN)
if key not in (EVENT_PIN_USED, CONF_NAME, CONF_PIN)
):
self._update_state()

Expand All @@ -231,6 +247,9 @@ async def async_added_to_hass(self) -> None:
await BinarySensorEntity.async_added_to_hass(self)
await BaseLockCodeManagerEntity.async_added_to_hass(self)

if not self._issue_reg:
self._issue_reg = ir.async_get(self.hass)

self.async_on_remove(
async_dispatcher_connect(
self.hass,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/lock_code_manager/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async def async_step_user(

if user_input is not None:
self.title = user_input.pop(CONF_NAME)
self.async_set_unique_id(slugify(self.title))
await self.async_set_unique_id(slugify(self.title))
self._abort_if_unique_id_configured()
self.data = user_input
return await self.async_step_choose_path()
Expand Down
9 changes: 8 additions & 1 deletion custom_components/lock_code_manager/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

DOMAIN = "lock_code_manager"
VERSION = "0.0.0" # this will be automatically updated as part of the release workflow
PLATFORMS = (Platform.BINARY_SENSOR, Platform.SENSOR)
PLATFORMS = (Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR)

HACS_DOMAIN = "hacs"

FILES_URL_BASE = f"/{DOMAIN}_files"
STRATEGY_FILENAME = "lock-code-manager-strategy.js"
STRATEGY_PATH = f"{FILES_URL_BASE}/{STRATEGY_FILENAME}"
FOLD_ENTITY_ROW_FILENAME = "fold-entity-row.js"

ATTR_CODE_SLOT = "code_slot"
ATTR_USERCODE = "usercode"
Expand All @@ -29,6 +32,9 @@
ATTR_CODE_SLOT_NAME = "code_slot_name"
ATTR_NOTIFICATION_SOURCE = "notification_source"

# Event entity event type
EVENT_PIN_USED = "pin_used"

# Configuration Properties
CONF_LOCKS = "locks"
CONF_SLOTS = "slots"
Expand All @@ -54,4 +60,5 @@
CONF_NAME: Platform.TEXT,
CONF_NUMBER_OF_USES: Platform.NUMBER,
CONF_PIN: Platform.TEXT,
EVENT_PIN_USED: Platform.EVENT,
}
Loading

0 comments on commit 8dd94b3

Please sign in to comment.