fix(solis): stop rewriting a CID 636 TOU bit the inverter refuses - #4710
Conversation
Some Solis inverters accept a CID 636 (Modbus 43110) storage mode write -- the control API answers with code 0 -- but keep the time-of-use bit clear. compute_solis_mode_value() then recomputes the same value from the cached read-back on the next cycle, so Predbat rewrote it once a minute indefinitely, warning each time (GH#4707). On firmware "4B and above" that is expected: the timed charge/discharge enable moved to the per-slot registers (CIDs 5916/5922, Modbus 43707, six slots rather than three) and every mode value carrying bit 1 -- 3, 35, 43, 51, 98 -- was dropped from the mode table, 35 becoming 33 and 98 becoming 96. Rather than predict that from the firmware generation, note_tou_bit_refused() learns it from the inverter. Only a read-back differing by exactly bit 1 counts, so a write dropped wholesale does not disable it. The verdict expires after SOLIS_TOU_BIT_REPROBE_HOURS and is retested, bounding the cost of a false positive -- GH#4239 already found the bit is only retained while a window is configured, and slot1_active reflects Predbat's intent rather than the inverter's state. Nothing is persisted, so a restart re-probes and a firmware update cannot be masked by a stale verdict. Also stop writing control registers to an inverter Solis Cloud reports as having no battery. automatic_config() already refused to enrol it, but only into a local list -- it stayed in self.inverter_sn, so the control loop and startup_reset_registers() kept driving the full write path against PV-only hardware every cycle. Predbat never plans for it, so its stale slot 1 is never cleared and it asks for a storage mode forever; that was 11 of the 12 CID 636 failures in the log this was diagnosed from. It is still polled and published, so its PV sensors keep working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new “TOU bit refused” path still emits a generic warning and includes an overly-certain explanatory log message that isn’t guaranteed by the code’s detection logic.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves Solis inverter control behaviour in Predbat by (1) learning when an inverter refuses the CID 636 “TOU” bit and avoiding repeated write/verify loops, and (2) skipping all control writes to PV-only (“No Battery”) inverters while continuing to poll/publish their telemetry.
Changes:
- Add a “TOU bit refused” learning/re-probe mechanism so storage mode writes stop retrying the same unsupported bit repeatedly.
- Gate all control writes (including startup register reset) behind a new
is_battery_inverter()check for inverters reportingbatteryType: "No Battery". - Add a set of targeted Solis tests covering the new storage-mode learning behaviour, re-probe expiry, and no-battery write suppression.
File summaries
| File | Description |
|---|---|
| apps/predbat/solis.py | Learns and re-probes TOU-bit refusal for CID 636; adds PV-only inverter write gating. |
| apps/predbat/tests/test_solis.py | Adds new unit tests for TOU-bit refusal learning/re-probe and “No Battery” inverter write skipping/logging. |
| .claude/skills/issue-triage/references/debug-journal.md | Updates internal debugging notes to reflect the new understanding/handling of CID 636 TOU-bit refusal and PV-only inverter behaviour. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # Write storage mode CID | ||
| success = await self.read_and_write_cid(inverter_sn, SOLIS_CID_STORAGE_MODE, mode_value, field_description=f"storage mode to {mode_value}") | ||
| if not success: | ||
| self.note_tou_bit_refused(inverter_sn, mode_value) | ||
| self.log(f"Warn: Solis API set storage mode encountered errors for {inverter_sn}") | ||
|
|
Source for the "bit 1 was dropped from the mode table" claimPermalinked at The same register, in the two firmware plugins that project ships.
register=43110,
option_dict={
1: "Self-Use - No Grid Charging",
3: "Timed Charge/Discharge - No Grid Charging",
17: "Backup/Reserve - No Grid Charging",
33: "Self-Use - No Timed Charge/Discharge",
35: "Self-Use",
37: "Off-Grid Mode",
41: "Battery Awaken",
43: "Battery Awaken + Timed Charge/Discharge",
49: "Backup/Reserve - No Timed Charge/Discharge",
51: "Backup/Reserve",
64: "Feed-in priority - No Grid Charging",
96: "Feed-in priority - No Timed Charge/Discharge",
98: "Feed-in priority",
},
register=43110,
option_dict={
1: "Self-Use - No Grid Charging",
17: "Backup/Reserve - No Grid Charging",
33: "Self-Use",
37: "Off-Grid Mode",
41: "Battery Awaken",
49: "Backup/Reserve",
64: "Feed-in priority - No Grid Charging",
96: "Feed-in priority",
},Removed: 3, 35, 43, 51, 98 — exactly the values with bit 1 set. The names then slide down onto the bit-1-clear values: That is both values from the issue:
The read-only mirrors of the register carry the same two tables: Where the enable moved to
name="Timed Charge Discharge On Off",
key="timed_charge_discharge_on_off",
register=43707,and L2060-2078 drives it one bit per slot: name="Timed Charge Slot 1 Enable",
register=43707,
register_bit=0,
...
name="Timed Charge Slot 2 Enable",
register=43707,
register_bit=1,
Provenance
Field reports of the same symptom on the newer hardware: Two caveats
Neither caveat affects this PR: |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Fixes #4707.
The problem
Some Solis inverters accept a CID 636 (Modbus 43110) storage mode write — the control API answers with
code 0— but keep the time-of-use bit clear.compute_solis_mode_value()starts from the cached value, the failed verify read writes the read-back into that cache, and the next cycle recomputes the same value and writes it again. Once a minute, indefinitely, with a warning each time:Why the bit is refused
On firmware "4B and above" the timed charge/discharge enable moved out of CID 636 and into the per-slot registers —
SOLIS_CID_CHARGE_ENABLE_BASE/..._DISCHARGE_ENABLE_BASE, Modbus 43707, six slots rather than three — and every mode value carrying bit 1 (3, 35, 43, 51, 98) was dropped from the mode table at the same time. The modes shift down: 35 becomes 33, 98 becomes 96, 51 becomes 49. That accounts for both values in the issue, including179 → 177(51 + 128→49 + 128, bit 7 being an independent modifier).Corroborated by
wills106/homeassistant-solax-modbus2024.12.1 ("Simplified Energy Control Switch options", "6 charge and discharge slots rather than 3") and its separateplugin_solis_fb00.py, whose 43110 option table drops exactly the bit-1 values.Why this does not gate on the firmware generation
is_tou_v2_mode()is not a firmware read — it checks CID 6798 for 43605 (0xAA55). The alignment with 4B is an inference, and the failure would be asymmetric: if a V2-marked inverter does honour bit 1, sending 33 means "Self-Use - No Timed Charge/Discharge" on the older table, silently disabling timed charging. Inverter B in the issue is also on the V1 path with the same signature, which a V2 gate would not touch.So
note_tou_bit_refused()learns it from the inverter instead. Only a read-back differing by exactly bit 1 counts — a write dropped wholesale says nothing about that bit and must not disable it. The verdict expires afterSOLIS_TOU_BIT_REPROBE_HOURS(8) and is retested, which bounds the cost of a false positive: #4239 already found the bit is only retained while a window is configured, andslot1_activereflects Predbat's intent rather than the inverter's state, so one probe at one moment can mislead. Nothing is persisted, so a restart re-probes and a firmware update cannot be masked by a stale verdict.Net effect: one write and one warning per inverter per 8 hours, instead of one per minute.
Second fix: no-battery inverters
automatic_config()already refused to enrol an inverter reportingbatteryType 'No Battery', but only into a local list. It stayed inself.inverter_sn, so the control loop andstartup_reset_registers()kept driving the full write path against PV-only hardware every cycle. Predbat never plans for it, so its stale slot 1 is never cleared,slot1_activestays true and it asks for a storage mode forever.In the log this was diagnosed from, that was 11 of the 12 CID 636 failures.
is_battery_inverter()now gates every control write. The inverter is still polled and published, so its PV sensors keep working.Tests
Nine new tests in
test_solis.py, all written before the code:test_compute_solis_mode_value_can_drop_the_tou_bit— value table including179 → 177; default behaviour pinnedtest_storage_mode_stops_asking_for_a_refused_tou_bit— four cycles produce exactly one write and one warningtest_storage_mode_keeps_the_tou_bit_when_the_inverter_accepts_it— no false positivetest_storage_mode_does_not_learn_from_an_unrelated_verify_failure— a wholesale refusal retries with the bittest_refused_tou_bit_is_retested_after_the_reprobe_window— quiet at 7h59m, one re-probe at 8htest_refused_tou_bit_is_not_persisted_across_a_restarttest_run_skips_control_writes_for_no_battery_invertertest_run_skips_startup_register_reset_for_no_battery_invertertest_run_logs_why_a_no_battery_inverter_is_not_controlledThe storage-mode tests drive the real
read_and_write_cid()path via a stub inverter, so the write → verify → cache round-trip is genuinely exercised.Full suite green (
./run_all, exit 0).pre-commitclean.Not in scope
switch.<prefix>_solis_<sn>_time_of_usestill writes bit 1 directly when a user toggles it, so on an affected inverter that switch cannot work. Worth a separate decision: hide it once a refusal is known, or let it fail visibly.read_and_write_cid()verifies with no settle time. The timing evidence says lag is not what is happening here, but the code cannot distinguish refusal from lag in principle.solis.py, which it does not use today. The re-probe window covers the same ground without it.Still open
Whether the cleared bit actually breaks slot execution. On 4B firmware 33 is "Self-Use" and the slots are gated by CIDs 5916/5922, which verify fine — so this may be log noise rather than a dead plan. Worth confirming against a real charge slot.
🤖 Generated with Claude Code