Skip to content

Home Assistant no-op state_changed events consume cooldown and suppress the next real change #12062

@NewTurn2017

Description

@NewTurn2017

Summary

HomeAssistantAdapter updates its per-entity cooldown timestamp before it knows whether the event will actually produce a message. As a result, a no-op state_changed event (old_state == new_state) can consume the cooldown window and suppress the next real state change.

Affected code

  • gateway/platforms/homeassistant.py:286-299
  • Related behavior in gateway/platforms/homeassistant.py:321-335
  • Existing tests: tests/gateway/test_homeassistant.py:325-385

Why this is a bug

The code does:

  1. check cooldown
  2. write self._last_event_time[entity_id] = now
  3. call _format_state_change(...)
  4. return early if _format_state_change() returns None

But _format_state_change() returns None when the state did not actually change. That means unchanged/no-op events still refresh the cooldown timestamp even though nothing is forwarded.

Minimal reproduction

Using the current code on main:

ha = HomeAssistantAdapter(PlatformConfig(enabled=True, token='t', extra={
    'url': 'http://x',
    'watch_all': True,
    'cooldown_seconds': 60,
}))
ha.handle_message = AsyncMock()

await ha._handle_ha_event({'data': {
    'entity_id': 'sensor.temp',
    'old_state': {'state': '20'},
    'new_state': {'state': '20', 'attributes': {}},
}})
await ha._handle_ha_event({'data': {
    'entity_id': 'sensor.temp',
    'old_state': {'state': '20'},
    'new_state': {'state': '21', 'attributes': {}},
}})

assert ha.handle_message.await_count == 1  # expected
# actual: 0

Observed in a narrow repro run:

  • HA_CALLS 0
  • HA_LAST_EVENT_SET True

Expected behavior

Only forwarded events should consume the cooldown window. A no-op event should be ignored without blocking the next real change.

Actual behavior

A no-op event writes _last_event_time, so the next real event within the cooldown window is dropped.

Suggested investigation

Move the cooldown timestamp update until after _format_state_change() returns a non-empty message, or otherwise ensure unchanged events do not advance cooldown state.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/gatewayGateway runner, session dispatch, deliverytype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions