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:
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.
Installed in an enclosure with a PoE adapter to power the Pi and provide connectivity.
Enabled ModBus interface on the inverter. You'll need the service password, mine was set to the default of 1111
.
Then connected to the CAN port.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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
Algorithm | Hash digest | |
---|---|---|
SHA256 | 05d147785580acd0f75577936fca42b2939fb46d2901e5b88f596cac0a55ae3b |
|
MD5 | d0897173dc187077b424b853e95d393e |
|
BLAKE2b-256 | b0e4ecff5fb4a7004314d0f1d4f7562d56daa229bbb020ef0af00f9db5d294d8 |
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
Algorithm | Hash digest | |
---|---|---|
SHA256 | b19d7da89957d73d44e2aeb0f5e3bee5f44ab9af41a5cb6fbabb91382bdcfa47 |
|
MD5 | b037669c4bbf72dac7c13a8eacd1413e |
|
BLAKE2b-256 | da0db3372b07820d7894eb7ee46854c81569a0388ee412416eab867846cf2708 |