Skip to content

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

Open
mgazza wants to merge 3 commits into
mainfrom
fix/solis-callback-loop-affinity
Open

fix(solis): queue entity events for the component loop#4875
mgazza wants to merge 3 commits 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

run() currently drains queued events before first-cycle session creation/inverter discovery, which can drop or mis-handle early queued writes during startup.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR changes the Solis component so Home Assistant entity callbacks (select_event, number_event, switch_event) no longer perform API I/O inline on the HA loop, and instead queue work for the Solis component loop to execute (avoiding cross-event-loop aiohttp.ClientSession usage).

Changes:

  • Add an in-memory queued_events queue to Solis, convert entity callbacks into stubs that enqueue handler calls, and drain the queue from run() on the Solis loop.
  • Rename the previous callback bodies into *_event_handler methods and update Solis tests to call handlers directly.
  • Add tests asserting callbacks queue (don’t execute on the calling loop) and that one failing queued event doesn’t block subsequent events.
File summaries
File Description
apps/predbat/solis.py Introduces queued event stubs + handler methods and drains queued events in run() to keep API work on the Solis loop.
apps/predbat/tests/test_solis.py Updates existing tests to target handlers and adds coverage for queuing/dispatch behavior.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/solis.py Outdated
@springfall2008 springfall2008 added the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 2, 2026
springfall2008 and others added 2 commits September 2, 2026 21:06
Copilot review: run() drained the event queue before the startup block created
the ClientSession and discovered inverters, so an event queued during startup
ran against session=None and an empty inverter list and was popped and lost.
Drain after the startup block instead; on a failed startup the queue is left
intact for the next attempt. Adds a test asserting an event queued before the
first run() drains only after discovery (mutation-checked).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@springfall2008 springfall2008 removed the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 2, 2026
@springfall2008
springfall2008 requested a lite review from Copilot September 3, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The current queue-draining loop can starve polling indefinitely if callbacks append events while draining, which is a reliability risk and can be fixed with a small, local change.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread apps/predbat/solis.py
Comment on lines +3322 to +3327
while self.queued_events:
handler, *args = self.queued_events.pop(0)
try:
await handler(*args)
except Exception as e:
self.log("Warn: Solis API: Event handler error: {}".format(e))
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.

3 participants