Skip to content

Fix: Warn about very large Keymaster lock configurations#690

Merged
tykeal merged 1 commit into
FutureTense:mainfrom
tykeal:fix/679-large-lock-warning
Jul 11, 2026
Merged

Fix: Warn about very large Keymaster lock configurations#690
tykeal merged 1 commit into
FutureTense:mainfrom
tykeal:fix/679-large-lock-warning

Conversation

@tykeal

@tykeal tykeal commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Part of #670
Closes #679

What

Adds a Home Assistant Repairs warning when a single keymaster lock's projected entity count reaches 8000 entities (HA's per-dispatch event guard trips at 10,000). The repair is fixable — an acknowledgement: selecting Fix opens a confirm step ("here be dragons") that records that the user understands the risk and dismisses the warning. Two thresholds: 8000 raises the acknowledgeable warning; 9500 (critical, nearing the 10k guard) re-raises even after acknowledgement because the risk has materially worsened. It also auto-clears when the projected count drops back below 8000. Acknowledgement is persisted per-lock in a dedicated HA Store (survives restarts; no config-entry reload), preserved across disable/unload, and cleared on removal / below-threshold / critical re-alert.

Details

  • Projection helper computes a lock's entity count from its config (slots, advanced date range, advanced day-of-week, parent/child, door sensor, provider connection-status), matching the real platform generators.
  • The check runs at integration setup for all existing entries (swept once per startup) and on config/reconfigure; disabled/ignored/not-loaded entries have any stale issue cleared and are skipped.
  • Repair create/clear is wrapped so a failure can never block setup or a reconfigure flow.
  • No hard slot cap — large configs are still accepted; this only warns. The structural mitigation is the per-lock coordinator work (Refactor: Add runtime per-lock Keymaster coordinators #680Fix: Scope quick and debounced refresh to Keymaster lock entries #684).
  • Adds repairs.py (fixable acknowledgement fix flow) and a dedicated ack Store. Translations cover the issue text and the fix-flow confirm step. Unit tests cover projection (parent/child, DOW on/off, door-sensor sentinel), the 8000 boundary, >10000 warn-only/no-cap, setup-time stale clearing, disabled/unload ack preservation vs removal cleanup, the two-threshold + critical re-alert logic, and the fix-flow confirm/abort paths.

@tykeal tykeal requested a review from Copilot July 8, 2026 20:34
@github-actions github-actions Bot added the bugfix Fixes a bug label Jul 8, 2026
@codecov-commenter

codecov-commenter commented Jul 8, 2026

Copy link
Copy Markdown

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

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.09%. Comparing base (cdb4922) to head (5f74de4).
⚠️ Report is 195 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #690      +/-   ##
==========================================
+ Coverage   84.14%   93.09%   +8.94%     
==========================================
  Files          10       42      +32     
  Lines         801     5112    +4311     
  Branches        0       30      +30     
==========================================
+ Hits          674     4759    +4085     
- Misses        127      353     +226     
Flag Coverage Δ
python 92.97% <100.00%> (?)

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

☔ View full report in Codecov by Harness.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from 0f9439b to b0c9952 Compare July 8, 2026 20:41
@tykeal tykeal requested a review from Copilot July 8, 2026 20:41

This comment was marked as outdated.

@tykeal tykeal marked this pull request as draft July 8, 2026 21:08
@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from b0c9952 to d639f84 Compare July 8, 2026 23:01
@tykeal tykeal requested a review from Copilot July 8, 2026 23:05

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from d639f84 to 9d411b7 Compare July 8, 2026 23:16
@tykeal tykeal requested a review from Copilot July 8, 2026 23:17

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from 9d411b7 to bd2131f Compare July 8, 2026 23:30
@tykeal tykeal requested a review from Copilot July 8, 2026 23:32

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from bd2131f to db23564 Compare July 8, 2026 23:46
@tykeal tykeal requested a review from Copilot July 8, 2026 23:47

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from db23564 to b504878 Compare July 9, 2026 03:17
@tykeal tykeal requested a review from Copilot July 9, 2026 03:30

This comment was marked as outdated.

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from ae01807 to cf09d06 Compare July 9, 2026 20:01
@tykeal tykeal requested a review from Copilot July 9, 2026 20:04

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch 2 times, most recently from 4d43fad to 309be18 Compare July 9, 2026 20:19
@tykeal tykeal requested a review from Copilot July 9, 2026 20:23

This comment was marked as outdated.

@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from 309be18 to bd2c919 Compare July 9, 2026 21:54
@tykeal tykeal requested a review from Copilot July 9, 2026 21:58

This comment was marked as outdated.

Add projected entity-count repairs warnings for large single-lock configurations and auto-clear them when counts fall below the warning threshold.

Closes FutureTense#679

Part of FutureTense#670
@tykeal tykeal force-pushed the fix/679-large-lock-warning branch from bd2c919 to 5f74de4 Compare July 9, 2026 22:51
@tykeal tykeal requested a review from Copilot July 9, 2026 23:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@tykeal tykeal requested review from firstof9, raman325 and secondof9 and removed request for secondof9 July 9, 2026 23:22
@secondof9

Copy link
Copy Markdown

Line-by-line code review of PR #690: "Fix: Warn about very large Keymaster lock configurations"

Review Status: 🟡 APPROVED with minor improvement suggestions
Change Type: 🛠️ Refactor / 🐛 Bug Fix
Review Effort: 🟡 Medium


🔍 Architectural Walkthrough

custom_components/keymaster/helpers.py

projected_lock_entity_count() function:

  • Adds entity count projection for Keymaster locks based on config parameters
  • Computes binary_sensor, button, datetime, event, number, sensor, switch, text, and time counts
  • Uses _has_value() helper to detect parent/child relationships
  • Handles supports_connection_status parameter but does not use it in the body - this is a dead parameter

async_update_large_lock_repair_issue() function:

  • Creates or clears a large-lock repair issue based on projected entity count
  • Thresholds: WARNING at 8000 entities, CRITICAL at 9500 entities
  • Acknowledgement is persisted in a Home Assistant Store (survives restarts)
  • Clears acknowledgement if count drops below warning threshold or reaches critical

async_update_all_large_lock_repair_issues() function:

  • Sweeps all Keymaster config entries for repair issues
  • Skips disabled, ignored, or not-loaded entries
  • Runs once per startup via _LARGE_LOCK_REPAIR_SWEEP_DONE flag

async_load_large_lock_ack_store() function:

  • Loads acknowledgement store from disk (v1, "keymaster_large_lock_ack")
  • Returns cached data from hass.data if already loaded
  • Converts stored entries to dict[str, int]

async_get_large_lock_ack() and async_set_large_lock_ack() functions:

  • Read/write acknowledgement data
  • async_get_large_lock_ack() always calls async_load_large_lock_ack_store() which triggers disk read
  • ISSUE: This causes redundant disk I/O after the first load since hass.data already caches the data

async_clear_large_lock_ack() function:

  • Clears acknowledgement for an entry
  • Same issue as above - redundant disk read

_has_value() helper:

  • Returns True if value is not None, empty string, or NONE_TEXT
  • Used to detect parent/child relationships

_get_kmlock_for_entry() helper:

  • Returns lock object for a config entry if coordinator exists
  • Returns None if coordinator doesn't exist

_entry_is_generating_entities() helper:

  • Returns True if entry is active and expected to generate Keymaster entities
  • Checks: not disabled, not ignored, state in {LOADED, SETUP_IN_PROGRESS}

_supports_connection_status() helper:

  • Returns True if loaded provider creates connection-status binary sensor
  • Defaults to True if no provider loaded

custom_components/keymaster/__init__.py

async_setup() function:

  • Adds _LARGE_LOCK_REPAIR_SWEEP_DONE flag to prevent multiple sweep runs per startup

async_setup_entry() function:

  • Calls async_update_large_lock_repair_issue() for the current entry before the sweep
  • Uses supports_connection_status from loaded provider
  • Wraps repair update in try/except to prevent blocking setup
  • Calls async_update_all_large_lock_repair_issues() for sweep (also wrapped in try/except)
  • Sets _LARGE_LOCK_REPAIR_SWEEP_DONE flag to True after sweep

async_unload_entry() function:

  • Calls async_delete_large_lock_repair_issue() for disabled entries
  • Wrapped in try/except to prevent blocking unload

async_remove_entry() function:

  • Calls async_delete_large_lock_repair_issue() and async_clear_large_lock_ack() for removed entries
  • Wrapped in try/except to prevent blocking removal

custom_components/keymaster/config_flow.py

_start_config_flow() function:

  • Calls async_update_large_lock_repair_issue() after updating entry data during reconfigure
  • Wrapped in try/except to prevent blocking reconfigure flow

custom_components/keymaster/const.py

Constants added:

  • LARGE_LOCK_WARNING_THRESHOLD = 8000
  • LARGE_LOCK_CRITICAL_THRESHOLD = 9500
  • LARGE_LOCK_EVENT_GUARD_LIMIT = 10000
  • LARGE_LOCK_ACK_STORE_KEY = "keymaster_large_lock_ack"
  • LARGE_LOCK_ACK_STORE_VERSION = 1
  • LARGE_LOCK_ENTITY_WARNING_THRESHOLD = LARGE_LOCK_WARNING_THRESHOLD (duplicate)
  • LARGE_LOCK_REPAIR_TRANSLATION_KEY = "large_lock_configuration"
  • LARGE_LOCK_ACK_STORE = "large_lock_ack_store"
  • LARGE_LOCK_ACK_DATA = "large_lock_ack_data"
  • _LARGE_LOCK_REPAIR_ISSUE_ID_PREFIX = "large_lock_configuration"
  • NORMALIZED_TO_NONE_SENTINELS = (NONE_TEXT, "sensor.fake", "binary_sensor.fake")

custom_components/keymaster/repairs.py

LargeLockConfigurationRepairFlow class:

  • Acknowledges a very large Keymaster lock configuration
  • Two-step flow: init → confirm
  • Confirm step: records acknowledgement if count >= warning threshold
  • Aborts if entry_id is None or config entry not found

async_create_fix_flow() function:

  • Factory function to create repair flow
  • Takes issue_id and data, returns LargeLockConfigurationRepairFlow

custom_components/keymaster/strings.json and translations/en.json

Translations added:

  • Title: "Very large Keymaster lock configuration"
  • Fix flow title: "Acknowledge very large Keymaster lock: {lock_name}"
  • Description: Explains entity count, HA event limit, warning threshold, and risks

tests/test_large_lock_repairs.py

Test cases added:

  • test_projected_lock_entity_count_representative_configs() - validates projection for parent/child/DOW configs
  • test_projected_lock_entity_count_matches_normalized_door_sensor_values() - tests door sensor sentinel handling
  • test_large_lock_repair_issue_created_and_deleted() - tests issue creation and auto-clearing
  • test_large_lock_ack_store_helpers() - tests acknowledgement store read/write/clear
  • test_large_lock_fix_flow_factory_handles_missing_data() - tests factory with None data
  • test_large_lock_fix_flow_form_includes_issue_placeholders() - tests translation placeholders
  • test_large_lock_fix_flow_acknowledges_current_projected_count() - tests acknowledgement on confirm
  • test_large_lock_fix_flow_aborts_without_entry_id() - tests abort when entry_id is None
  • test_large_lock_fix_flow_aborts_for_removed_config_entry() - tests abort for removed entries
  • test_large_lock_fix_flow_does_not_ack_stale_below_warning_flow() - tests stale flow below threshold
  • test_acknowledged_warning_stays_dismissed_until_critical() - tests critical threshold override
  • test_large_lock_drop_below_warning_clears_ack_and_future_recross_warns() - tests clearing acknowledgement
  • test_large_lock_repair_issue_created_at_exact_threshold() - tests exact threshold boundary
  • test_remove_entry_clears_large_lock_repair_issue() - tests entry removal cleanup
  • test_remove_entry_cleanup_failure_does_not_skip_coordinator_teardown() - tests cleanup failure isolation
  • test_setup_repair_update_failure_does_not_block_setup() - tests setup failure isolation
  • test_setup_sweep_failure_does_not_block_setup() - tests sweep failure isolation
  • test_setup_uses_loaded_provider_connection_status() - tests provider connection status
  • test_setup_continues_when_add_lock_times_out() - tests timeout handling
  • test_unload_disabled_entry_clears_issue_but_preserves_ack() - tests unload behavior
  • test_unload_disabled_entry_delete_failure_does_not_block_unload() - tests unload failure isolation
  • test_setup_sweep_clears_stale_and_inactive_large_lock_issues() - tests sweep clearing
  • test_setup_sweep_entry_failure_logs_and_continues() - tests sweep entry failure isolation
  • test_supports_connection_status_uses_loaded_provider() - tests connection status helper
  • test_supports_connection_status_defaults_true_without_loaded_provider() - tests connection status default
  • test_setup_checks_all_existing_entries() - tests sweep for all entries
  • test_setup_sweeps_large_lock_repairs_once() - tests sweep once per startup
  • test_reconfigure_updates_large_lock_repair_issue() - tests reconfigure updates
  • test_reconfigure_repair_update_failure_does_not_block_flow() - tests reconfigure failure isolation
  • test_reconfigure_accepts_very_large_warn_only_config() - tests very large configs

⚠️ Warnings

  1. helpers.py projected_lock_entity_count() - Unused parameter supports_connection_status

    • The function accepts supports_connection_status parameter but never uses it in the body
    • This parameter should be removed to avoid confusion
    • Suggestion: Remove the supports_connection_status parameter from the function signature
  2. helpers.py async_get_large_lock_ack() and async_set_large_lock_ack() - Redundant disk I/O

    • Both functions call async_load_large_lock_ack_store() which always triggers store.async_load() from disk
    • Since hass.data already caches the data, this causes unnecessary disk reads
    • Suggestion: Add an early return in async_get_large_lock_ack() if data is cached in hass.data[DOMAIN][LARGE_LOCK_ACK_DATA]
  3. helpers.py async_update_large_lock_repair_issue() - Inconsistent parameter passing

    • The function accepts supports_connection_status as bool | None = None
    • If None, it calls _supports_connection_status() to determine the value
    • But _supports_connection_status() defaults to True if no provider loaded
    • This inconsistency could lead to different behavior depending on when the function is called

✅ Looks Good

  1. Architectural compliance: Proper separation of concerns - repair logic in helpers.py, integration hooks in __init__.py
  2. Defensive dict access: Uses config_entry.data.get(prop) in NORMALIZED_TO_NONE_SENTINELS instead of hardcoded sentinels
  3. Async safety: All repair operations wrapped in try/except to prevent blocking setup/reconfigure/unload
  4. Store persistence: Acknowledgement survives restarts and disable/unload, only cleared on removal or below-threshold
  5. Two-threshold system: Warning at 8000, critical at 9500 - critical re-raises even after acknowledgement
  6. One-time sweep: _LARGE_LOCK_REPAIR_SWEEP_DONE flag prevents multiple sweep runs per startup
  7. Comprehensive test coverage: 30+ test cases covering projection, thresholds, lifecycle, failure isolation
  8. Translations: Properly localized issue text and fix flow
  9. Reconfigure integration: Updates repair issue after config changes
  10. Stale issue clearing: Sweep clears issues for below-threshold and inactive entries

💡 Suggestions

  1. Remove unused supports_connection_status parameter from projected_lock_entity_count():

    • This parameter is accepted but never used in the function body
    • It should be removed to avoid confusion
  2. Optimize async_get_large_lock_ack():

    • Add early return if data is already cached in hass.data[DOMAIN][LARGE_LOCK_ACK_DATA]
    • This avoids redundant disk reads after the first load
  3. Consider caching supports_connection_status in async_update_large_lock_repair_issue():

    • Currently re-queries _supports_connection_status() each time
    • Could be cached in the config entry data or hass.data to avoid repeated coordinator lookups

Overall: Well-structured implementation with good architectural patterns. The code properly handles lifecycle events, prevents blocking on failures, and has comprehensive test coverage. The minor issues identified are low-risk and could be addressed in a follow-up PR.

🔒 Security: No secrets or sensitive data exposed
🧪 Testing: Comprehensive test suite with 30+ cases
📝 Documentation: Good docstrings and PR description

@tykeal

tykeal commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Quick clarifications on the three warnings, all against current head 5f74de4:

1. projected_lock_entity_count() supports_connection_status — this parameter is used, at helpers.py:111:

binary_sensor_count = slots + (1 if supports_connection_status else 0)

It conditionally counts the provider's connection-status binary sensor, mirroring what the provider actually creates. Removing it would make the projection undercount for providers that expose that sensor.

2. async_get_large_lock_ack() / async_set_large_lock_ack() disk I/O — the loader already short-circuits on the cache before any disk read, at helpers.py:222-223:

hass_data = hass.data.setdefault(DOMAIN, {})
if LARGE_LOCK_ACK_DATA in hass_data:
    return hass_data[LARGE_LOCK_ACK_DATA]

store.async_load() only runs on the first load; subsequent calls return the cached hass.data dict, so there is no redundant disk read.

3. async_update_large_lock_repair_issue() supports_connection_status resolution — the bool | None default is intentional. Callers that already know the provider status (setup) pass it explicitly; otherwise it resolves via _supports_connection_status(), whose True default is the conservative direction (counts the sensor → higher projection → more likely to warn). No behavioral inconsistency results.

@firstof9 firstof9 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks like it should get the job done properly.

@tykeal tykeal merged commit d6f0787 into FutureTense:main Jul 11, 2026
7 checks passed
@tykeal tykeal deleted the fix/679-large-lock-warning branch July 11, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix: Warn about very large Keymaster lock configurations

6 participants