From 0b6be11d0da47154322c676dd56ebca7440348aa Mon Sep 17 00:00:00 2001 From: fboundy Date: Fri, 3 Jul 2026 19:57:27 +0100 Subject: [PATCH 1/2] Map HMI/DSP firmware version to SolisCloud; fix V1 TOU slot count in docs Adds SolisCloud support for the DSP/HMI firmware version sensors: inverterDetail's hmiVersionAll/dspmVersionAll fields are 4-hex-char strings equal to the raw Modbus register value (registers 33002/33001), confirmed against a live inverterDetail dump. Cloud-only/dual entries previously left these unavailable. Also corrects docs/source/tou.md: Time-Charging (V1) has 5 slots on Modbus (43143-43190), not 4, and documents SolisCloud control CID 103 as the (not yet implemented) path to V1 TOU cloud support, including the 3-slot/per-slot-current mismatch vs Modbus's 5-slot/global-current model. Full field layout and implementation plan recorded in WORKING_NOTES.md. Tests: 267 passed (3 new). Ruff clean. Co-Authored-By: Claude Fable 5 --- .../solisconnect/sensor_data/cloud_mapping.py | 14 ++++++++++++++ docs/source/protocols.md | 6 +++++- docs/source/tou.md | 12 ++++++------ tests/test_cloud_mapping.py | 18 ++++++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/custom_components/solisconnect/sensor_data/cloud_mapping.py b/custom_components/solisconnect/sensor_data/cloud_mapping.py index ab4f526..67f662a 100644 --- a/custom_components/solisconnect/sensor_data/cloud_mapping.py +++ b/custom_components/solisconnect/sensor_data/cloud_mapping.py @@ -143,6 +143,16 @@ def encode_cid_value(mapping: CloudCidMapping, msg: str) -> dict[int, int] | Non return _words_for_raw(raw, mapping.registers, mapping.data_type) +def _encode_hex_version(register: int) -> Callable[[str, dict], dict[int, int]]: + """Cloud firmware-version fields are 4-hex-char strings (e.g. "4B00"); the Modbus register + holds the same value as a raw 16-bit integer (register value == int(hex_string, 16)).""" + + def encode(value, detail: dict) -> dict[int, int]: + return {register: int(str(value), 16) & 0xFFFF} + + return encode + + def _encode_battery_power(value: float, detail: dict) -> dict[int, int]: """Signed cloud batteryPower (negative = charging) -> magnitude registers + direction flag. @@ -188,6 +198,10 @@ def _encode_battery_power(value: float, detail: dict) -> dict[int, int]: CloudFieldMapping(registers=(33093,), api_field="inverterTemperature", multiplier=0.1, data_type=DataType.S16), CloudFieldMapping(registers=(33094,), api_field="fac", multiplier=0.01), CloudFieldMapping(registers=(33095,), api_field="currentState", multiplier=0), + # Firmware versions (live-verified: inverterDetail "hmiVersionAll"/"dspmVersionAll" are 4-hex-char + # strings equal to the raw Modbus register value, e.g. hmiVersionAll "4B00" == register 33002 = 0x4B00) + CloudFieldMapping(registers=(33001,), api_field="dspmVersionAll", encoder=_encode_hex_version(33001)), + CloudFieldMapping(registers=(33002,), api_field="hmiVersionAll", encoder=_encode_hex_version(33002)), # Battery CloudFieldMapping(registers=(33133,), api_field="batteryVoltage", multiplier=0.1), CloudFieldMapping(registers=(33134,), api_field="storageBatteryCurrent", multiplier=0.1, data_type=DataType.S16), diff --git a/docs/source/protocols.md b/docs/source/protocols.md index c0c6936..5f21eef 100644 --- a/docs/source/protocols.md +++ b/docs/source/protocols.md @@ -33,7 +33,11 @@ Current cloud write support is limited to verified CIDs and CID families: Cloud writes read the previous CID value, send the control command, update the cache optimistically, and verify after SolisCloud catches up. If the cloud value does not match, SolisConnect restores the value reported by the device. -The TOU V2 timed-slot CID family (`5916-5987`) is mapped into SolisConnect's local `43707-43791` register model. The remaining caveat is feature detection: SolisCloud CID `6798` reports whether TOU V2 is enabled (`43605` / `0xAA55`), but SolisConnect does not yet use that gate to hide or disable the TOU V2 cloud mappings automatically. +The TOU V2 timed-slot CID family (`5916-5987`) is mapped into SolisConnect's local `43707-43791` register model. The remaining caveat is feature detection: SolisCloud CID `6798` reports whether TOU V2 is enabled (`43605` / `0xAA55`), but SolisConnect does not yet use that gate to hide or disable the TOU V2 cloud mappings automatically. As a related data point, TOU V2 hardware/firmware shipped from HMI version `FB00` onward (see below); this is not yet used as an alternate detection signal either. + +SolisCloud also reports the inverter's `HMI Version` (register `33001` "DSP Version" and `33002` "HMI Version" are populated from the `inverterDetail` fields `dspmVersionAll`/`hmiVersionAll`) so firmware version sensors work the same in cloud-only mode as they do over Modbus. + +**Not yet supported over cloud:** the older `Time-Charging` (TOU V1) schedule is Modbus-only. SolisCloud control CID `103` can drive 3 of its 5 time slots (with per-slot charge/discharge current, unlike Modbus's single global current), but that mapping has not been implemented yet — see the project's working notes for the full field layout and implementation plan. ## Operational notes diff --git a/docs/source/tou.md b/docs/source/tou.md index 1c36e69..f7e0db4 100644 --- a/docs/source/tou.md +++ b/docs/source/tou.md @@ -7,13 +7,13 @@ Solis hybrid inverters expose two independent, non-interacting sets of scheduled | | Time-Charging (V1) | Grid Time of Use V2 | | --- | --- | --- | | Enable switch | `switch.solis_time_of_use` — bit 1 of storage-mode register `43110` | `switch.solis_time_of_use_v2_switch` — bitfield register `43707`, one bit per slot | -| Slot count | 4 | 6 | +| Slot count | 5 | 6 | | Per-slot fields | Start/end hour+minute for charge and discharge | Start/end hour+minute, target SOC, cut-off voltage, battery current, per slot, for charge and discharge | | Charge/discharge current | One global pair (`43141`/`43142`), not per-slot | Per-slot (register offset `+1` in each slot block) | | Target SOC | Not available | Per-slot (register offset `+0` in each slot block) | | Cut-off voltage | Not available | Per-slot (register offset `+2` in each slot block) | -| Modbus registers | `43143-43190` | `43707-43791` | -| SolisCloud support | **Not mapped** — Modbus only | Mapped: control CIDs `5916-5987` | +| Modbus registers | `43141-43190` | `43707-43791` | +| SolisCloud support | **Planned, not yet implemented** — control CID `103` exists but only covers 3 of the 5 Modbus slots (see below) | Mapped: control CIDs `5916-5987` | | SolisCloud enablement gate | N/A | CID `6798` (`0xAA55`) reports whether the inverter supports V2; SolisConnect does not yet enforce this gate (see caveat below) | ## Time-Charging (V1) @@ -22,11 +22,11 @@ The older scheduling mechanism. One shared charge current and one shared dischar - Enable: bit 1 (`Time of Use`) of the storage-mode bitfield at register `43110` (`switch_sensors.py`). This bit requires either `Self-Use Mode` (bit 0) or `Feed-In Priority Mode` (bit 6) to be conflict-free with the other storage-mode bits. - Global settings: `Time-Charging Charge Current` (`43141`) and `Time-Charging Discharge Current` (`43142`), both in amps, editable as `number` entities. -- Per slot (1-4), each slot is a block of paired hour/minute registers exposed as HA `time` entities: +- Per slot (1-5), each slot is a block of paired hour/minute registers exposed as HA `time` entities: - Charge Start / Charge End - Discharge Start / Discharge End -- Register layout: slot 1 starts at `43143`, slot 2 at `43153`, slot 3 at `43163`, slot 4 at `43173` (each slot block is 8 registers of data plus a 2-register reserved gap). -- **No SolisCloud mapping exists for any V1 register.** V1 TOU can only be read or controlled over local Modbus. There is no plan tracked to add cloud CIDs for it, since Solis's own control-CID scheme (used by `mkuthan/solis-cloud-control`) is built around V2 semantics. +- Register layout: slot 1 starts at `43143`, slot 2 at `43153`, slot 3 at `43163`, slot 4 at `43173`, slot 5 at `43183` (each slot block is 8 registers of data plus a 2-register reserved gap). +- **SolisCloud mapping is planned but not yet implemented.** SolisCloud control CID `103` ("Charge and discharge Settings") can read/write V1 TOU: a single comma-separated string of 18 values covering **3 slots only**, with **per-slot** charge/discharge current — unlike Modbus, where current is one pair shared by all 5 slots. Field order per slot: `ChargeCurrent, DischargeCurrent, ChargeStart(HH:mm), ChargeEnd(HH:mm), DischargeStart(HH:mm), DischargeEnd(HH:mm)`. Source: `SolisCloud_control_api_command_list` workbook, "Hybrid Inverter" sheet. Slots 4 and 5 have no cloud equivalent and would stay Modbus-only even once this is implemented. See the project's working notes for the full implementation plan and the slot/current modeling compromise this mismatch requires. ## Grid Time of Use V2 diff --git a/tests/test_cloud_mapping.py b/tests/test_cloud_mapping.py index ff6c0db..426a2fa 100644 --- a/tests/test_cloud_mapping.py +++ b/tests/test_cloud_mapping.py @@ -150,6 +150,24 @@ def test_battery_power_encoder_discharging(): assert (words[33149] << 16 | words[33150]) == 2000 +def test_hmi_version_encoder_decodes_hex_string(): + mapping = CLOUD_INPUT_MAP["hmiVersionAll"] + words = mapping.encoder("4B00", {}) + assert words == {33002: 0x4B00} + + +def test_dsp_version_encoder_decodes_hex_string(): + mapping = CLOUD_INPUT_MAP["dspmVersionAll"] + words = mapping.encoder("4200", {}) + assert words == {33001: 0x4200} + + +def test_version_encoder_rejects_non_hex_value(): + mapping = CLOUD_INPUT_MAP["hmiVersionAll"] + with pytest.raises(ValueError): + mapping.encoder("not-hex", {}) + + def test_negative_value_for_unsigned_register_is_dropped(): mapping = CLOUD_INPUT_MAP["batteryCapacitySoc"] # U16 assert encode_engineering_value(mapping, -5, None) is None From d042255b14dfba006aad113b19a900a74cf954bb Mon Sep 17 00:00:00 2001 From: fboundy Date: Fri, 3 Jul 2026 20:06:10 +0100 Subject: [PATCH 2/2] Fix V1 TOU slot count: 3, not 5; flag Slot 4/5 as likely a bug The official RS485_MODBUS Hybrid Inverter protocol document only documents Time-Charging slot 1 (43141-43150); registers 43152-43249 are one contiguous Reserved block. SolisConnect's own hybrid_sensors.py extends this to slots 2-3 (43153, 43163) - undocumented in that PDF revision but independently confirmed by SolisCloud control CID 103, which covers exactly 3 slots - and then further to "slots 4-5" (43173, 43183), which fall inside the vendor's Reserved range with no confirmation from any source. Corrects the previous doc pass (which had already wrongly corrected "4 slots" to "5"); the real count is 3. Adds a "Known issue" section flagging slots 4-5 as an unresolved, likely-fictional feature pending the user's decision on remediation. Also updates the CID 103 implementation plan notes in WORKING_NOTES.md (26 registers = 2 global + 3 slots x 8, not affected by this correction) to reflect that, once implemented, cloud coverage of V1 TOU would actually be complete rather than partial. Co-Authored-By: Claude Fable 5 --- docs/source/protocols.md | 2 +- docs/source/tou.md | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/source/protocols.md b/docs/source/protocols.md index 5f21eef..fbdb944 100644 --- a/docs/source/protocols.md +++ b/docs/source/protocols.md @@ -37,7 +37,7 @@ The TOU V2 timed-slot CID family (`5916-5987`) is mapped into SolisConnect's loc SolisCloud also reports the inverter's `HMI Version` (register `33001` "DSP Version" and `33002` "HMI Version" are populated from the `inverterDetail` fields `dspmVersionAll`/`hmiVersionAll`) so firmware version sensors work the same in cloud-only mode as they do over Modbus. -**Not yet supported over cloud:** the older `Time-Charging` (TOU V1) schedule is Modbus-only. SolisCloud control CID `103` can drive 3 of its 5 time slots (with per-slot charge/discharge current, unlike Modbus's single global current), but that mapping has not been implemented yet — see the project's working notes for the full field layout and implementation plan. +**Not yet supported over cloud:** the older `Time-Charging` (TOU V1) schedule is Modbus-only. SolisCloud control CID `103` can drive all 3 of its time slots (with per-slot charge/discharge current, unlike Modbus's single global current), but that mapping has not been implemented yet — see the project's working notes for the full field layout and implementation plan. Note SolisConnect currently also defines a "Slot 4" and "Slot 5" for this schedule (registers `43173-43190`) that the official Modbus protocol document does not support — see `docs/source/tou.md` for details; treat those two slots as unverified. ## Operational notes diff --git a/docs/source/tou.md b/docs/source/tou.md index f7e0db4..ce60201 100644 --- a/docs/source/tou.md +++ b/docs/source/tou.md @@ -7,13 +7,13 @@ Solis hybrid inverters expose two independent, non-interacting sets of scheduled | | Time-Charging (V1) | Grid Time of Use V2 | | --- | --- | --- | | Enable switch | `switch.solis_time_of_use` — bit 1 of storage-mode register `43110` | `switch.solis_time_of_use_v2_switch` — bitfield register `43707`, one bit per slot | -| Slot count | 5 | 6 | +| Slot count | 3 (see caveat below) | 6 | | Per-slot fields | Start/end hour+minute for charge and discharge | Start/end hour+minute, target SOC, cut-off voltage, battery current, per slot, for charge and discharge | | Charge/discharge current | One global pair (`43141`/`43142`), not per-slot | Per-slot (register offset `+1` in each slot block) | | Target SOC | Not available | Per-slot (register offset `+0` in each slot block) | | Cut-off voltage | Not available | Per-slot (register offset `+2` in each slot block) | -| Modbus registers | `43141-43190` | `43707-43791` | -| SolisCloud support | **Planned, not yet implemented** — control CID `103` exists but only covers 3 of the 5 Modbus slots (see below) | Mapped: control CIDs `5916-5987` | +| Modbus registers | `43141-43170` (documented slots 1-3); SolisConnect also defines `43173-43190` as "slots 4-5" — **not backed by the vendor protocol, likely a bug**, see caveat | `43707-43791` | +| SolisCloud support | **Planned, not yet implemented** — control CID `103` exists and covers exactly the 3 vendor-documented slots (see below) | Mapped: control CIDs `5916-5987` | | SolisCloud enablement gate | N/A | CID `6798` (`0xAA55`) reports whether the inverter supports V2; SolisConnect does not yet enforce this gate (see caveat below) | ## Time-Charging (V1) @@ -22,11 +22,20 @@ The older scheduling mechanism. One shared charge current and one shared dischar - Enable: bit 1 (`Time of Use`) of the storage-mode bitfield at register `43110` (`switch_sensors.py`). This bit requires either `Self-Use Mode` (bit 0) or `Feed-In Priority Mode` (bit 6) to be conflict-free with the other storage-mode bits. - Global settings: `Time-Charging Charge Current` (`43141`) and `Time-Charging Discharge Current` (`43142`), both in amps, editable as `number` entities. -- Per slot (1-5), each slot is a block of paired hour/minute registers exposed as HA `time` entities: +- Per slot, each slot is a block of paired hour/minute registers exposed as HA `time` entities: - Charge Start / Charge End - Discharge Start / Discharge End -- Register layout: slot 1 starts at `43143`, slot 2 at `43153`, slot 3 at `43163`, slot 4 at `43173`, slot 5 at `43183` (each slot block is 8 registers of data plus a 2-register reserved gap). -- **SolisCloud mapping is planned but not yet implemented.** SolisCloud control CID `103` ("Charge and discharge Settings") can read/write V1 TOU: a single comma-separated string of 18 values covering **3 slots only**, with **per-slot** charge/discharge current — unlike Modbus, where current is one pair shared by all 5 slots. Field order per slot: `ChargeCurrent, DischargeCurrent, ChargeStart(HH:mm), ChargeEnd(HH:mm), DischargeStart(HH:mm), DischargeEnd(HH:mm)`. Source: `SolisCloud_control_api_command_list` workbook, "Hybrid Inverter" sheet. Slots 4 and 5 have no cloud equivalent and would stay Modbus-only even once this is implemented. See the project's working notes for the full implementation plan and the slot/current modeling compromise this mismatch requires. +- Register layout: slot 1 starts at `43143`, slot 2 at `43153`, slot 3 at `43163` (each slot block is 8 registers of data plus a 2-register reserved gap). +- **SolisCloud mapping is planned but not yet implemented.** SolisCloud control CID `103` ("Charge and discharge Settings") can read/write V1 TOU: a single comma-separated string of 18 values covering all 3 slots, with **per-slot** charge/discharge current — unlike Modbus, where current is one pair shared by all slots. Field order per slot: `ChargeCurrent, DischargeCurrent, ChargeStart(HH:mm), ChargeEnd(HH:mm), DischargeStart(HH:mm), DischargeEnd(HH:mm)`. Source: `SolisCloud_control_api_command_list` workbook, "Hybrid Inverter" sheet. See the project's working notes for the full implementation plan. + +### Known issue: "Slot 4" and "Slot 5" entities are likely fictional + +SolisConnect's `hybrid_sensors.py`/`time_sensors.py` also define a "Slot 4" (`43173-43180`) and "Slot 5" (`43183-43190`) for Time-Charging. This is **not supported by any known source**: + +- The official Solis RS485_MODBUS Hybrid Inverter protocol document defines only slot 1 (`43141-43150`) and marks the entire remaining range through `43249` as `Reserved` — no vendor documentation for a repeating 3-slot (or 5-slot) pattern exists in that document; slots 2-3 are themselves undocumented extensions that happen to work in practice. +- SolisCloud's control CID `103` independently confirms exactly 3 slots and no more. + +So registers `43173-43190` ("slots 4-5" in the current entity definitions) fall inside the vendor's documented `Reserved` block, with no independent source confirming they do anything real. These entities may be reading/writing to registers that don't correspond to Time-Charging at all. This is tracked as an open bug — see the project's working notes for status before relying on Slot 4/5 entities for anything. ## Grid Time of Use V2