Skip to content

@state_trigger with watch= never fires on force_update repeated-value events — no way to observe every state EVENT of an entity #864

Description

@skm7463

Environment

  • pyscript 2.1.0 (HACS)
  • Home Assistant Core 2026.8.2 (HAOS), Python 3.14

Summary

There is no @state_trigger form that fires on every state event of a
watched entity. Both the bare-name form and the
@state_trigger("True", watch=["sensor.x"]) form silently discard any
state_changed event whose value equals the previous value — which is
exactly what a sensor with force_update: true produces on repeated
readings. For an ADC-backed sensor holding steady, that can be a large
fraction of all events.

The docs for the expression + watch= form read as "evaluated on every
state event of the watched variables", which is what we relied on.

Mechanism (from the 2.1.0 source)

decorators/state.py (_cycle) routes each queued notify through:

if ident_any_values_changed(func_args, self.state_trig_ident_any):
    trig_ok = True
elif ident_values_changed(func_args, self.state_trig_ident):
    ...
else:
    continue   # <- repeated-value events end here

and trigger.py::ident_any_values_changed() requires a value change even
for the watch= (state_trig_ident_any) path:

for check_var in ident:
    if check_var == var_name and old_value != value:   # <- filters equal values
        return True

StateVal subclasses str, so two force_update events carrying the same
reading compare equal and the event is dropped before the trigger
expression is ever evaluated. Delivery is fine (the queue receives the
event); it is the change filter that discards it.

Real-world impact / how we found it

We used @state_trigger("True", watch=[SENSOR]) as a liveness heartbeat: an
ESPHome analog sensor publishes every 10s with force_update: true
(deliberately, so last_updated means "last heard"), and a watchdog
refreshed a hardware dead-man timer on each event. The trigger went silent
for >25s hundreds of times per night while the recorder had every row —
tripping the dead-man twice.

Recorder statistics for one night (12-bit ADC, physically stable input):

  • 3,600 state events in 10h (one per 10s — none missing in HA itself)
  • 34.5% of consecutive events carried an identical value
  • 309 runs of ≥3 identical values in a row (≥25s of trigger silence each)

which matched the measured trigger-side gap count (357) in the same window.
@time_trigger("period(now, 10s)") ran 4,027 ticks at 10.000s ±5ms
alongside — the discrepancy is specific to the state-trigger change filter.

Workaround (works, worth documenting)

Watch the .last_updated virtual attribute instead — it changes on every
force_update event, so the filter passes:

@state_trigger("True", watch=["sensor.my_adc.last_updated"])

Suggestion

Either of these would close the gap:

  1. Document that all @state_trigger forms are change-filtered, and
    document the .last_updated workaround for every-event use-cases; or
  2. Add an opt-in kwarg (e.g. @state_trigger(..., any_event=True)) that
    skips the old_value != value check for watched variables.

Happy to provide more detail or test a patch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions