-
Notifications
You must be signed in to change notification settings - Fork 54
refactor: introduce provider abstraction for multi-platform lock support #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Conversation
|
YAY! @raman325 you're awesome! I'll get this onto my test system as soon as I can! I've been wanting something like this as I've been working with someone that went all Schlage WiFi locks but they are doing short term rentals. I wrote a custom automation that's really fragile to work with the lock and rental control, but if (when) this lands getting a Schlage provider in (after I get home-assistant/core#151014) merged will make it so much better! |
cfe6c5c to
36b5378
Compare
36b5378 to
1f7fa28
Compare
- Create providers/ module with BaseLockProvider abstract base class - Implement ZWaveJSLockProvider extracting all Z-Wave JS specific code - Refactor coordinator to use provider methods instead of direct API calls - Update entity platforms to use provider capabilities (supports_connection_status, etc.) - Add async_has_supported_provider() and async_get_lock_platform() helpers - Maintain backward compatibility with existing Z-Wave JS configurations This is Phase 1-3 of the provider abstraction refactor, enabling future support for additional lock platforms (ZHA, Zigbee2MQTT) without changes to the core coordinator logic. Files added: - providers/__init__.py: Provider registry and factory functions - providers/_base.py: BaseLockProvider ABC and CodeSlot dataclass - providers/zwave_js.py: Z-Wave JS lock provider implementation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename _handle_lock_event_from_provider to _handle_provider_lock_event - Update test fixtures to patch async_has_supported_provider instead of async_using_zwave_js in binary_sensor and switch modules - Add cleanup fixture to prevent JSON file corruption between tests - Add provider field to excluded_fields in test_lock_dataclass - Rewrite coordinator event tests for new provider callback interface - Add PROVIDERS.md developer guide for implementing new lock providers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The callback is async and needs to be scheduled as a task rather than called directly. Update LockEventCallback type alias to reflect async signature. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1f7fa28 to
ff7d1ef
Compare
The previous cleanup looked for json_kmlocks in .venv, but CI uses system-wide package installation. Now dynamically finds the testing_config path from the installed pytest_homeassistant_custom_component package. Also cleans up after tests, not just before. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused TYPE_CHECKING import from providers/__init__.py - Add noqa for intentional lazy import in _register_providers - Convert relative imports to absolute in providers modules - Move ZWaveJSLockProvider conditional import to top level in coordinator - Move is_platform_supported import to top level in helpers - Move pytest_homeassistant_custom_component import to top level in conftest - Fix TRY300: move return to else block in zwave_js provider Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create test_provider_zwave_js.py with 42 tests covering: - Provider properties (domain, push updates, connection status) - Connection handling (entity lookup, config entry, client access) - Usercode operations (get/set/clear with error handling) - Event subscription - Diagnostics (node ID, status, platform data) - Provider factory functions - Integration test for Z-Wave JS notification events - Add MockProvider class to conftest.py for provider-agnostic testing - Move Z-Wave JS specific test from test_helpers.py to provider module - Coverage increased from 78.84% to 81.21% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## beta #538 +/- ##
==========================================
+ Coverage 80.86% 84.91% +4.05%
==========================================
Files 19 24 +5
Lines 2341 2685 +344
==========================================
+ Hits 1893 2280 +387
+ Misses 448 405 -43
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Add 14 new tests to test_helpers.py achieving 100% coverage: - Timer expired edge cases (cancel with timer_elapsed, is_running/is_setup/end_time/remaining_seconds when expired) - _async_using TypeError and kmlock without entity_id cases - async_has_supported_provider with entity_id parameter - async_get_lock_platform all branches - dismiss_persistent_notification function Add 31 new tests to test_coordinator.py improving coverage from 62% to 67%: - _encode_pin/_decode_pin roundtrip tests - _is_slot_active with real KeymasterCodeSlot instances - File operations (create folder, delete JSON, write config) with error handling - _dict_to_kmlocks and _kmlocks_to_dict conversion tests Overall test coverage improved from 81.21% to 84.38%. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
41cd0b6 to
cc9f393
Compare
- Move ZWaveJSLockProvider import to top-level in providers/__init__.py - Remove lazy registration pattern (no longer needed with HA 2025.8.0+) - Remove try/except import wrapper in coordinator.py - Replace isinstance checks with domain property checks (we trust our own providers) - Import ZWAVE_JS_DOMAIN constant instead of hardcoding string Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move async_get_usercode and async_get_usercode_from_node to base class as optional methods with default None implementations. This eliminates the need for type: ignore comments when calling these methods on providers that support them. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove zwave_js_lock_node and zwave_js_lock_device from KeymasterLock - Remove ZWAVE_JS_DOMAIN import and usage from coordinator - Remove backwards compatibility block that set deprecated fields - Clean up JSON load/save handling for removed fields - Simplify code slot refresh to use base class API without domain checks The coordinator is now fully provider-agnostic - all provider interaction happens through the BaseLockProvider API. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename ZWaveIntegrationNotConfiguredError to ProviderNotConfiguredError - Remove unused ZWaveNetworkNotReady exception - Remove deprecated async_using_zwave_js function and _async_using helper - Remove ZWAVE_JS_DOMAIN import and ZWAVE_JS_SUPPORTED constant from helpers - Remove unused mock_using_zwavejs fixture and related tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused test fixtures: mock_listdir, mock_listdir_err, mock_osremove, mock_osrmdir, mock_osmakedir, mock_os_path_join - Remove async_get_lock_platform from helpers (not used by production code) - Remove corresponding tests for removed function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move test_provider_zwave_js.py to tests/providers/test_zwave_js.py - Add tests/providers/__init__.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
I pulled the latest version of this and pushed it onto my test instance. The good news, the new dynamic dashboards still work ;) The bad news I see the following in the logs during startup (yes I have debug logs enabled but the ERROR isn't at DEBUG level: What's more, when I add a PIN I see the same error. The PIN does get set (eventually) but that error does happen when the slot is first modified |
The provider object (ZWaveJSLockProvider) was being included when serializing locks to JSON, causing a TypeError since the provider is not JSON serializable. Added provider to the list of fields excluded during JSON export alongside autolock_timer and listeners. Fixes error reported by tykeal in PR FutureTense#538. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add blank lines before closing docstring quotes in _base.py - Fix import order and f-string in compare_lovelace_output.py - Remove unused pytest import in test_helpers.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@raman325 thanks. I pulled the latest version and now I'm getting the following during startup: Could this be because of the prior issue? I can revert to the latest version of KM, restart, and then upgrade again if you want me to try that. |
|
@tykeal Yes, that's likely from the corrupted JSON file that was written before the fix. The easiest solution is to delete the corrupted file: rm /config/custom_components/keymaster/json_kmlocks/keymaster_kmlocks.jsonThen restart Home Assistant. Keymaster will recreate the file from scratch using your config entry data. Alternatively, you could revert to beta, restart (to write a clean JSON), then upgrade again - but deleting the file is simpler. |
|
@raman325 ok, I removed the json file and restarted. Everything started worked (after I had to reset my test codes). Tested and works:
Tested and does not work: Not tested: |
Resolve conflicts: - coordinator.py: keep provider abstraction (no direct zwave_js imports) - compare_lovelace_output.py: add tempfile import from beta Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The _is_slot_active method now checks for int type instead of float, so update the test to match. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
More platform-agnostic name (removes Z-Wave "node" terminology) and clearer intent that it forces a refresh from the device. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
For integrations without caching, refresh and get are functionally identical. Only integrations with caching need to override. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add filter_func parameter to _get_entities for optional entity filtering. Config flow now only shows locks from supported platforms (e.g., Z-Wave JS). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Config flow now filters to supported platforms only, and coordinator fails setup if provider creation fails. By platform setup time, the provider is guaranteed to exist, making these checks redundant. - Remove async_has_supported_provider checks from binary_sensor.py - Remove async_has_supported_provider if/else wrapper from switch.py - Remove corresponding test fixture patches Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fce01d0 to
d2d4cc5
Compare
|
@raman325 ok... so the number of uses is working.... kinda. It seems to take about 30 seconds for it to catch up (standard entity refresh time?) So, it's technically possible to use a code multiple times even if it's set for 1 in a very short amount of time. |
|
ah we probably need to write state on setting the native value of the number entity. I actually noticed that but didn't think to fix it |
|
Well, with me sitting with my test lock in my lap in a demo "door" it's real easy for me to cheese my way through 2 - 3 cycles on a '1' real quick ;) |
Summary
This PR introduces a provider abstraction layer to enable support for multiple lock platforms. The architecture allows adding support for additional platforms without modifying core keymaster code.
Breaking change
None. Existing Z-Wave JS configurations continue to work unchanged. The provider is automatically detected and instantiated based on the lock entity's platform.
Proposed change
Architecture
providers/module with:_base.py-BaseLockProviderabstract base class andCodeSlotdataclass__init__.py- Provider registry and factory functionszwave_js.py- Z-Wave JS implementation (extracted from coordinator)PROVIDERS.md- Developer guide for implementing new providersKey Changes
Provider Interface: New
BaseLockProviderABC defines required methods:async_connect()- Connect to lockasync_is_connected()- Check connection statusasync_get_usercodes()- Get all codes from lockasync_get_usercode()- Get a specific code (may return cached data)async_refresh_usercode()- Bypass cache and query device directly (for integrations with caching)async_set_usercode()- Set a code slotasync_clear_usercode()- Clear a code slotCapability Flags: Providers declare capabilities:
supports_push_updates- Real-time lock/unlock eventssupports_connection_status- Connection monitoringCoordinator Refactoring:
Config Flow Improvements:
filter_funcparameter to_get_entitiesfor extensible filteringEntity Updates:
binary_sensor.pyandswitch.pyuseasync_has_supported_provider()Files Changed
New Files:
providers/__init__.py- Registry and factoryproviders/_base.py- Base classesproviders/zwave_js.py- Z-Wave JS providerproviders/PROVIDERS.md- Developer documentationtests/providers/- Provider-specific testsModified Files:
coordinator.py- Use provider abstractionlock.py- Addedproviderfieldhelpers.py- Addedasync_has_supported_provider()config_flow.py- Filter locks by supported platformbinary_sensor.py,switch.py- Use new helperTest Updates
async_has_supported_providerfunctiontests/providers/directoryType of change
Additional information
async_refresh_usercodedefaults to callingasync_get_usercode- only integrations with caching (like Z-Wave JS) need to override🤖 Generated with Claude Code