Introduction

A few years ago we got some solar panels installed. The installation includes solar panels (duh!), a Ginlong Solis inverter and a Landis+Gyr meter. The meter uploads meter data to the platform of the installer, however it only does so twice a day.

To get real-time data on our solar production, I figured I had 3 options:

  1. Get the data from the inverter.
    While the Solis inverter has modules that can upload the meter data over WiFi or 2G/3G to the cloud portal of Ginlong, our installer didn’t install any as they for some reason chose to use a seperate meter.
    However, the COM port is still accessible.
    I’d have to figure out how to connect to it, which pins I need, the protocol it speaks and how to decode the data.
  2. Get the data from the meter.
    The Landis+Gyr meter has an LED that does 1.000 pulses for 1 kWH.
    So I could hook up a light sensor, like the Home Assistant glow, and count pulses.
  3. Hook a clamp onto the power wire entering the fuse box.
    A Shelly EM connected to the incoming Live wire would give me an easy Commercial off-the-shelf (COTS) solution.
    I already have a 3EM measuring the flow from/to the grid, as I discussed in a previous post.

After following multiple tutorials, like this one from the ESPHome cookbook without much success, I dropped plan no. 2 and decided to take my changes with option 1. If this would fail, I’d give up on DIY and go for option 3: the Shelly EM.

Luckily I found these two tutorials giving me a pretty detailed explanation on how to hook up a Raspberry Pi to the inverter’s COM port using a RS485 serial to UART/USB converter.

After a bit of swearing and a bit of help from Solis’ local help center, I managed to get this solution to work :)
Since I there were a few differences between the tutorials I found and the solution I finally managed to hack together, I thought it may be a good idea to document my process. Even if it doesn’t help anyone else, it’ll help future me if the solution ever breaks down :p

What we’ll need

Our shopping list is pretty small for this one:

Preparing the Raspberry Pi

Flashing microSD card

We’ll only need the Raspberry Pi to read the data over UART and send it to Home Assistant over MQTT. This will be done via a simple Python script.

So I just flashed Raspbian Lite on the SD card using balenaEtcher as we don’t need a Graphical User Interface (GUI).

WiFi and SSH

To get our Raspberry Pi to immediately connect to our WiFi, we can add a wpa_supplicant.conf file in the boot partition on the SD card.

  1. Plug the microSD card back in the computer after flashing.
  2. Create a new file and name it wpa_supplicant.conf
  3. Add the following code and save the file.
    Make sure you modify it to match your situation and ensure you’re using Unix line endings (\n).
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=BE # changeme

network={
    ssid="YOUR_SSID"
    psk="YOUR_PASSWORD"
    key_mgmt=WPA-PSK
}

To make sure we can SSH into the RPi, create an empty file named ssh. No extension, no data.
This must be done before the first boot of the Raspberry Pi.

RS485 HAT

If you got the RS485 HAT, you’ll still need to solder the 2x20 pin header, and optionally the A/B/GND pin header and/or DB9 serial connector.
The HAT should come with a resistor. Normally the last device in the line in Modbus has a resistor on it, so I soldered mine on as well.

Picture of Raspberry Pi with RS485 HAT
RS485 HAT with components

Setting up serial connection

Power the Raspberry Pi with a phone charger (5V A1 is sufficient) so that we can connect to it over SSH.
Before we do anything else, it may also be a good idea to update our Raspberry Pi.

ssh pi@<ip.of.RPi.zero>
# password = raspberry

sudo apt update
sudo apt dist-upgrade -y
sudo apt autoremove -y

The RPi Zero W uses the same serial for the Bluetooth as we want to use for our HAT (which is connected to GPIO 14 and 15). So we need to do a few tweak, which includes enabling hardware UART and moving bluetooth to the secondary UART channel.

sudo raspi-config
# > 3  - Interface Options
# > P6 - Serial Port
# > Would you like a login shell to be accessible over serial?
#   > No
# > Would you like the serial port hardware to be enabled?
#   > Yes

sudo nano /boot/config.txt
# Before [pi4] add the following lines:
enable_uart=1
dtoverlay=miniuart-bt
force_turbo=1
# Save and close
# Ctrl+X / Y / [Return]

Also check that /boot/cmdline.txt doesn’t start a terminal over serial.
My file looks like:

console=tty1 root=PARTUUID=92a0ae12-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Give the Raspberry Pi a reboot. The serial connection of the HAT should be now connected to /dev/serial0, which actually is a symbolic link to /dev/ttyAMA0.

Solis inverter COM port

The COM port of the Solis inverter has 4 pins, conveniently numbered 1-4. We’ll only need pins 3 and 4, which correspond with pins A and B on the RS485 HAT respectively.

I got a connector with a length of wire attached to it already. That’s why I ‘hacked’ the connection between the HAT and the COM port together using Dupont wires and a DC jack.
In the future I’ll probably de-solder the 3-pin header and solder the COM-cable directly to the A and B holes on the HAT.

If you bought the connector from AliExpress, you’ll need to attach some wire yourself. A length of RJ11 (telephone) cable or ethernet cable or some speaker wire should suffice.
You can decide yourself whether you want to solder onto the RPi HAT, use a connector (e.g. Wago or DC jack) or crimp some Dupont connectors onto the wires.

Picture of Raspbarry Pi connected to Solis inverter's COM port
RS485 hooked up to the inverter

Script

Now it’s time for the script that will read the data over serial and forward them over MQTT.

We’ll need a few packages for this script, so let’s install them first.

# ssh pi@<ip.of.RPi.zero>
sudo apt update
sudo apt install -y python3 python3-pip
python3 -m pip install -U pyserial
python3 -m pip install -U minimalmodbus
python3 -m pip install -U mqtt-mqtt

Then create a new file (I called mine solis_meter.py) and paste the following code.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Read modbus data from Ginlong Solis inverter
# and send over MQTT
#
# Based on https://github.com/rogersia/Solis-4G
# Includes fixes, small modifcations and refactoring. Migrated to Python3.
# See https://sequr.be/blog/2021/08/reading-ginlong-solis-inverter-over-serial-and-importing-in-home-assistant-over-mqtt/
#
import logging
import minimalmodbus
import paho.mqtt.client as mqtt
import serial
import socket
import sys
import time

broker = "192.168.xxx.yyy"      # Set to MQTT broker IP (likely your HA IP)
port = 1883                     # Set to MQTT broker port
mqttuser = "YOUR_MQTT_USER"     # Set to your MQTT user username
mqttpass = "YOUR_MQTT_PASSWORD" # Set to your MQTT user password
client_id = "solis_com"

logging.basicConfig(stream=sys.stderr, level=logging.INFO)

def mqtt_connect():
  # callback for mqtt
  def on_connect(client, userdata, flags, rc):
    logging.debug("MQTT connected with result code {}".format(rc))

  client = mqtt.Client(client_id)
  client.username_pw_set(mqttuser, mqttpass)
  client.on_connect = on_connect
  client.connect_async(broker, port, 60)
  return client

def mqtt_subscribe(client):
  def on_message(client, userdata, msg):
    logging.debug("[ {} ({})] {}".format(msg.topic, msg.qos, msg.payload))

  client.subscribe("meters/solis_com/command", 2)
  client.on_message = on_message

def mqtt_publish(client, data):
  def on_publish(client, userdata, mid):
    logging.debug("[{}] published ({})".format(mid, userdata))

  def send(client, topic, payload="", qos=2, retain=False):
    res = client.publish(topic, payload, qos, retain)
    res.wait_for_publish()
    logging.debug("[{}] status: {} - {}".format(res.mid, res.rc, "Published" if res.is_published() else "Failed"))
    time.sleep(0.5)

  client.on_publish = on_publish
  time.sleep(2)
  for k, v in data.items():
    send(client, "meters/solis_com/{}".format(k), v)

def modbus_connect():
  instrument = minimalmodbus.Instrument('/dev/serial0', 2) # Set to inverter's address
  instrument.serial.baudrate = 9600
  instrument.serial.bytesize = 8
  instrument.serial.parity   = serial.PARITY_NONE
  instrument.serial.stopbits = 1
  instrument.serial.timeout  = 3
  #instrument.debug = True
  return instrument

def modbus_read(instrument):
  timestamp = time.time()
  # get data from solis
  Realtime_ACW = instrument.read_long(3004, functioncode=4, signed=False) # Read AC Watts as Unsigned 32-Bit
  logging.info("{:<23s}{:10.2f} W".format("AC Watts", Realtime_ACW))
  Realtime_DCV = instrument.read_register(3021, number_of_decimals=2, functioncode=4, signed=False) # Read DC Volts as Unsigned 16-Bit
  logging.info("{:<23s}{:10.2f} V".format("DC Volt", Realtime_DCV))
  Realtime_DCI = instrument.read_register(3022, number_of_decimals=0, functioncode=4, signed=False) # Read DC Current as Unsigned 16-Bit
  logging.info("{:<23s}{:10.2f} A".format("DC Current", Realtime_DCI))
  Realtime_ACV = instrument.read_register(3035, number_of_decimals=1, functioncode=4, signed=False) # Read AC Volts as Unsigned 16-Bit
  logging.info("{:<23s}{:10.2f} V".format("AC Volt", Realtime_ACV))
  Realtime_ACI = instrument.read_register(3038, number_of_decimals=1, functioncode=4, signed=False) # Read AC Current as Unsigned 16-Bit
  logging.info("{:<23s}{:10.2f} A".format("AC Current", Realtime_ACI))
  Realtime_ACF = instrument.read_register(3042, number_of_decimals=2, functioncode=4, signed=False) # Read AC Frequency as Unsigned 16-Bit
  logging.info("{:<23s}{:10.2f} Hz".format("AC Frequency", Realtime_ACF))
  Inverter_C = instrument.read_register(3041, number_of_decimals=1, functioncode=4, signed=True) # Read Inverter Temperature as Signed 16-Bit
  logging.info("{:<23s}{:10.2f} °C".format("Inverter Temperature", Inverter_C))
  AlltimeEnergy_KW = instrument.read_long(3008, functioncode=4, signed=False) # Read All Time Energy (KWH Total) as Unsigned 32-Bit
  logging.info("{:<23s}{:10.2f} kWh".format("Generated (All time)", AlltimeEnergy_KW))
  Today_KW = instrument.read_register(3014, number_of_decimals=1, functioncode=4, signed=False) # Read Today Energy (KWH Total) as 16-Bit
  logging.info("{:<23s}{:10.2f} kWh".format("Generated (Today)", Today_KW))

  data = {
    'online': timestamp,
    'acw': Realtime_ACW,
    'dcv': Realtime_DCV,
    'dci': Realtime_DCI,
    'acv': Realtime_ACV,
    'aci': Realtime_ACI,
    'acf': Realtime_ACF,
    'inc': Inverter_C
  }

  # Fix for 0-values during inverter powerup
  if AlltimeEnergy_KW > 0: data["gat"] = AlltimeEnergy_KW
  if Today_KW > 0: data["gto"] = Today_KW

  return data

def main():
  try:
    mqttc = mqtt_connect()
    mqtt_subscribe(mqttc)
    mqttc.loop_start()
    modc = modbus_connect()
    data = modbus_read(modc)
    mqtt_publish(mqttc, data)

  except TypeError as err:
    logging.error("TypeError:\n{}".format(err))

  except ValueError as err:
    logging.error("ValueError:\n{}".format(err))

  except minimalmodbus.NoResponseError as err:
    logging.error("Modbus no response:\n{}".format(err))

  except serial.SerialException as err:
    logging.error("SerialException:\n{}".format(err))

  except Exception as err:
    logging.error("Exception:\n{}".format(err))

if __name__ == "__main__":
  main()

Make sure you modify the broker IP and MQTT username/password.

You’ll also notice the comment Set inverter's address.
This is something that had me swearing for way to long …
The scripts I based my configuration on used modbus address 1 for the Solis inverter, however mine appeared to be configured to use address 2 … <insert headbang emoji>.
To find the address of your inverter, follow the steps in this Solis training video (Enter > Settings > Set Address).

Running the script with the RPi connected to the Solis inverter and the inverter being online, should give you something like below.
If not, change the log level at the top of the code to logging.DEBUG figure out what the issue is.

$ ./solis_meter.py
INFO:root:AC Watts                  1233.00 W
INFO:root:DC Volt                     28.12 V
INFO:root:DC Current                  45.00 A
INFO:root:AC Volt                    243.60 V
INFO:root:AC Current                   5.10 A
INFO:root:AC Frequency                49.99 Hz
INFO:root:Inverter Temperature        38.00 °C
INFO:root:Generated (All time)      7018.00 kWh
INFO:root:Generated (Today)            5.30 kWh

If you listen to meters/solis_com/# on your MQTT broker you should see the same values come in.

Cronjob

To run this script every minute, add it to your crontab.

crontab -e

# Add the following line to the end
* * * * * /home/pi/solis_meter.py > /dev/null

Home Assistant

Creating sensors

To get the data into Home Assistant, we’ll need to create some MQTT sensors. This is about the same as we did in the Getting Started with MQTT post.

Add the following config under the sensor: section, either in configuration.yaml or in your sensors file if you seperated your config.

- platform: mqtt
  name: "Solis meter - Last update"
  qos: 1
  unique_id: "solis_com_online"
  state_topic: "meters/solis_com/online"
  device_class: timestamp
  value_template: "{{ (value | int | timestamp_local | as_datetime()).isoformat() }}"
- platform: mqtt
  name: "Solis meter - AC Watts"
  qos: 1
  unique_id: "solis_com_acw"
  state_topic: "meters/solis_com/acw"
  device_class: power
  unit_of_measurement: W
- platform: mqtt
  name: "Solis meter - DC Volt"
  qos: 1
  unique_id: "solis_com_dcv"
  state_topic: "meters/solis_com/dcv"
  device_class: voltage
  unit_of_measurement: V
- platform: mqtt
  name: "Solis meter - DC Current"
  qos: 1
  unique_id: "solis_com_dci"
  state_topic: "meters/solis_com/dci"
  device_class: current
  unit_of_measurement: A
- platform: mqtt
  name: "Solis meter - AC Volt"
  qos: 1
  unique_id: "solis_com_acv"
  state_topic: "meters/solis_com/acv"
  device_class: voltage
  unit_of_measurement: V
- platform: mqtt
  name: "Solis meter - AC Current"
  qos: 1
  unique_id: "solis_com_aci"
  state_topic: "meters/solis_com/aci"
  device_class: current
  unit_of_measurement: A
- platform: mqtt
  name: "Solis meter - AC Frequency"
  qos: 1
  unique_id: "solis_com_acf"
  state_topic: "meters/solis_com/acf"
  unit_of_measurement: Hz
- platform: mqtt
  name: "Solis meter - Inverter Temperature"
  qos: 1
  unique_id: "solis_com_inc"
  state_topic: "meters/solis_com/inc"
  device_class: temperature
  unit_of_measurement: °C
- platform: mqtt
  name: "Solis meter - Generated (All time)"
  qos: 1
  unique_id: "solis_com_gat"
  state_topic: "meters/solis_com/gat"
  device_class: energy
  unit_of_measurement: kWh
  state_class: total_increasing
- platform: mqtt
  name: "Solis meter - Generated (Today)"
  qos: 1
  unique_id: "solis_com_gto"
  state_topic: "meters/solis_com/gto"
  device_class: energy
  unit_of_measurement: kWh
  state_class: total_increasing

Home Assistant 2021.9.0

HA 2021.9.0 introduced state_class: total_increasing. Before this version, we need to add a last reset timestamp in the sensor’s attributes to be able to add this sensor to the Energy Dashboard. More info on the Home Assistant Developers Blog.

The old solution was to use the following attributes for the ‘All time’ sensor.

  state_class: measurement
  last_reset_topic: 'meters/solis_com/gat'
  last_reset_value_template: '1970-01-01T00:00:00+00:00'

After saving this config, go to Configuration > Server Controls > YAML configuration reloading and click Manually configured MQTT entities.
Wait for the next MQTT message to come in and enjoy seeing the data appear in Home Assistant :)

Home Assistant - Solis sensor entities
Our sensors in Home Assistant
type: entities
entities:
  - entity: sensor.solis_meter_last_update
  - entity: sensor.solis_meter_ac_current
  - entity: sensor.solis_meter_ac_volt
  - entity: sensor.solis_meter_ac_watts
  - entity: sensor.solis_meter_ac_frequency
  - entity: sensor.solis_meter_dc_current
  - entity: sensor.solis_meter_dc_volt
  - entity: sensor.solis_meter_generated_today
  - entity: sensor.solis_meter_generated_all_time
  - entity: sensor.solis_meter_inverter_temperature

Incorrect values when inverter boots

After running this code for a while, I noticed the inverter would sometimes report 0 (zero) for the Generated (All time) sensor. I figured out this happens when the inverter boots in the morning, as it shuts down when there’s no solar production.

This would cause Home Assistant to see this as a reset of the counter, after which the next (correct) was seen as just a huge production increase. As a result, I would get some very wild readings.

To fix this, I created a filtered version of the Generated (All time) sensor, which ignores readings that vary too much.

Add the following below the sensor configurations and use the clean version of the sensor in your Energy Dashboard.

- platform: filter
  name: "Solis meter - Generated (All time) - clean"
  entity_id: sensor.solis_meter_generated_all_time
  filters:
    - filter: outlier
      window_size: 3  # Watch last 3 values
      radius: 10      # Ignore any change > 10kWh

Note: This issue was fixed later in the Python code on the Raspberry Pi, only sending the gat and gto values if they are bigger than 0. Home Assistant is will detect the drop in the value of the Generated (Today) output and doesn’t require a 0 (zero) value to be sent first for that day’s counter to be reset. The value of the Generated (All time) should never drop.

Home Assistant Energy Dashboard

We can now add our sensor to the Home Assistant Energy Dashboard.

We discussed setting up the Energy Dashboard in a previous blog post. If you don’t know how to use the Energy Dashboard, I advise you to go read that post. It also discusses using a Shelly 3EM to monitor our grid consumption/return.

Energy Dashboard - adding solar production meter
Adding our meter to the Energy Dashboard

If you configured the Forecast integration, the dashboard will use historic averages combined with weather forecasting to show you the expected solar production for the day. Using this integration, you’ll get a line graph depicting the forecasted solar production for that day.

Energy Dashboard - solar forecast
Solar Forecast and first hour of data capture

Special thanks to the very helpful people at the EU Service team of Solis for helping me answer some questions regarding this setup!