From f334a817a3885691966efecb1c1d57be6ff416cb Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:53:02 -0500 Subject: [PATCH 1/3] address copilot comments Co-Authored-By: Claude Opus 4.5 --- custom_components/keymaster/__init__.py | 4 +-- custom_components/keymaster/coordinator.py | 29 ++++++++++++------ custom_components/keymaster/migrate.py | 4 +-- .../keymaster/providers/zwave_js.py | 30 +++++++++++-------- 4 files changed, 41 insertions(+), 26 deletions(-) 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..d2a4e83d 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 = "" + # Fix for Schlage and Yale masked responses: if slot is not in use (status=0) + # but usercode is masked (e.g., "**********" for Schalge, "0000" for Yale), + # treat it as empty + for mask_char in ("*", "0"): + if ( + not in_use + and usercode + and usercode != km_code_slot.pin + and usercode == mask_char * len(usercode) + ): + usercode = "" 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..28433a30 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,18 @@ 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 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, + ) + return False return True From b9c5a645e06d7b739f968f27051564b17ea7ed0c Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:01:11 -0500 Subject: [PATCH 2/3] address copilot comments Co-Authored-By: Claude Opus 4.5 --- custom_components/keymaster/coordinator.py | 5 +++-- custom_components/keymaster/providers/zwave_js.py | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/custom_components/keymaster/coordinator.py b/custom_components/keymaster/coordinator.py index d2a4e83d..db2e9f97 100644 --- a/custom_components/keymaster/coordinator.py +++ b/custom_components/keymaster/coordinator.py @@ -1601,16 +1601,17 @@ async def _sync_usercode(self, kmlock: KeymasterLock, usercode_slot: CodeSlot) - in_use = refreshed.in_use # Fix for Schlage and Yale masked responses: if slot is not in use (status=0) - # but usercode is masked (e.g., "**********" for Schalge, "0000" for Yale), + # but usercode is masked (e.g., "**********" for Schlage, "0000" for Yale), # treat it as empty for mask_char in ("*", "0"): if ( not in_use and usercode - and usercode != km_code_slot.pin 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 "") diff --git a/custom_components/keymaster/providers/zwave_js.py b/custom_components/keymaster/providers/zwave_js.py index 28433a30..6e4edbff 100644 --- a/custom_components/keymaster/providers/zwave_js.py +++ b/custom_components/keymaster/providers/zwave_js.py @@ -565,10 +565,7 @@ async def async_clear_usercode(self, slot_num: int) -> bool: return False # 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]) - ): + 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, From 5b360281e42aa2e9913f1ff36bdcf357b8f8e541 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:04:37 -0500 Subject: [PATCH 3/3] docs: clarify masked response comment Co-Authored-By: Claude Opus 4.5 --- custom_components/keymaster/coordinator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/keymaster/coordinator.py b/custom_components/keymaster/coordinator.py index db2e9f97..74795c4f 100644 --- a/custom_components/keymaster/coordinator.py +++ b/custom_components/keymaster/coordinator.py @@ -1600,9 +1600,8 @@ async def _sync_usercode(self, kmlock: KeymasterLock, usercode_slot: CodeSlot) - usercode = refreshed.code in_use = refreshed.in_use - # Fix for Schlage and Yale masked responses: if slot is not in use (status=0) - # but usercode is masked (e.g., "**********" for Schlage, "0000" for Yale), - # treat it as empty + # Fix for Schlage masked responses: if slot is not in use (status=0) but + # usercode is masked (e.g., "**********" or "0000000000"), treat it as empty for mask_char in ("*", "0"): if ( not in_use