Skip to main content

Async Python 3 library to read ModBus from an AlphaESS inverter

Project description

AlphaESS ModBus reader

Async Python 3 library to read ModBus from an AlphaESS inverter. Tested and assumes using a Raspberry Pi as the host for RTU.

Uses asynciominimalmodbus for ModBus/RS485 RTU communication. Uses pymodbys for Modbus TCP communication.

See alphaess_collector which uses this library to store values in MySQL.

Compatible with RTU:

Device Baud Tested
SMILE5 9600
SMILE-B3 9600
SMILE-T10 9600
Storion T30 19200

Hardware (RTU)

⚠️⚠️ This worked for me, so do at your own risk!! ⚠️⚠️

More information (and pictures) in the Notes section below.

  • Use the inverter menu to enable modbus in slave mode.
  • Snip one end of an ethernet cable off and connect (may vary):
    • Blue/white to RS485 A
    • Blue to RS485 B
    • RS485 RX to GPIO 15
    • RS485 TX to GPIO 14
  • Enable serial port on Raspberry Pi with raspi-config.
  • Connect other end of ethernet cable to the inverter CAN port.

Quick start

PIP

Install with:

python3 -m pip install alphaess-modbus

Checkout example.py or example-tcp.py to get started

Clone

Clone repo and run example.py:

git clone git@github.com:SorX14/alphaess_modbus.git
cd ./alphaess_modbus
./example.py
[Sun, 20 Nov 2022 21:36:54] INFO [example.py.main:27] PV: 0W GRID: 1078W USE: 1078W Battery: 0W

Done! 🎉

Architecture

This library concentrates on reading data, but writing is possible.

Uses a JSON definition file containing all the ModBus registers and how to parse them - lookup the register you want from the PDF and request it using the reader functions below.

For example, to get the capacity of your installed system, find the item in the PDF:

PDF entry

Copy the name - PV Capacity of Grid Inverter - and request with await reader.get_value("PV Capacity of Grid Inverter")

Definitions

An excerpt from registers.json:

  {
    "name": "pv2_current",
    "address": 1058,
    "hex": "0x0422",
    "type": "register",
    "signed": false,
    "decimals": 1,
    "units": "A"
  },

which would be used when called with:

await reader.get_value("PV2 current") # or await reader.get_value("pv2_current")

It will read register 0x0422, process the result as unsigned, divide it by 10, and optionally add A as the units.

The default JSON file was created with alphaess_pdf_parser. You can override the default JSON file with Reader(json_file=location)

Reading values

Reader()

Create a new RTU reader

import asyncio
from alphaess_modbus import Reader

async def main():
    reader: Reader = Reader()

    definition = await reader.get_definition("pv2_voltage")
    print(definition)

asyncio.run(main())

Optionally change the defaults with:

  • decimalAddress=85
  • serial='/dev/serial0'
  • debug=False
  • baud=9600
  • json_file=None
  • formatter=None

ReaderTCP()

Create a new TCP reader

import asyncio
from alphaess_modbus import ReaderTCP

async def main():
    reader: ReaderTCP = ReaderTCP(ip="192.168.1.100", port=502)

    definition = await reader.get_definition("pv2_voltage")
    print(definition)

asyncio.run(main())

Optionally change the defaults with:

  • ip=None
  • port=502
  • slave_id=int(0x55)
  • json_file=None
  • formatter=None

get_value(name) -> int

Requests a value from the inverter.

grid = await reader.get_value("total_active_power_grid_meter")
print(grid)

# 1234

Prints the current grid usage as an integer.

get_units(name) -> str

Get units (if any) for a register name.

grid_units = await reader.get_units("total_active_power_grid_meter")
print(grid_units)

# W

get_formatted_value(name, use_formatter=True)

Same as get_value() but returns a string with units. If a formatter is defined for the register, a different return type is possible.

grid = await reader.get_formatted_value("total_active_power_grid_meter")
print(grid)

# 1234W

Set use_formatter to False to prevent a formatter from being invoked.

get_definition(name) -> dict

Get the JSON entry for an item. Useful if you're trying to write a register.

item = await reader.get_definition("inverter_power_total")
print(item)

# {'name': 'inverter_power_total', 'address': 1036, 'hex': '0x040C', 'type': 'long', 'signed': True, 'decimals': 0, 'units': 'W'}

Formatters

Some registers are special and not just simple numbers - they could contain ASCII, hex-encoded numbers or another format.

For example, 0x0809 Local IP returns 4 bytes of the current IP, e.g. 0xC0,0xA8,0x01,0x01 (192.168.1.1).

To help, there is a built-in formatter which will be invoked when calling .get_formatted_value() e.g:

ip = await reader.get_formatted_value("Local IP")
print(ip)

# 192.168.0.1

Not all registers have a formatter, and you might have a preference on how the value is returned (e.g. time-format). To help with this, you can pass a formatter to Reader() and override or add to the default:

class my_custom_formatter:
  def local_ip(self, val) -> str:
    bytes = val.to_bytes(4, byteorder='big')
    return f"IP of device: {int(bytes[0])} - {int(bytes[1])} - {int(bytes[2])} - {int(bytes[3])}"

reader: Reader = Reader(formatter=my_customer_formatter)

local_ip = await reader.get_formatted_value("local_ip")
print(local_ip)

# IP of device: 192 - 168 - 0 - 0

Each formatting function is based on the conformed name of a register. You can find the conformed name of a register by searching registers.json or by using await reader.get_definition(name)

Writing values

☠️ ModBus gives full control of the inverter. There are device-level protections in place but be very careful ☠️

This library is intended to read values, but you can get a reference to the internal ModBus library with reader.instrument:

# Using internal reference to read a value
read = await reader.instrument.read_long(int(0x0021), 3, False)
print(read)

# Untested, but should set system language
await reader.instrument.write_register(int(0x071D), 1, 0)

Read the library docs for what to do next: https://minimalmodbus.readthedocs.io/en/stable/

Use the AlphaESS manual for how each register works.

Notes

Definitions

While my parsing script did its best, there are likely to be many faults and missing entries. I only need a few basic registers so haven't tested them all.

Some registers are longer than the default 4 bytes and won't work- you'll have to use the internal reader instead.

PR's are welcome 🙂

Registers always returning 0

There are a lot of registers, but they might not all be relevant depending on your system setup. For example, the PV meter section is useless if your AlphaESS is in DC mode.

Error handling

I've had the connection break a few times while testing, make sure you handle reconnecting correctly. example.py will output the full exception should one happen.

My TCP setup

Some of the more recent AlphaESS inverters have this out of the box, but mine didn't. The original RTU setup was to bridge this gap.

Eventually, I purchased a WaveShare RS485 TO POE Ethernet Converter but I'm sure there are alternatives. You want something that converts a RTU device to TCP.

The WaveShare one is powered by PoE, it was simple to unplug my RTU setup and put this in its place.

Added a small piece of DIN rail next to my inverter and gave the converter a static IP.

My RTU setup

I used a m5stamp RS485 module with a digital isolator and DC/DC isolator.

RS485 adapter

Installed in an enclosure with a PoE adapter to power the Pi and provide connectivity.

Enclosure

Enabled ModBus interface on the inverter. You'll need the service password, mine was set to the default of 1111.

Modbus enable

Then connected to the CAN port.

Installed

Credit and thanks

Special thanks go to https://github.com/CharlesGillanders/alphaess where I originally started playing around with my PV system. Their project uses the AlphaESS dashboard backend API to unofficially get inverter values from the cloud.

Invaluable resource for discussing with other users. Highly recommend reading https://github.com/CharlesGillanders/alphaess/issues/9 which ended up with AlphaESS creating an official API to retrieve data - https://github.com/alphaess-developer/alphacloud_open_api

Another great resource is https://github.com/dxoverdy/Alpha2MQTT which uses a ESP8266 instead of a Raspberry PI to communicate with the inverter - again - highly recommended.

https://github.com/scanapi/scanapi for 'helping' with github actions (I used their workflow actions as templates for this project).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alphaess-modbus-0.1.1.tar.gz (25.7 kB view details)

Uploaded Source

Built Distribution

alphaess_modbus-0.1.1-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file alphaess-modbus-0.1.1.tar.gz.

File metadata

  • Download URL: alphaess-modbus-0.1.1.tar.gz
  • Upload date:
  • Size: 25.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.10.6 Linux/5.15.0-1023-azure

File hashes

Hashes for alphaess-modbus-0.1.1.tar.gz
Algorithm Hash digest
SHA256 05d147785580acd0f75577936fca42b2939fb46d2901e5b88f596cac0a55ae3b
MD5 d0897173dc187077b424b853e95d393e
BLAKE2b-256 b0e4ecff5fb4a7004314d0f1d4f7562d56daa229bbb020ef0af00f9db5d294d8

See more details on using hashes here.

File details

Details for the file alphaess_modbus-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: alphaess_modbus-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 22.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.10.6 Linux/5.15.0-1023-azure

File hashes

Hashes for alphaess_modbus-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b19d7da89957d73d44e2aeb0f5e3bee5f44ab9af41a5cb6fbabb91382bdcfa47
MD5 b037669c4bbf72dac7c13a8eacd1413e
BLAKE2b-256 da0db3372b07820d7894eb7ee46854c81569a0388ee412416eab867846cf2708

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page