SAJ Solar Inverter and Home-Assistant
How I used an ESP32 and Max3232 board to read out the output from a SAJ solar inverter and integrated it in the Energy Dashboard of Home-Assistant.
Last year I moved to a new house with solar panels and a digital electricity meter. The digital meter registers the imported energy from the grid and the excess solar power not consumed internally (I don't have a home battery). For the digital meter (and gas), I use a P1 sensor from HomeWizard which I highly recommend due to their Home-Assistant integration friendliness. However, I didn't have a convenient way to view the total amount of produced solar power and how much is self-consumed.
Specifically, the house has a SAJ Sununo Plus 4K-M inverter with only an RS232 serial port for data logging.
Searching online, I found that 2 to 3 plugs exist to enable the inverter to be accessible from the network. However, the LAN/Wifi adapters are only available for purchase by companies, and even if you kindly ask a friend who owns a business, the cost is exorbitant (around €75).
Looking at the manual and resources of SAJ, I found out that they use Modbus as a communication protocol.
I hooked up my laptop through a USB to RS232 cable and found a tool called mbpoll which can query a Modbus device.
Great! If I can read out the values from my laptop, surely I can do so similarly and much more cheaply using an ESP.
I ordered the following from Amazon:
- AZDelivery ESP32 (~€11)
- StarTech DB9 RS232 null modem serial adapter (~€4.50)
- DIGITUS D-Sub 9 Gender changer - Adapter (~€2)
- MAX3232 RS232 to TTL 3V-5V Serial Port Module (~€8.30)
Which totals around €26. Way better than €75 (plus it's a fun project).
This was my first time using ESPHome and was very happy with how easy it was to implement what I wanted.
I created a file called solar-inverter.yaml with the following code after implementing most of SAJ's Modbus protocol.
substitutions:
name: solar-inverter
device_description: "Monitor and control a SAJ Sununo Plus 4k-m inverter via RS232"
tx_pin: TX
rx_pin: RX
api_key: ""
ota_password: ""
wifi_ssid: ""
wifi_password: ""
wifi_fallback_password: ""
esphome:
name: ${name}
comment: ${device_description}
esp32:
board: az-delivery-devkit-v4
framework:
type: arduino
web_server:
port: 80
# Enable logging
logger:
level: debug # makes uart stream available in esphome logstream
baud_rate: 0 # disable logging over uart
# Enable Home Assistant API
api:
encryption:
key: ${api_key}
ota:
password: ${ota_password}
wifi:
ssid: ${wifi_ssid}
password: ${wifi_password}
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Solar-Inverter Fallback Hotspot"
password: ${wifi_fallback_password}
captive_portal:
time:
- platform: sntp
timezone: Europe/Brussels
uart:
id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 115200
stop_bits: 1
data_bits: 8
parity: NONE
#debug:
# direction: BOTH
modbus:
id: modbus0
uart_id: uart_bus
modbus_controller:
- id: saj
## the modbus device addr
address: 0x1
modbus_id: modbus0
setup_priority: -10
update_interval: 5s
text_sensor:
- platform: modbus_controller
modbus_controller_id: saj
name: "Device Type"
id: solar_inverter_device_type
address: 0x8f00
register_type: holding
skip_updates: 5
raw_encode: HEXBYTES
lambda: |-
uint16_t value = modbus_controller::word_from_hex_str(x, 0);
switch (value) {
case 17: return std::string("Sununo Plus inverter one MPPT");
case 18: return std::string("Sununo Plus inverter dual MPPT");
case 33: return std::string("Suntrio Plus inverter");
default: return std::string("Unkown");
}
return x;
- platform: modbus_controller
modbus_controller_id: saj
name: "Working Mode"
id: mpv_mode
address: 0x0100
register_type: holding
raw_encode: HEXBYTES
lambda: |-
uint16_t value = modbus_controller::word_from_hex_str(x, 0);
switch (value) {
case 1: return std::string("Wait");
case 2: return std::string("Normal");
case 3: return std::string("Fault");
case 4: return std::string("Update");
default: return std::string("Unkown");
}
return x;
sensor:
- platform: modbus_controller
modbus_controller_id: saj
name: "Solar Inverter PV1 voltage"
id: pv1volt
address: 0x0107
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "PV1 total current"
id: solar_inverter_pv1curr
address: 0x0108
register_type: holding
value_type: U_WORD
device_class: "current"
unit_of_measurement: "A"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "PV1 power"
id: solar_inverter_pv1power
address: 0x0109
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "PV2 voltage"
id: solar_inverter_pv2volt
address: 0x010A
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "PV2 total current"
id: solar_inverter_pv2curr
address: 0x010B
register_type: holding
value_type: U_WORD
device_class: "current"
unit_of_measurement: "A"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "PV2 power"
id: solar_inverter_pv2power
address: 0x010C
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "PV3 voltage"
id: solar_inverter_pv3volt
address: 0x010D
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "PV3 total current"
id: solar_inverter_pv3curr
address: 0x010E
register_type: holding
value_type: U_WORD
device_class: "current"
unit_of_measurement: "A"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "PV3 power"
id: solar_inverter_pv3power
address: 0x010F
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "BUS voltage"
id: solar_inverter_busvolt
address: 0x0110
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "Inverter Temperature"
id: solar_inverter_invtempc
address: 0x0111
register_type: holding
value_type: S_WORD
device_class: "temperature"
unit_of_measurement: "°C"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "Ground-fault circuit interrupter"
id: solar_inverter_gfci
address: 0x0112
register_type: holding
value_type: S_WORD
device_class: "current"
unit_of_measurement: "mA"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "Active Power of inverter total output"
id: solar_inverter_power
address: 0x0113
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "Reactive Power of inverter total output"
id: solar_inverter_qpower
address: 0x0114
register_type: holding
value_type: S_WORD
device_class: "reactive_power"
unit_of_measurement: "var"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "Total power factor of inverter"
id: solar_inverter_pf
address: 0x0115
register_type: holding
value_type: U_WORD
device_class: "power_factor"
state_class: "measurement"
accuracy_decimals: 3
filters:
- multiply: 0.001
- platform: modbus_controller
modbus_controller_id: saj
name: "L1 voltage"
id: solar_inverter_l1volt
address: 0x0116
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "L1 current"
id: solar_inverter_l1curr
address: 0x0117
register_type: holding
value_type: U_WORD
device_class: "current"
unit_of_measurement: "A"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "L1 frequency"
id: solar_inverter_l1freq
address: 0x0118
register_type: holding
value_type: U_WORD
device_class: "frequency"
unit_of_measurement: "Hz"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "L1 DC"
id: solar_inverter_l1dci
address: 0x0119
register_type: holding
value_type: S_WORD
device_class: "current"
unit_of_measurement: "mA"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "L1 power"
id: solar_inverter_l1power
address: 0x011A
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "L1 power factor"
id: solar_inverter_l1pf
address: 0x011B
register_type: holding
value_type: S_WORD
device_class: "power_factor"
state_class: "measurement"
accuracy_decimals: 3
filters:
- multiply: 0.001
- platform: modbus_controller
modbus_controller_id: saj
name: "L2 voltage"
id: solar_inverter_l2volt
address: 0x011C
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "L2 current"
id: solar_inverter_l2curr
address: 0x011D
register_type: holding
value_type: U_WORD
device_class: "current"
unit_of_measurement: "A"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "L2 frequency"
id: solar_inverter_l2freq
address: 0x011E
register_type: holding
value_type: U_WORD
device_class: "frequency"
unit_of_measurement: "Hz"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "L2 DC"
id: solar_inverter_l2dci
address: 0x011F
register_type: holding
value_type: S_WORD
device_class: "current"
unit_of_measurement: "mA"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "L2 power"
id: solar_inverter_l2power
address: 0x0120
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "L2 power factor"
id: solar_inverter_l2pf
address: 0x0121
register_type: holding
value_type: S_WORD
device_class: "power_factor"
state_class: "measurement"
accuracy_decimals: 3
filters:
- multiply: 0.001
- platform: modbus_controller
modbus_controller_id: saj
name: "L3 voltage"
id: solar_inverter_l3volt
address: 0x0122
register_type: holding
value_type: U_WORD
device_class: "voltage"
unit_of_measurement: "V"
state_class: "measurement"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "L3 current"
id: solar_inverter_l3curr
address: 0x0123
register_type: holding
value_type: U_WORD
device_class: "current"
unit_of_measurement: "A"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "L3 frequency"
id: solar_inverter_l3freq
address: 0x0124
register_type: holding
value_type: U_WORD
device_class: "frequency"
unit_of_measurement: "Hz"
state_class: "measurement"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "L3 DC"
id: solar_inverter_l3dci
address: 0x0125
register_type: holding
value_type: S_WORD
device_class: "current"
unit_of_measurement: "mA"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "L3 power"
id: solar_inverter_l3power
address: 0x0126
register_type: holding
value_type: U_WORD
device_class: "power"
unit_of_measurement: "W"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "L3 power factor"
id: solar_inverter_l3pf
address: 0x0127
register_type: holding
value_type: S_WORD
device_class: "power_factor"
state_class: "measurement"
accuracy_decimals: 3
filters:
- multiply: 0.001
- platform: modbus_controller
modbus_controller_id: saj
name: "PV1+_ISO"
id: solar_inverter_iso1
address: 0x0128
register_type: holding
value_type: U_WORD
unit_of_measurement: "kΩ"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "PV2+_ISO"
id: solar_inverter_iso2
address: 0x0129
register_type: holding
value_type: U_WORD
unit_of_measurement: "kΩ"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "PV3+_ISO"
id: solar_inverter_iso3
address: 0x012A
register_type: holding
value_type: U_WORD
unit_of_measurement: "kΩ"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "PV4+_ISO"
id: solar_inverter_iso4
address: 0x012B
register_type: holding
value_type: U_WORD
unit_of_measurement: "kΩ"
state_class: "measurement"
- platform: modbus_controller
modbus_controller_id: saj
name: "Power generation on current day"
id: solar_inverter_today_energy
address: 0x012C
register_type: holding
value_type: U_WORD
device_class: "energy"
unit_of_measurement: "kWh"
state_class: "total_increasing"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "Power generation in the current month"
id: solar_inverter_month_energy
address: 0x012D
register_type: holding
value_type: U_DWORD
device_class: "energy"
unit_of_measurement: "kWh"
state_class: "total_increasing"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "Power generation in the current year"
id: solar_inverter_year_energy
address: 0x012F
register_type: holding
value_type: U_DWORD
device_class: "energy"
unit_of_measurement: "kWh"
state_class: "total_increasing"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "Total power generation"
id: solar_inverter_total_energy
address: 0x0131
register_type: holding
value_type: U_DWORD
device_class: "energy"
unit_of_measurement: "kWh"
state_class: "total"
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: saj
name: "Daily working hours"
id: solar_inverter_today_hour
address: 0x0133
register_type: holding
value_type: U_WORD
device_class: "duration"
unit_of_measurement: "h"
state_class: "total_increasing"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "Total working hours"
id: solar_inverter_total_hour
address: 0x0134
register_type: holding
value_type: U_DWORD
device_class: "duration"
unit_of_measurement: "h"
state_class: "total"
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
modbus_controller_id: saj
name: "Error Count"
id: solar_inverter_error_count
address: 0x0136
register_type: holding
value_type: U_WORD
state_class: "measurement"
number:
- platform: modbus_controller
modbus_controller_id: saj
name: "Limit Power"
id: solar_inverter_powerlimit
address: 0x801F
register_type: holding
mode: slider
use_write_multiple: true
step: 1
min_value: 0
max_value: 100
multiply: 10.0
I used the ESPHome commands by running it inside a Docker container for the installation and the programming:
docker run --rm -v "${PWD}":/config -it ghcr.io/esphome/esphome wizard solar-inverter.yaml
docker run --rm --privileged -v "${PWD}":/config --device=/dev/ttyUSB0 -it ghcr.io/esphome/esphome run solar-inverter.yaml
And voila, really nice looking output:
Depending on how many phases and panel groups you have, values for L2, L3, P2, P3 might look strange or are maxed out.
Since ESPHome is integrated really well into Home-Assistant, the device is very easy to add:
I can then use the Total Power Generation metric which is an incrementing value useful for utility type of measurements.