Skip to content

fix(solis): queue entity events for the component loop - #4875

Open
mgazza wants to merge 1 commit into
mainfrom
fix/solis-callback-loop-affinity
Open

fix(solis): queue entity events for the component loop#4875
mgazza wants to merge 1 commit into
mainfrom
fix/solis-callback-loop-affinity

Conversation

@mgazza

@mgazza mgazza commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Solis entity callbacks perform real API reads and writes inline — solis.py:2650, :2880, :3056.

But those callbacks run on the HA component's event loop, not Solis's own. Standalone Predbat runs every component in its own thread with its own asyncio.run() loop (hass.py:217, :223), and the HA interface is one of those components — its websocket loop awaits trigger_callback() directly (ha.py:619).

The ClientSession is bound to the Solis loop. Issuing a request from the callback loop raises inside aiohttp, the handler catches it and returns normally, and the user is told a write succeeded that never reached the inverter.

Why Solis specifically

Ohme and Octopus already avoid this — their callbacks only append to a queue and do the work in their own run():

component callback does exposed?
Octopus appends a command (octopus.py:591), executed in run() (:641) no
Ohme queues; explicitly commented as stubs (ohme.py:689), drained in run() (:239) no
Solis performs the read/write inline yes

Change

Solis follows the same pattern. select_event, number_event and switch_event become thin stubs that queue; the existing bodies become *_event_handler; run() drains the queue on the Solis loop before polling. A failing handler is logged and the rest of the queue still drains — the queue is in memory only, so anything dropped is lost outright.

This deliberately does not touch session lifetime. One session stays on one loop, so connection reuse, socket lifetime and teardown are all unchanged, and injected test sessions keep working.

Tests

Existing event tests now call the handlers directly — that is what they were always exercising.

Two new tests cover the dispatch itself: that a callback queues rather than executing on the calling loop, and that one failing event does not strand the rest of the queue. MockSolisAPI gains queued_events, since it hand-rolls the state the real __init__ sets.

Verified with the repository harness — unit_test.py --test solis, which --quick skips:

  • Solis suite passes.
  • The two pre-existing Unclosed client session warnings and the multi_car_iog failure are present on a clean tree too, so neither is introduced here.
  • Mutation-checked: making select_event call its handler inline again fails the new test with callback executed API work on the calling loop.

Note

This supersedes #4874, which tried to solve it at the session layer by rebinding the ClientSession per loop. That was the wrong level — it broke injected test sessions, leaked a session per rebind, and destroyed connection reuse in exactly the alternating-loop case it targeted. Closing that in favour of this.

Solis entity callbacks perform real API reads and writes inline. They are invoked
from the HA component's loop (ha.py -> trigger_callback), not Solis's own, because
standalone Predbat runs every component in its own thread with its own
asyncio.run() loop (hass.py). The ClientSession belongs to the Solis loop, so
issuing a request from the callback loop raises inside aiohttp; the handler
swallows it and returns normally, and the user is told a write succeeded that
never reached the inverter.

Ohme and Octopus already avoid this: their callbacks only append to a queue and
the work happens in their own run(). Solis now does the same. select_event,
number_event and switch_event become thin stubs that queue, the existing bodies
become *_event_handler, and run() drains the queue on the Solis loop before
polling. A failing handler is logged and the rest of the queue still drains,
since the queue is in memory only and anything dropped is lost outright.

This keeps one session on one loop, so connection reuse, socket lifetime and
teardown are all unchanged — no per-loop session juggling, and nothing special
for injected test sessions.

Existing event tests now call the handlers directly, which is what they were
always exercising. Two new tests cover the dispatch itself: that a callback
queues rather than executing on the calling loop, and that one failing event does
not strand the rest. MockSolisAPI gains queued_events, as it hand-rolls the state
the real __init__ would set.

Verified with the repository harness (unit_test.py --test solis, which --quick
skips): the suite passes, and the two pre-existing unclosed-session warnings and
the multi_car_iog failure are present on a clean tree too. Mutation-checked —
making select_event call its handler inline again fails the new test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant