Skip to content

fix(solis): apply slot events immediately so Predbat's read-back sees them - #5247

Merged
springfall2008 merged 2 commits into
mainfrom
fix/solis-cloud-select-immediate
Sep 26, 2026
Merged

springfall2008 merged 2 commits into
mainfrom
fix/solis-cloud-select-immediate

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Problem

With Solis Cloud, Predbat sometimes logs that a discharge-time write failed, but the value then reaches the inverter anyway.

Predbat writes the slot time to select.predbat_solis_<sn>_discharge_slot1_start_time and polls the select for about 20 s (write_and_poll_sleep 2 s × INVERTER_MAX_RETRY 10). Since #4875, SolisAPI.select_event only queues the event. The queue is drained at the start of run(), and ComponentBase only calls run() once a minute (seconds % 60 == 0). So the select keeps its old value past Predbat's poll, and Predbat logs Trying to write … didn't complete. On the next cycle the queue drains, the handler updates the schedule, and run() writes the value to the inverter.

A second, smaller problem: read_and_write_cid reports a failure whenever the write call errors, even if the read-back it has just taken already shows the new value. That happens when the SolisCloud relay applies a write but its reply times out.

Changes

  • Slot events are applied immediately. Slot times (select), slot SoC and power (number) and slot enables (switch) of an already-discovered inverter now run their handler on the calling loop.
    • Those handlers only change the in-memory charge_discharge_time_windows and make no API calls, so the loop-affinity problem fix(solis): queue entity events for the component loop #4875 fixed doesn't apply. The cloud write still happens in run().
    • Everything that calls the cloud is still queued: storage mode, reserve and the inverter-wide settings. So are slot events that arrive before discovery.
  • The slot handlers publish only the slot controls. A new publish_slot_entities(inverter_sn) holds the slot loops, moved unchanged apart from indentation out of publish_entities(). Handlers call it instead of republishing every entity from the calling thread.
  • read_and_write_cid checks the read-back first. If the read-back matches, the write counts as a success, and the log notes the error reply.
  • Refused slot writes are rolled back (V2, pass 2). If the inverter refuses a slot enable or time write, restore_refused_slot_write sets the schedule back to what the inverter holds (cached_values). The select then shows that value, and Predbat writes again on its next execute.
    • It only rolls back while the schedule still holds the value it tried to write. Slot events are now applied from another loop, and a newer request that arrives during the write must win.
    • Pass 1 keeps retrying every minute: rolling back a failed "clear this slot" write would switch the slot back on. So do V1, which writes one combined register, and slot SoC/current, which the inverter can legitimately adjust.

Tests

Seven new tests in test_solis.py:

  • a slot select, number and switch are applied and published without draining the queue
  • a slot event before discovery is still queued (guards existing behaviour)
  • read_and_write_cid treats a landed-but-errored write as a success
  • a refused slot time and a refused slot enable are restored to the inverter's value
  • a newer request that arrives during a failed write is kept. I mutation-checked this one: removing the compare-and-set makes it fail.

test_rate_setpoints_do_not_drift_with_battery_voltage now calls publish_entities() after the slot handler. It had relied on the handler republishing the inverter-wide max_charge_power, which run() still does every minute.

  • ./run_all --test solis passes. It failed on the five behaviour tests before the fix, and passes on unmodified main.
  • ./run_all --quick passes
  • pre-commit passes

🤖 Generated with Claude Code

… them

Predbat writes a slot time to the Solis Cloud select and polls it for about
20s. The select callback only queued the event (#4875), and the queue is
drained at the start of run(), which runs once a minute. So the select kept
its old value, Predbat logged "Trying to write ... didn't complete", and the
value then went through on the next cycle.

- Slot times, SoCs, powers and enables of a discovered inverter now run
  their handler on the calling loop. Those handlers only change the
  in-memory schedule and make no API calls, so #4875's loop-affinity
  concern does not apply. Everything that calls the cloud is still queued,
  as are slot events that arrive before the inverter is discovered.
- The slot handlers publish only the slot controls, through a new
  publish_slot_entities() moved unchanged out of publish_entities(),
  instead of republishing every entity from the calling thread.
- read_and_write_cid() checks the read-back before reporting a failure:
  a write whose reply errored but whose value landed is a success.
- In V2 pass 2, a slot enable or time the inverter refuses is restored to
  the value the inverter holds, so the select shows it and Predbat writes
  again on its next execute. It is only restored while the schedule still
  holds the attempted value, so a newer request that arrives during the
  write is not overwritten. Pass 1, V1, and slot SoC/current keep their
  existing retry-every-minute behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 added the BOT_REVIEW Trigger an autotriage label Sep 26, 2026
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/solis.py Outdated
Comment thread apps/predbat/solis.py Outdated
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/tests/test_solis.py Outdated
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Sep 26, 2026
…ored writes

- Slot events now change the schedule through set_slot_schedule_field(),
  under a threading.Lock that write_time_windows_if_changed() also holds
  while it deep-copies the schedule and that restore_refused_slot_write()
  holds for its check-and-update. Slot events arrive on another loop, so
  the Solis loop is no longer the only writer. None of the locked sections
  await.
- Slot events are applied immediately only once the inverter's schedule has
  been decoded (schedules_loaded). The first run() discovers the inverter
  before it decodes, and the decode replaces the schedule wholesale, so an
  event applied in between was lost; until then it is queued.
- read_and_write_cid() takes the settled re-read on the write-error path
  too. A value that lands a moment after a timed-out reply is a success,
  instead of being rolled back and then overwritten with the old value.
- One route_event() helper replaces the three copies of the routing
  branch, and routing uses self.prefix instead of get_arg('prefix') on
  every event.
- restore_refused_slot_write() documents why the whole start-end pair is
  restored, and the remaining window: a start event queued before the
  first decode.
- The rollback tests now run the real read_and_write_cid() and read_cid()
  against a fake inverter with a stale cache. Removing read_cid()'s cache
  refresh makes them fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit 00a7030 into main Sep 26, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/solis-cloud-select-immediate branch September 26, 2026 15:39
springfall2008 added a commit that referenced this pull request Sep 27, 2026
…9-27

docs(debug-journal): fold in the 2026-09-27 queue slice (15 candidates); correct entries overtaken by the #5126/#5251/#5220/#5231/#5216/#5247/#5256 merge batch
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