diff --git a/custom_components/keymaster/__init__.py b/custom_components/keymaster/__init__.py index be46acdc..c5419c90 100644 --- a/custom_components/keymaster/__init__.py +++ b/custom_components/keymaster/__init__.py @@ -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", diff --git a/custom_components/keymaster/coordinator.py b/custom_components/keymaster/coordinator.py index 4b1818c1..74795c4f 100644 --- a/custom_components/keymaster/coordinator.py +++ b/custom_components/keymaster/coordinator.py @@ -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 "") @@ -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", @@ -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], diff --git a/custom_components/keymaster/migrate.py b/custom_components/keymaster/migrate.py index e138901a..b0597344 100644 --- a/custom_components/keymaster/migrate.py +++ b/custom_components/keymaster/migrate.py @@ -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, @@ -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: diff --git a/custom_components/keymaster/providers/zwave_js.py b/custom_components/keymaster/providers/zwave_js.py index b2be8cf6..6e4edbff 100644 --- a/custom_components/keymaster/providers/zwave_js.py +++ b/custom_components/keymaster/providers/zwave_js.py @@ -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", @@ -550,19 +546,15 @@ 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", @@ -570,6 +562,15 @@ async def async_clear_usercode(self, slot_num: int) -> bool: e.__class__.__qualname__, e, ) + return False + + # Treat both "" and full string of "0" as cleared (Schlage BE469 firmware bug workaround) + if usercode[ZWAVEJS_ATTR_USERCODE] not in ("", "0" * len(usercode[ZWAVEJS_ATTR_USERCODE])): + _LOGGER.debug( + "[ZWaveJSProvider] Slot %s not yet cleared, will retry", + slot_num, + ) + return False return True