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:
- check cooldown
- write
self._last_event_time[entity_id] = now
- call
_format_state_change(...)
- 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.
Summary
HomeAssistantAdapterupdates its per-entity cooldown timestamp before it knows whether the event will actually produce a message. As a result, a no-opstate_changedevent (old_state == new_state) can consume the cooldown window and suppress the next real state change.Affected code
gateway/platforms/homeassistant.py:286-299gateway/platforms/homeassistant.py:321-335tests/gateway/test_homeassistant.py:325-385Why this is a bug
The code does:
self._last_event_time[entity_id] = now_format_state_change(...)_format_state_change()returnsNoneBut
_format_state_change()returnsNonewhen 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:Observed in a narrow repro run:
HA_CALLS 0HA_LAST_EVENT_SET TrueExpected 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.