Monday, 11 May 2026

Importing Solis Inverter Data to Node Red

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: