Summary
State.notify_del() returns from the whole cleanup loop when it encounters a subscription that has already been removed.
This can happen when a trigger watches both an entity and one of its attributes, because both are registered under the same base-entity key.
The early return leaves later subscriptions attached to the old trigger queue. After the trigger task has stopped, state changes continue adding messages to that queue. Repeated reloads can therefore accumulate stale queues and duplicate notifications.
Changing the return to continue fixes the issue in my testing.
The defect is present in PyScript 2.0.1 and 2.1.0, and the same code is currently present on master.
Possibly related to #281, which also reports triggers surviving reloads, but this appears to be a separate concrete cleanup defect in State.notify_del().
Environment
Live reproduction and verification:
- PyScript 2.0.1
- Home Assistant Core 2026.7.3, container install
- Python 3.14
- State-triggered scripts importing a shared PyScript module
PyScript 2.1.0 and current master were also checked and contain the same notify_del() early-return logic.
Cause
For a watch set such as:
sensor.example
sensor.example.flag
sensor.other
notify_add() registers both sensor.example watches under the same sensor.example key.
During notify_del():
- The first entry removes the queue from
sensor.example.
- The attribute entry finds that queue already absent.
notify_del() executes return.
sensor.other is never cleaned up.
The stopped trigger queue therefore remains registered for sensor.other.
Reproduction
Using the real State class:
- Register an old queue and another active queue against the three watches above.
- Call
State.notify_del() with iteration order:
sensor.example
sensor.example.flag
sensor.other
- Check the notification registry.
With the current code, the old queue remains registered under sensor.other.
Sending a sensor.other update through State.update() also delivers a notification to that stopped queue.
Proposed fix
if state_var_name not in cls.notify or queue not in cls.notify[state_var_name]:
- return
+ continue
An absent subscription only means there is nothing to remove for that particular watch. It does not mean cleanup of the remaining watches is complete.
Observed result
I reproduced this across script reloads on 2.0.1:
|
Baseline |
Reload 1 |
Reload 2 |
| Registered queues, unpatched |
94 |
96 |
98 |
| Current trigger queues |
94 |
94 |
94 |
| Stale/unmatched queues |
0 |
2 |
4 |
| Registered queues, patched |
94 |
94 |
94 |
| Stale/unmatched queues, patched |
0 |
0 |
0 |
Before the patch, the retained queues accumulated more than 30,000 queued notifications during the test.
After changing return to continue, restarting Home Assistant and repeating two reloads, I saw no stale queues or accumulated stale messages.
I also ran 10 focused cleanup tests against the patched installed package; all 10 passed.
Notes
The failure depends on watch iteration order, so it will not occur for every trigger.
The proposed change is deliberately narrow. Queue limits or periodic cleanup would only mask the retained-subscription problem rather than fix it.
Summary
State.notify_del()returns from the whole cleanup loop when it encounters a subscription that has already been removed.This can happen when a trigger watches both an entity and one of its attributes, because both are registered under the same base-entity key.
The early return leaves later subscriptions attached to the old trigger queue. After the trigger task has stopped, state changes continue adding messages to that queue. Repeated reloads can therefore accumulate stale queues and duplicate notifications.
Changing the
returntocontinuefixes the issue in my testing.The defect is present in PyScript 2.0.1 and 2.1.0, and the same code is currently present on
master.Possibly related to #281, which also reports triggers surviving reloads, but this appears to be a separate concrete cleanup defect in
State.notify_del().Environment
Live reproduction and verification:
PyScript 2.1.0 and current
masterwere also checked and contain the samenotify_del()early-return logic.Cause
For a watch set such as:
notify_add()registers bothsensor.examplewatches under the samesensor.examplekey.During
notify_del():sensor.example.notify_del()executesreturn.sensor.otheris never cleaned up.The stopped trigger queue therefore remains registered for
sensor.other.Reproduction
Using the real
Stateclass:State.notify_del()with iteration order:With the current code, the old queue remains registered under
sensor.other.Sending a
sensor.otherupdate throughState.update()also delivers a notification to that stopped queue.Proposed fix
An absent subscription only means there is nothing to remove for that particular watch. It does not mean cleanup of the remaining watches is complete.
Observed result
I reproduced this across script reloads on 2.0.1:
Before the patch, the retained queues accumulated more than 30,000 queued notifications during the test.
After changing
returntocontinue, restarting Home Assistant and repeating two reloads, I saw no stale queues or accumulated stale messages.I also ran 10 focused cleanup tests against the patched installed package; all 10 passed.
Notes
The failure depends on watch iteration order, so it will not occur for every trigger.
The proposed change is deliberately narrow. Queue limits or periodic cleanup would only mask the retained-subscription problem rather than fix it.