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.

SAJ Solar Inverter and 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).

SAJ Sununo Plus 4K-M
SAJ Sununo Plus 4K-M Specs

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.

mbpoll output

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:

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"

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:

ESPHome Solar Inverter page

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:

Home-Assistant ESPHome Devices
ESPHome Device

I can then use the Total Power Generation metric which is an incrementing value useful for utility type of measurements.

Mastodon