Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions custom_components/solisconnect/sensor_data/cloud_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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),
Expand Down
6 changes: 5 additions & 1 deletion docs/source/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 15 additions & 6 deletions docs/source/tou.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
18 changes: 18 additions & 0 deletions tests/test_cloud_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading