Skip to content

Conversation

@raman325
Copy link
Collaborator

@raman325 raman325 commented Jan 12, 2026

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

  • Created providers/ module with:
    • _base.py - BaseLockProvider abstract base class and CodeSlot dataclass
    • __init__.py - Provider registry and factory functions
    • const.py - Provider-specific constants (alarm sensor types)
    • zwave_js.py - Z-Wave JS implementation (extracted from coordinator)
    • PROVIDERS.md - Developer guide for implementing new providers

Key Changes

  1. Provider Interface: New BaseLockProvider ABC defines required methods:

    • async_connect() - Connect to lock
    • async_is_connected() - Check connection status
    • async_get_usercodes() - Get all codes from lock
    • async_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 slot
    • async_clear_usercode() - Clear a code slot
  2. Capability Flags: Providers declare capabilities:

    • supports_push_updates - Real-time lock/unlock events
    • supports_connection_status - Connection monitoring
  3. Coordinator Refactoring:

    • Removed direct Z-Wave JS imports and API calls
    • Uses provider methods for all lock operations
    • Provider handles event subscription internally
    • Added immediate UI notification when sync status changes
    • Fixed event subscription timing for locks restored from JSON
  4. Z-Wave Specific Logic Moved to Provider:

    • Sensor-based event detection (_handle_lock_state_change) moved to ZWaveJSLockProvider.subscribe_lock_events()
    • LOCK_ACTIVITY_MAP, LOCK_STATE_MAP, and LockActivity moved to providers/zwave_js.py
    • Alarm sensor translation logic is now internal to the Z-Wave JS provider
    • Coordinator is now platform-agnostic for event handling
  5. Config Flow Improvements:

    • Now filters lock entities to only show supported platforms
    • Added generic filter_func parameter to _get_entities for extensible filtering
    • Prevents users from selecting unsupported locks
  6. Entity Updates:

    • binary_sensor.py and switch.py use async_has_supported_provider()
    • Connection sensor only created when provider supports it
  7. Lovelace Updates:

    • Restored reset button to code slot cards (was present before but got lost at some point)

Files Changed

New Files:

  • providers/__init__.py - Registry and factory
  • providers/_base.py - Base classes and dataclasses
  • providers/const.py - Provider-specific constants
  • providers/zwave_js.py - Z-Wave JS provider
  • providers/PROVIDERS.md - Developer documentation
  • tests/providers/ - Provider-specific tests

Modified Files:

  • coordinator.py - Use provider abstraction, removed Z-Wave specific code
  • const.py - Removed Z-Wave specific maps (moved to provider)
  • lock.py - Added provider field
  • helpers.py - Added async_has_supported_provider()
  • config_flow.py - Filter locks by supported platform
  • binary_sensor.py, switch.py - Use new helper
  • lovelace.py - Restored reset button to code slot cards

Test Updates

  • Added cleanup fixture to prevent JSON file corruption between tests
  • Updated fixtures to patch new async_has_supported_provider function
  • Rewrote coordinator event tests for provider callback interface
  • Moved provider tests to tests/providers/ directory
  • Added tests for dual event subscription (notification + state change)
  • All 352 tests pass with 85% coverage

Type of change

  • New feature (which adds functionality)

Additional information

  • Developer guide included for implementing new providers
  • async_refresh_usercode defaults to calling async_get_usercode - only integrations with caching (like Z-Wave JS) need to override
  • Z-Wave JS provider handles both notification events AND state-change-based event detection (for locks that don't reliably fire notification events)
  • Copilot review feedback addressed: event subscription timing, refresh condition optimization, documentation updates

🤖 Generated with Claude Code

@tykeal
Copy link
Contributor

tykeal commented Jan 12, 2026

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!

@raman325 raman325 force-pushed the feature/provider-abstraction branch from cfe6c5c to 36b5378 Compare January 13, 2026 05:59
@firstof9 firstof9 added the enhancement New feature or request label Jan 13, 2026
@raman325 raman325 force-pushed the feature/provider-abstraction branch from 36b5378 to 1f7fa28 Compare January 18, 2026 14:49
raman325 and others added 4 commits January 18, 2026 12:54
- 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>
@raman325 raman325 force-pushed the feature/provider-abstraction branch from 1f7fa28 to ff7d1ef Compare January 18, 2026 18:00
raman325 and others added 3 commits January 18, 2026 13:06
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-commenter
Copy link

codecov-commenter commented Jan 18, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 77.91932% with 104 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.62%. Comparing base (a9a30ef) to head (1934c7a).
⚠️ Report is 14 commits behind head on beta.

Files with missing lines Patch % Lines
custom_components/keymaster/providers/zwave_js.py 77.39% 59 Missing ⚠️
custom_components/keymaster/coordinator.py 68.96% 18 Missing ⚠️
custom_components/keymaster/providers/_base.py 77.63% 17 Missing ⚠️
custom_components/keymaster/switch.py 82.35% 3 Missing ⚠️
custom_components/keymaster/binary_sensor.py 66.66% 2 Missing ⚠️
custom_components/keymaster/providers/__init__.py 93.93% 2 Missing ⚠️
custom_components/keymaster/config_flow.py 75.00% 1 Missing ⚠️
custom_components/keymaster/exceptions.py 50.00% 1 Missing ⚠️
custom_components/keymaster/helpers.py 85.71% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             beta     #538      +/-   ##
==========================================
+ Coverage   80.86%   83.62%   +2.75%     
==========================================
  Files          19       25       +6     
  Lines        2341     2711     +370     
==========================================
+ Hits         1893     2267     +374     
+ Misses        448      444       -4     
Flag Coverage Δ
python 83.62% <77.91%> (?)

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

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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>
@raman325 raman325 force-pushed the feature/provider-abstraction branch from 41cd0b6 to cc9f393 Compare January 18, 2026 20:11
raman325 and others added 6 commits January 19, 2026 02:40
- 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>
@tykeal
Copy link
Contributor

tykeal commented Jan 19, 2026

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:

2026-01-19 06:38:05.681 DEBUG (MainThread) [custom_components.keymaster.coordinator] [async_setup] Imported 1 keymaster locks
2026-01-19 06:38:05.683 DEBUG (MainThread) [custom_components.keymaster.coordinator] [unsubscribe_listeners] ZWaveTestLock: Removing all listeners
2026-01-19 06:38:05.683 DEBUG (MainThread) [custom_components.keymaster.coordinator] [update_listeners] ZWaveTestLock: Setting create_listeners to run when HA starts
2026-01-19 06:38:05.684 DEBUG (MainThread) [custom_components.keymaster.coordinator] ================================
2026-01-19 06:38:05.684 DEBUG (MainThread) [custom_components.keymaster.coordinator] [verify_lock_configuration] Verifying 01K4ZADY3CDS5DT5BVPBQYQHXP
2026-01-19 06:38:05.684 DEBUG (MainThread) [custom_components.keymaster.coordinator] ================================
2026-01-19 06:38:05.685 DEBUG (MainThread) [custom_components.keymaster.coordinator] [connect_and_update_lock] ZWaveTestLock: Provider connected (platform: zwave_js)
2026-01-19 06:38:05.685 DEBUG (MainThread) [custom_components.keymaster.coordinator] [update_lock_data] ZWaveTestLock: usercodes count: 30
2026-01-19 06:38:09.038 ERROR (MainThread) [custom_components.keymaster.coordinator] Unexpected error fetching keymaster data
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 416, in _async_refresh
    self.data = await self._async_update_data()
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/keymaster/coordinator.py", line 1604, in _async_update_data
    await self.hass.async_add_executor_job(self._write_config_to_json)
  File "/usr/local/lib/python3.13/concurrent/futures/thread.py", line 59, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/keymaster/coordinator.py", line 392, in _write_config_to_json
    json.dump(config, jsonfile)
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/json/__init__.py", line 181, in dump
    for chunk in iterable:
                 ^^^^^^^^
  File "/usr/local/lib/python3.13/json/encoder.py", line 435, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/usr/local/lib/python3.13/json/encoder.py", line 409, in _iterencode_dict
    yield from chunks
  File "/usr/local/lib/python3.13/json/encoder.py", line 409, in _iterencode_dict
    yield from chunks
  File "/usr/local/lib/python3.13/json/encoder.py", line 442, in _iterencode
    o = _default(o)
  File "/usr/local/lib/python3.13/json/encoder.py", line 182, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
                    f'is not JSON serializable')
TypeError: Object of type ZWaveJSLockProvider is not JSON serializable
2026-01-19 06:38:09.304 DEBUG (MainThread) [custom_components.keymaster.coordinator] Finished fetching keymaster data in 3.619 seconds (success: False)

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>
@raman325
Copy link
Collaborator Author

Thanks for testing @tykeal!

Fixed in ab9c410 - the provider field was being included in the JSON serialization. It's now excluded alongside autolock_timer and listeners (which are also non-serializable runtime objects).

raman325 and others added 2 commits January 19, 2026 10:10
- 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>
@tykeal
Copy link
Contributor

tykeal commented Jan 19, 2026

@raman325 thanks. I pulled the latest version and now I'm getting the following during startup:

2026-01-19 09:00:40.025 INFO (MainThread) [custom_components.keymaster.coordinator] Keymaster v0.0.0 is starting, if you have any issues please report them here: https://github.com/FutureTense/keymaster
2026-01-19 09:00:40.026 DEBUG (SyncWorker_3) [custom_components.keymaster.coordinator] [create_json_folder] json_kmlocks Location: /config/custom_components/keymaster/json_kmlocks
2026-01-19 09:00:40.030 ERROR (MainThread) [homeassistant.config_entries] Error setting up entry ZWaveTestLock for keymaster
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/config_entries.py", line 762, in __async_setup_with_context
    result = await component.async_setup_entry(hass, self)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/keymaster/__init__.py", line 131, in async_setup_entry
    await async_setup_services(hass)
  File "/config/custom_components/keymaster/services.py", line 33, in async_setup_services
    await coordinator.initial_setup()
  File "/config/custom_components/keymaster/coordinator.py", line 108, in initial_setup
    await self._async_setup()
  File "/config/custom_components/keymaster/coordinator.py", line 118, in _async_setup
    imported_config = await self.hass.async_add_executor_job(self._get_dict_from_json_file)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/concurrent/futures/thread.py", line 59, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/keymaster/coordinator.py", line 147, in _get_dict_from_json_file
    config = json.load(jsonfile)
  File "/usr/local/lib/python3.13/json/__init__.py", line 298, in load
    return loads(fp.read(),
        cls=cls, object_hook=object_hook,
        parse_float=parse_float, parse_int=parse_int,
        parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/usr/local/lib/python3.13/json/__init__.py", line 352, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "/usr/local/lib/python3.13/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 410 (char 409)

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.

@raman325
Copy link
Collaborator Author

@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.json

Then 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.

@tykeal
Copy link
Contributor

tykeal commented Jan 20, 2026

@raman325 ok, I removed the json file and restarted. Everything started worked (after I had to reset my test codes).

Tested and works:

  • Adding and removing codes works
  • Using the date range works

Tested and does not work:
Number of uses does not work (though I think the current release doesn't actually work either). Number never decrements. I did, however, test if it removes the code if it hits zero by manually updated the count and that does work

Not tested:
Custom days of week

raman325 and others added 5 commits January 20, 2026 10:00
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>
@raman325 raman325 force-pushed the feature/provider-abstraction branch from fce01d0 to d2d4cc5 Compare January 20, 2026 16:38
@tykeal
Copy link
Contributor

tykeal commented Jan 20, 2026

@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.

@raman325
Copy link
Collaborator Author

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

@tykeal
Copy link
Contributor

tykeal commented Jan 20, 2026

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 ;)

…straction

* upstream/beta:
  fix: immediately update UI when accesslimit_count changes (FutureTense#554)
@tykeal
Copy link
Contributor

tykeal commented Jan 21, 2026

@raman325 I just tested everything (including the custom days of week + custom days of week with specific time range) it's all working correctly now.

The one thing that I notice is that while everything seems to be working well, the sync status doesn't stay current. For instance, when I was playing with the limited number of uses, the count hit zero and the code stopped working (like it should) but the sync status stayed Synced for an entity refresh cycle then it went to Disconnecting and then after another refresh cycle Disconnected so it lags

raman325 and others added 2 commits January 21, 2026 16:33
Add async_set_updated_data() calls in set_pin_on_lock and
clear_pin_from_lock right after setting sync status. This ensures
entities see ADDING/DELETING status immediately rather than waiting
for the next coordinator refresh cycle.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update sync status and notify entities after provider operations
complete:
- set_pin_on_lock: set synced=SYNCED after successful set
- clear_pin_from_lock: set synced=DISCONNECTED after successful clear

This gives immediate feedback when operations complete, rather than
waiting for the next coordinator refresh cycle.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@raman325
Copy link
Collaborator Author

try now 🙂

@tykeal
Copy link
Contributor

tykeal commented Jan 22, 2026

Everything is working great with this now and the response is fast! :)

@raman325 raman325 marked this pull request as ready for review January 22, 2026 17:40
Copilot AI review requested due to automatic review settings January 22, 2026 17:40
Copy link

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

This PR introduces a provider abstraction layer so keymaster can support multiple lock platforms (starting with Z-Wave JS) without embedding platform-specific logic in the coordinator and entities.

Changes:

  • Added a generic BaseLockProvider interface and a concrete ZWaveJSLockProvider, plus a registry/factory in providers/ for resolving the correct provider based on entity platform.
  • Refactored KeymasterCoordinator, KeymasterLock, helpers, and entities to use provider APIs for usercode management, connection status, and event subscriptions instead of direct Z-Wave JS calls.
  • Updated config flow and tests to only allow supported lock platforms, to exercise the new provider behavior, and to harden JSON file handling and internal conversions.

Reviewed changes

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

Show a summary per file
File Description
tests/test_lock_dataclass.py Extends dataclass lookup tests to account for the new non-serializable provider field on KeymasterLock.
tests/test_helpers.py Replaces Z-Wave JS–specific helper tests with async_has_supported_provider coverage and adds tests for KeymasterTimer and notification dismissal.
tests/test_coordinator_events.py Rewrites coordinator event tests to target the new _handle_provider_lock_event interface instead of direct Z-Wave JS events.
tests/test_coordinator.py Adds unit tests for PIN encode/decode, slot-activity evaluation, JSON file operations, and dataclass↔dict conversion helpers in the coordinator.
tests/test_config_flow.py Removes Z-Wave JS–specific patching from the reconfiguration flow tests to align with provider-based platform support checks.
tests/providers/test_zwave_js.py New test suite validating ZWaveJSLockProvider behavior, provider factory functions, and an end-to-end Z-Wave JS notification integration path.
tests/providers/init.py Marks the new tests.providers package for provider-specific test modules.
tests/conftest.py Introduces a MockProvider implementation, automatic cleanup of keymaster JSON test files, and updates entity listing fixtures for the new filter_func parameter.
custom_components/keymaster/switch.py Removes Z-Wave JS gating and builds switch entities unconditionally once a KeymasterLock is present for the config entry.
custom_components/keymaster/providers/_base.py Defines the provider abstraction: BaseLockProvider, CodeSlot, and callback types for lock and connection events.
custom_components/keymaster/providers/zwave_js.py Implements the Z-Wave JS provider, including connection discovery, usercode operations, event subscription, and diagnostics.
custom_components/keymaster/providers/init.py Adds the provider registry/factory (PROVIDER_MAP, get_provider_class_for_lock, create_provider, is_platform_supported, get_supported_platforms).
custom_components/keymaster/providers/PROVIDERS.md Documents the provider architecture and gives guidance and examples for implementing additional providers.
custom_components/keymaster/lock.py Replaces direct Z-Wave JS node/device fields with a generic provider field on KeymasterLock.
custom_components/keymaster/helpers.py Removes Z-Wave JS–specific helpers and adds async_has_supported_provider; leaves timer and notification helpers in place (with new tests).
custom_components/keymaster/exceptions.py Replaces ZWaveIntegrationNotConfiguredError with generic ProviderNotConfiguredError and generalizes NotFoundError.
custom_components/keymaster/coordinator.py Refactors the coordinator to use provider APIs for connection, usercode fetch/update, and event handling, and to serialize/deserialize KeymasterLock instances without provider internals.
custom_components/keymaster/config_flow.py Extends _get_entities with a filter_func and filters lock choices to those with a supported provider via is_platform_supported.
custom_components/keymaster/binary_sensor.py Relies on the presence and capabilities of kmlock.provider (e.g., supports_connection_status) and handles missing locks via PlatformNotReady.

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

raman325 and others added 2 commits January 22, 2026 13:31
- Move sensor-based event detection (_handle_lock_state_change) to
  ZWaveJSLockProvider's subscribe_lock_events method
- Move LOCK_ACTIVITY_MAP and LOCK_STATE_MAP to providers/zwave_js.py
- Create providers/const.py for provider-specific constants
- Make get_activity_for_sensor_event opt-in (not in base class)
- Add _update_listeners call after provider connects to fix event
  subscription timing for restored locks
- Fix refresh condition to only query lock when slot has code but
  value is unknown (addresses Copilot review feedback)
- Remove unnecessary event_label initialization
- Update PROVIDERS.md to match actual PROVIDER_MAP pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
LockActivity is only used internally within the Z-Wave JS provider
for sensor event translation, so it belongs there rather than in
the base module.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@tykeal
Copy link
Contributor

tykeal commented Jan 28, 2026

@raman325 I've rolled this out onto one of my more complex systems (7 locks in play) and it's working flawlessly :)

I think the only thing that would make this better would be fixing the slot code reset button being added to the generated UI to fix #533 for @FutureTense

That could also be a separate PR and I would be willing to see this merge as is if @firstof9 agrees.

@firstof9
Copy link
Collaborator

I'm good with merging this.

Also simplify conditional logic for parent/child code slot dict generation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@tykeal
Copy link
Contributor

tykeal commented Jan 28, 2026

@raman325 thanks for the addition of the reset button. Looks good and works :)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@raman325 raman325 merged commit 6044f1d into FutureTense:beta Jan 28, 2026
6 checks passed
@raman325 raman325 deleted the feature/provider-abstraction branch January 28, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants