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..fbdb944 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 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 1c36e69..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 | 4 | 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 | `43143-43190` | `43707-43791` | -| SolisCloud support | **Not mapped** — Modbus only | 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-4), 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` (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` (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 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