Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions custom_components/keymaster/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"""Set up is called when Home Assistant is loading our component."""
updated_config = config_entry.data.copy()

for prop in [
for prop in (
CONF_PARENT,
CONF_NOTIFY_SCRIPT_NAME,
CONF_DOOR_SENSOR_ENTITY_ID,
CONF_ALARM_LEVEL_OR_USER_CODE_ENTITY_ID,
CONF_ALARM_TYPE_OR_ACCESS_CONTROL_ENTITY_ID,
]:
):
if config_entry.data.get(prop) in {
NONE_TEXT,
"sensor.fake",
Expand Down
27 changes: 19 additions & 8 deletions custom_components/keymaster/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1582,24 +1582,35 @@ async def _update_slot(
async def _sync_usercode(self, kmlock: KeymasterLock, usercode_slot: CodeSlot) -> None:
"""Sync a usercode from the lock."""
code_slot_num: int = usercode_slot.slot_num
km_code_slot: KeymasterCodeSlot | None = None
if kmlock.code_slots:
km_code_slot = kmlock.code_slots.get(code_slot_num)
usercode: str | None = usercode_slot.code
in_use: bool = usercode_slot.in_use

if not kmlock.code_slots or code_slot_num not in kmlock.code_slots:
if not km_code_slot:
return

# Refresh from lock if slot claims to have a code but we don't have the value
# (e.g., masked responses where in_use=True but code is None or non-numeric)
if in_use and (usercode is None or not usercode.isdigit()) and kmlock.provider:
# (e.g., masked responses where in_use=True but code is None or all one
# character)
if kmlock.provider and in_use and (usercode is None or len(set(usercode)) == 1):
refreshed = await kmlock.provider.async_refresh_usercode(code_slot_num)
if refreshed:
usercode = refreshed.code
in_use = refreshed.in_use

# Fix for Schlage masked responses: if slot is not in use (status=0) but
# usercode is masked (e.g., "**********"), treat it as empty
if not in_use and usercode and not usercode.isdigit():
usercode = ""
# usercode is masked (e.g., "**********" or "0000000000"), treat it as empty
for mask_char in ("*", "0"):
if (
not in_use
and usercode
and usercode == mask_char * len(usercode)
and usercode != km_code_slot.pin # Make sure PIN isn't set to e.g. 0000
):
usercode = ""
break

await self._sync_pin(kmlock, code_slot_num, usercode or "")

Expand Down Expand Up @@ -1718,7 +1729,7 @@ async def _update_child_code_slots(
prev_enabled = child_kmlock.code_slots[code_slot_num].enabled
prev_active = child_kmlock.code_slots[code_slot_num].active

for attr in [
for attr in (
"enabled",
"name",
"active",
Expand All @@ -1729,7 +1740,7 @@ async def _update_child_code_slots(
"accesslimit_date_range_start",
"accesslimit_date_range_end",
"accesslimit_day_of_week_enabled",
]:
):
if hasattr(kmslot, attr):
setattr(
child_kmlock.code_slots[code_slot_num],
Expand Down
4 changes: 2 additions & 2 deletions custom_components/keymaster/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def _migrate_2to3_delete_folder(absolute_path: Path, *relative_paths: str) -> No

async def _migrate_2to3_reload_package_platforms(hass: HomeAssistant) -> bool:
"""Reload package platforms to pick up any changes to package files."""
for domain in [
for domain in (
AUTO_DOMAIN,
IN_BOOL_DOMAIN,
IN_DT_DOMAIN,
Expand All @@ -309,7 +309,7 @@ async def _migrate_2to3_reload_package_platforms(hass: HomeAssistant) -> bool:
SCRIPT_DOMAIN,
TEMPLATE_DOMAIN,
TIMER_DOMAIN,
]:
):
if hass.services.has_service(domain=domain, service=SERVICE_RELOAD):
await hass.services.async_call(domain=domain, service=SERVICE_RELOAD, blocking=True)
else:
Expand Down
28 changes: 15 additions & 13 deletions custom_components/keymaster/providers/zwave_js.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,10 +538,6 @@ async def async_clear_usercode(self, slot_num: int) -> bool:

try:
await clear_usercode(self._node, slot_num)
_LOGGER.debug(
"[ZWaveJSProvider] Cleared usercode on slot %s",
slot_num,
)
except BaseZwaveJSServerError as e:
_LOGGER.error(
"[ZWaveJSProvider] Failed to clear usercode on slot %s: %s: %s",
Expand All @@ -550,26 +546,32 @@ async def async_clear_usercode(self, slot_num: int) -> bool:
e,
)
return False
else:
_LOGGER.debug(
"[ZWaveJSProvider] Cleared usercode on slot %s",
slot_num,
)

# Verify the code was cleared
try:
usercode = get_usercode(self._node, slot_num)
# Treat both "" and full string of "0" as cleared (Schlage BE469 firmware bug workaround)
if not (
usercode[ZWAVEJS_ATTR_USERCODE] == ""
or all(char == "0" for char in usercode[ZWAVEJS_ATTR_USERCODE])
):
_LOGGER.debug(
"[ZWaveJSProvider] Slot %s not yet cleared, will retry",
slot_num,
)
except BaseZwaveJSServerError as e:
_LOGGER.error(
"[ZWaveJSProvider] Failed to verify clear on slot %s: %s: %s",
slot_num,
e.__class__.__qualname__,
e,
)
return False

# Treat both "" and full string of "0" as cleared (Schlage BE469 firmware bug workaround)
code_value = usercode.get(ZWAVEJS_ATTR_USERCODE) or ""
if code_value not in ("", "0" * len(code_value)):
_LOGGER.debug(
"[ZWaveJSProvider] Slot %s not yet cleared, will retry",
slot_num,
)
return False

return True

Expand Down