I wanted a reliable way to pull live data from my Solis hybrid inverter into Node-RED. The goal was not just to view the same information shown in SolisCloud, but to make the data available locally for dashboards, automation, MQTT, Home Assistant / Homey, and eventually longer-term graphing in something like Grafana.
The SolisCloud app is useful, but it is not ideal for automation. It can lag by several minutes, and the values shown in the app are not always updated at the same moment. For automation, I wanted local data directly from the inverter/datalogger path.
This is what I ended up with;
The General Architecture
The working architecture is:
Solis Hybrid Inverter
↓
Solis S2-WL-ST Datalogger
↓
Local Modbus TCP
↓
Node-RED
↓
Dashboard / MQTT / Future Grafana
The key component is the Solis S2-WL-ST datalogger. Once local Modbus TCP access is enabled, Node-RED can poll the datalogger directly over the local network using TCP port 502 via node-red-contrib-modbus
Node-RED Flow Concept
The Node-RED flow is deliberately simple:
Inject → Modbus Flex Getter → Decode Function → Debug / Dashboard Gauges
The Inject node triggers the polling request once per minute. The Modbus Flex Getter sends the request to the Solis datalogger. The Function node then decodes the raw register values into useful named fields such as PV power, house load, battery SOC, grid import/export, and battery power.
Working Modbus Configuration
| Setting | Value |
|---|---|
| Protocol | Modbus TCP |
| Port | 502 |
| Node-RED package | node-red-contrib-modbus |
| Node used | modbus-flex-getter |
| TCP Type | DEFAULT |
| Unit ID | 1 |
| Timeout | 5000 ms |
| Polling interval | 60 seconds |
The Modbus Request
The working request embedded in the injector payload uses Function Code 4, not Function Code 3.
{
"value": 0,
"fc": 4,
"unitid": 1,
"address": 33057,
"quantity": 100
}
This was one of the most important discoveries. Attempts to use Holding Registers with FC3 failed with illegal data address errors. The useful live telemetry is exposed as Input Registers using FC4.
The Register Challenge
At first, some values appeared to make sense but were subtly wrong. The main reason was that several power values are not single 16-bit registers. They are 32-bit values spread across two adjacent registers.
For example, PV power is not just register 33057 or 33058 on its own. It is the combined 32-bit value from 33057-33058.
| Metric | Register(s) | Type |
|---|---|---|
| PV Power | 33057-33058 | U32 |
| Grid Power | 33130-33131 | S32 |
| Battery SOC | 33139 | U16 |
| House Load | 33147 | U16 |
| Backup Load | 33148 | U16 |
| Battery Power | 33149-33150 | S32 |
| Inverter Temperature | 33093 | S16, divided by 10 |
Signed Values and Direction
Some values also need to be interpreted as signed numbers. Grid power is a good example. In this setup:
Positive grid power = export Negative grid power = import
Battery power also needs signed interpretation. In testing, positive battery power matched battery discharge. Charging direction still needs to be confirmed under a clear charging condition, but the discharge readings matched the inverter screen very closely.
Polling Frequency
A practical issue appeared during testing. Polling every 10 seconds caused SolisCloud values to become unreliable. Load and battery values in the app appeared stale or incorrect.
Reducing the polling interval to 60 seconds fixed the issue. The local Modbus data remained useful, and SolisCloud returned to normal behaviour.
The S2-WL-ST appears to be happier when treated as a modest telemetry bridge rather than a high-frequency industrial Modbus server.
Dashboard Implementation
For the first version, I used the classic Node-RED Dashboard package. The Function node outputs named fields, and each dashboard gauge reads one of those fields.
| Gauge | Payload field |
|---|---|
| PV Power | msg.payload.pv_power_w |
| House Load | msg.payload.house_load_w |
| Grid Power | msg.payload.grid_power_w |
| Battery Power | msg.payload.battery_power_w |
| Battery SOC | msg.payload.battery_soc_pct |
Once the 32-bit register handling was fixed, the dashboard values matched the inverter screen very closely.
Graphing and Future Use
The current dashboard is intentionally simple. It is mainly a live view and validation tool. The next logical step is to publish the decoded values to MQTT and then store them in a time-series database such as InfluxDB for Grafana dashboards.
That would allow useful 10-minute and 1-hour averages for things like PV production, base load, grid import/export, and battery behaviour. Those averages will be more useful than raw instantaneous values for automation decisions.
Key Lessons
- Use FC4 Input Registers, not FC3 Holding Registers.
- Do not assume every value is a single register.
- Combine U32 and S32 register pairs correctly.
- Validate against the inverter screen, not just SolisCloud.
- Use a conservative polling interval. 60 seconds is stable here.
- Keep the flow simple: poll, decode, display, then expand later.
References
Here is the Node Red flow for reference;
[{"id":"90e781546219ac6b","type":"inject","name":"Run Every 1min","repeat":"60","once":true,"onceDelay":"1","payload":"{\"value\":0,\"fc\":4,\"unitid\":1,\"address\":33057,\"quantity\":100}","payloadType":"json","wires":[["a8f1c3f6352898fd"]]},{"id":"a8f1c3f6352898fd","type":"modbus-flex-getter","server":"68d219b6ae0242a0","keepMsgProperties":true,"x":380,"y":180,"wires":[["a145e485cb7ccc9b"],[]]},{"id":"a145e485cb7ccc9b","type":"function","name":"Solis Decode Live Telemetry","func":"const r=msg.payload;const start=msg.modbusRequest.address;function reg(n){const i=n-start;return i>=0&&i<r.length?r[i]:null;}function u32(h,l){const hi=reg(h),lo=reg(l);if(hi===null||lo===null)return null;return((hi<<16)>>>0)+lo;}function s32(h,l){const v=u32(h,l);if(v===null)return null;return v>0x7FFFFFFF?v-0x100000000:v;}function s16(n){const v=reg(n);if(v===null)return null;return v>32767?v-65536:v;}const pvPower=u32(33057,33058);const gridPower=s32(33130,33131);const gridPortPower=s32(33151,33152);const batteryPower=s32(33149,33150);msg.payload={timestamp:new Date().toISOString(),pv_power_w:pvPower,house_load_w:reg(33147),backup_load_w:reg(33148),grid_power_w:gridPower,grid_export_w:gridPower>0?gridPower:0,grid_import_w:gridPower<0?Math.abs(gridPower):0,grid_port_power_w:gridPortPower,battery_power_w:batteryPower,battery_discharge_w:batteryPower>0?batteryPower:0,battery_charge_w:batteryPower<0?Math.abs(batteryPower):0,battery_soc_pct:reg(33139),battery_soh_pct:reg(33140),battery_voltage_v:reg(33141)!==null?reg(33141)/100:null,battery_current_a:reg(33142)!==null?s16(33142)/10:null,meter_voltage_v:reg(33128)!==null?reg(33128)/10:null,meter_current_a:reg(33129)!==null?reg(33129)/10:null,inverter_temp_c:reg(33093)!==null?s16(33093)/10:null,operating_status_raw:reg(33121)};return msg;","wires":[["165ab03b0b13730c"]]},{"id":"165ab03b0b13730c","type":"debug","name":"debug payload","complete":"payload"},{"id":"68d219b6ae0242a0","type":"modbus-client","name":"Solis","clienttype":"tcp","tcpHost":"192.168.1.71","tcpPort":"502","tcpType":"DEFAULT","unit_id":"1","clientTimeout":"5000"}]


No comments:
Post a Comment