Skip to content

Fix: Skip unchanged Keymaster entity state writes #677

Description

@tykeal

Part of #670

Summary

Reduce redundant state-event volume after Phase A by suppressing entity writes when
availability/value/attributes are unchanged. This is one sub-issue rather than nine platform
sub-issues so the base helper contract and per-platform signatures stay coherent.

Scope reframing (2026-07-07, per @raman325): This is a performance optimization, not part of the guard fix. The guard is fixed by Phase A deferral (#676) + per-lock sharding (#680#684). Verified in HA 2026.7.0 core.py async_set_internal: an unchanged async_write_ha_state() still fires EVENT_STATE_REPORTED (HA only skips the State allocation), so HA's built-in dedup does not prevent the guard and does not make the write free of an event fire. The only thing this task adds above HA is deciding whether to call async_write_ha_state() at all. Do not reimplement HA's state/attribute comparison; gate the write on the entity's own derived value/availability and let HA dedupe underneath. Because HA already makes unchanged writes cheap, treat this as lower priority and measure the benefit rather than assume it.

Scope / changes

  • Add a base helper in entity.py near KeymasterEntity (entity.py:28-72) that records the
    last published state signature and calls async_write_ha_state() only when changed, with a
    force=True escape hatch.
  • Convert platform coordinator handlers that currently write unconditionally:
    binary_sensor.py:105-108, button.py:85-101, datetime.py:91-120, event.py:137-152,
    number.py:124-154, sensor.py:120-136 and sensor.py:167-204, switch.py:188-278,
    text.py:100-136, and time.py:96-142.
  • Preserve actual event entity writes for real lock/reset events at event.py:99-134 by using
    force=True or bypassing the dedupe helper.
  • Preserve optimistic immediate local feedback where it already exists, such as
    number.py:187.
  • Add per-platform tests in existing files: tests/test_switch.py, tests/test_text.py,
    tests/test_sensor.py, tests/test_number.py, tests/test_datetime.py,
    tests/test_time.py, tests/test_button.py, tests/test_binary_sensor.py, and
    tests/test_event.py.

Plan reference

§5.9, §7.4, §8 PR 3; decisions 9 and 10.

Dependencies

Depends on: #676

Acceptance criteria

  • A second identical coordinator update does not call async_write_ha_state() for representative
    entities from every platform.
  • Changing the relevant exposed value writes exactly once.
  • Event entity actual event/reset handlers still write even when availability is unchanged.
  • Targeted validation: pytest tests/test_switch.py tests/test_text.py tests/test_sensor.py tests/test_number.py tests/test_datetime.py tests/test_time.py tests/test_event.py tests/test_binary_sensor.py tests/test_button.py.
  • Broader validation: ruff check custom_components/ tests/, ruff format --check custom_components/ tests/, mypy custom_components/keymaster/.

Risk / notes

Watch for signatures containing mutable dicts; normalize attributes into stable
sorted tuples. Event entities are the main false-dedupe risk.

Size

L

Availability-only fan-out note (2026-07-07)

Change-detection also neutralizes availability-only entities (buttons and code-slot events): they
should no-op unless availability flips. always_update=False is inert on the current global
coordinator because updates use a fresh dict(self.kmlocks) after in-place mutation, so
entity-level change-detection is the real lever until per-lock coordinators pass stable data. Full
detachment of availability-only entities is tracked separately in #686.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions