Skip to content

fix: Schlage sync loop and uncaught ReadTimeout#1089

Merged
raman325 merged 3 commits intomainfrom
fix/schlage-sync-loop
Apr 26, 2026
Merged

fix: Schlage sync loop and uncaught ReadTimeout#1089
raman325 merged 3 commits intomainfrom
fix/schlage-sync-loop

Conversation

@raman325
Copy link
Copy Markdown
Owner

@raman325 raman325 commented Apr 26, 2026

Proposed change

Fixes two bugs causing Schlage locks to repeatedly re-set codes every sync cycle:

1. Schlage set_usercode sync loop from eventual consistency

Schlage's cloud API has eventual consistency — get_codes may not return a code that add_code knows about. This caused async_set_usercode to attempt add_code for a code that already existed, getting "already exists" error → LockOperationFailed → retry → same error → infinite loop.

Fix: Simplified async_set_usercode to always delete-then-add. Before adding a code, both the existing tagged name and the new tagged name are speculatively deleted (deduplicated via set when names match). LockDisconnected on delete is swallowed since the code may not exist. This eliminates the "already exists" error entirely rather than handling it reactively.

2. Catch OSError in BaseLock async_call_service

ReadTimeout from requests/urllib3 extends OSError, not HomeAssistantError. When Schlage's cloud API timed out during a set operation, the error leaked past async_call_service to the sync manager's generic except Exception handler, which suspended the lock instead of retrying.

Fix: async_call_service now catches (HomeAssistantError, OSError) — covering both HA-wrapped errors and raw network errors from integrations that don't wrap them. Added test coverage for this behavior.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

…adTimeout

Two fixes:

1. Schlage async_set_usercode: handle "already exists" error from add_code
   by falling back to delete-then-add. Schlage's cloud API has eventual
   consistency — get_codes may not return a code that add_code knows about.
   This caused an infinite retry loop where the sync manager kept trying
   to add a code that already existed.

2. BaseLock async_call_service: catch OSError alongside HomeAssistantError.
   ReadTimeout (from requests/urllib3) extends OSError, not
   HomeAssistantError. When Schlage's cloud API timed out, the error leaked
   to the generic exception handler which suspended the lock instead of
   retrying.

Fixes #1083

Entire-Checkpoint: 1028a19f8cac
Copilot AI review requested due to automatic review settings April 26, 2026 16:55
@github-actions github-actions Bot added python Pull requests that update Python code bug Something isn't working labels Apr 26, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.74%. Comparing base (f017c74) to head (6eeecd2).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1089      +/-   ##
==========================================
+ Coverage   95.70%   95.74%   +0.03%     
==========================================
  Files          46       46              
  Lines        5286     5283       -3     
  Branches      503      503              
==========================================
- Hits         5059     5058       -1     
+ Misses        227      225       -2     
Flag Coverage Δ
python 97.42% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...om_components/lock_code_manager/providers/_base.py 96.10% <100.00%> (ø)
..._components/lock_code_manager/providers/schlage.py 94.37% <100.00%> (+1.12%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Schlage-specific sync retry loops and prevents network timeouts from escaping provider service-call wrappers during sync operations.

Changes:

  • Schlage provider: simplify set-usercode flow to delete-then-add when a code already exists, and recover from Schlage “already exists” eventual-consistency errors by deleting and retrying add.
  • Base provider: treat raw OSError failures from hass.services.async_call() as transient and wrap them as LockDisconnected for retry handling.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
custom_components/lock_code_manager/providers/schlage.py Updates async_set_usercode to avoid Schlage add-code duplicate-name/consistency failures that previously caused repeated retries.
custom_components/lock_code_manager/providers/_base.py Expands async_call_service exception handling to include OSError so timeouts/network errors are wrapped consistently.
Comments suppressed due to low confidence (1)

custom_components/lock_code_manager/providers/_base.py:844

  • async_call_service now wraps OSError into LockDisconnected, but the current base provider tests only cover HomeAssistantError wrapping and propagation of non-HA exceptions (e.g., TypeError). Please add a unit test that registers a service raising an OSError (e.g., TimeoutError/ConnectionError) and asserts it is converted into LockDisconnected, to lock in the intended behavior and prevent regressions.
        except (HomeAssistantError, OSError) as err:
            # HomeAssistantError covers ServiceValidationError and HA-wrapped
            # failures. OSError covers transient network errors (ReadTimeout,
            # ConnectionError) from integrations that don't wrap them in
            # HomeAssistantError. CancelledError and programming bugs
            # (TypeError, KeyError) deliberately propagate.
            LOGGER.error(
                "Error calling %s.%s service call: %s", domain, service, str(err)
            )
            raise LockDisconnected(
                f"Service call {domain}.{service} failed: {err}"
            ) from err

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread custom_components/lock_code_manager/providers/schlage.py Outdated
Comment thread custom_components/lock_code_manager/providers/schlage.py Outdated
raman325 and others added 2 commits April 26, 2026 13:02
Replace the two-codepath approach (try add → catch duplicate → delete →
retry) with unconditional delete-before-add. This handles Schlage cloud
API eventual consistency more cleanly by pre-emptively deleting both
the existing tagged name and the new tagged name before adding.

Uses a set to deduplicate when the name is unchanged (same code path
for PIN-only updates and name+PIN updates).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e5ef38dbac33
…all_service

Verifies that transient network errors (TimeoutError, ReadTimeout) from
integrations that don't wrap them in HomeAssistantError are correctly
routed through the retry/backoff path as LockDisconnected.

Addresses Copilot review comment on PR #1089.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e3a77ed6db14
@raman325 raman325 merged commit 032b2d2 into main Apr 26, 2026
22 checks passed
@raman325 raman325 deleted the fix/schlage-sync-loop branch April 26, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ISSUE] Blueprint does not trigger (Schlage Encode)

2 participants