# 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](https://github.com/guyradford/asynciominimalmodbus) for ModBus/RS485 RTU communication.
Uses [pymodbys](https://github.com/riptideio/pymodbus) for Modbus TCP communication.
See [alphaess_collector](https://github.com/SorX14/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](#my-rtu-setup) 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:
``` bash
python3 -m pip install alphaess-modbus
```
Checkout `example.py` or `example-tcp.py` to get started
### Clone
Clone repo and run `example.py`:
``` bash
git clone git@github.com:SorX14/alphaess_modbus.git
cd ./alphaess_modbus
./example.py
```
``` bash
[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](#writing-values) 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](https://www.alpha-ess.de/images/downloads/handbuecher/AlphaESS_Register_Parameter_List.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](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/pdf_register.png)
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`:
``` json
{
"name": "pv2_current",
"address": 1058,
"hex": "0x0422",
"type": "register",
"signed": false,
"decimals": 1,
"units": "A"
},
```
which would be used when called with:
``` python
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](https://github.com/SorX14/alphaess_pdf_parser). You can override the default JSON file with `Reader(json_file=location)`
## Reading values
### `Reader()`
Create a new RTU reader
``` python
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
``` python
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.
``` python
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.
``` python
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](#formatters) is defined for the register, a different return type is possible.
``` python
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](#writing-values) a register.
``` python
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:
``` python
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:
``` python
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](https://pypi.org/project/AsyncioMinimalModbus/) with `reader.instrument`:
``` python
# 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](https://www.alpha-ess.de/images/downloads/handbuecher/AlphaESS_Register_Parameter_List.pdf) for how each register works.
## Notes
### Definitions
While [my parsing script](https://github.com/SorX14/alphaess_pdf_parser) 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](https://www.waveshare.com/rs485-to-eth-b.htm) 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](https://shop.m5stack.com/products/m5stamp-rs485-module) with a digital isolator and DC/DC isolator.
![RS485 adapter](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/rs485_adapter.png)
Installed in an enclosure with a PoE adapter to power the Pi and provide connectivity.
![Enclosure](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/enclosure.png)
Enabled ModBus interface on the inverter. You'll need the service password, mine was set to the default of `1111`.
![Modbus enable](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/modbus_enable.png)
Then connected to the CAN port.
![Installed](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/installed.png)
# 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).
Raw data
{
"_id": null,
"home_page": "https://github.com/SorX14/alphaess_modbus",
"name": "alphaess-modbus",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.8,<4.0",
"maintainer_email": "",
"keywords": "python,alphaess,modbus,rs485,tcp,solar,pv",
"author": "steve.parker",
"author_email": "",
"download_url": "https://files.pythonhosted.org/packages/b0/e4/ecff5fb4a7004314d0f1d4f7562d56daa229bbb020ef0af00f9db5d294d8/alphaess-modbus-0.1.1.tar.gz",
"platform": null,
"description": "# AlphaESS ModBus reader\n\nAsync Python 3 library to read ModBus from an AlphaESS inverter. Tested and assumes using a Raspberry Pi as the host for RTU.\n\nUses [asynciominimalmodbus](https://github.com/guyradford/asynciominimalmodbus) for ModBus/RS485 RTU communication.\nUses [pymodbys](https://github.com/riptideio/pymodbus) for Modbus TCP communication.\n\nSee [alphaess_collector](https://github.com/SorX14/alphaess_collector) which uses this library to store values in MySQL.\n\nCompatible with RTU:\n\n| **Device** | **Baud** | **Tested** |\n|-------------|----------|------------|\n| SMILE5 | 9600 | \u2705 |\n| SMILE-B3 | 9600 | |\n| SMILE-T10 | 9600 | |\n| Storion T30 | 19200 | |\n\n## Hardware (RTU)\n\n\u26a0\ufe0f\u26a0\ufe0f This worked for me, so do at your own risk!! \u26a0\ufe0f\u26a0\ufe0f\n\nMore information (and pictures) in the [Notes](#my-rtu-setup) section below.\n\n- Use the inverter menu to enable modbus in slave mode.\n- Snip one end of an ethernet cable off and connect (may vary):\n - Blue/white to RS485 A\n - Blue to RS485 B \n - RS485 RX to GPIO 15\n - RS485 TX to GPIO 14\n- Enable serial port on Raspberry Pi with `raspi-config`.\n- Connect other end of ethernet cable to the inverter CAN port.\n\n## Quick start\n\n### PIP\n\nInstall with:\n\n``` bash\npython3 -m pip install alphaess-modbus\n```\n\nCheckout `example.py` or `example-tcp.py` to get started\n\n### Clone\n\nClone repo and run `example.py`:\n\n``` bash\ngit clone git@github.com:SorX14/alphaess_modbus.git\ncd ./alphaess_modbus\n./example.py\n```\n\n``` bash\n[Sun, 20 Nov 2022 21:36:54] INFO [example.py.main:27] PV: 0W GRID: 1078W USE: 1078W Battery: 0W\n```\n\nDone! \ud83c\udf89\n\n## Architecture\n\nThis library concentrates on reading data, but [writing](#writing-values) is possible.\n\nUses a JSON definition file containing all the ModBus registers and how to parse them - lookup the register you want from the [PDF](https://www.alpha-ess.de/images/downloads/handbuecher/AlphaESS_Register_Parameter_List.pdf) and request it using the reader functions below.\n\nFor example, to get the capacity of your installed system, find the item in the PDF:\n\n![PDF entry](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/pdf_register.png)\n\nCopy the name - `PV Capacity of Grid Inverter` - and request with `await reader.get_value(\"PV Capacity of Grid Inverter\")`\n\n### Definitions\n\nAn excerpt from `registers.json`:\n\n``` json\n {\n \"name\": \"pv2_current\",\n \"address\": 1058,\n \"hex\": \"0x0422\",\n \"type\": \"register\",\n \"signed\": false,\n \"decimals\": 1,\n \"units\": \"A\"\n },\n```\n\nwhich would be used when called with:\n\n``` python\nawait reader.get_value(\"PV2 current\") # or await reader.get_value(\"pv2_current\")\n```\n\nIt will read register `0x0422`, process the result as unsigned, divide it by 10, and optionally add `A` as the units.\n\nThe default JSON file was created with [alphaess_pdf_parser](https://github.com/SorX14/alphaess_pdf_parser). You can override the default JSON file with `Reader(json_file=location)`\n\n## Reading values\n\n### `Reader()`\n\nCreate a new RTU reader\n\n``` python\nimport asyncio\nfrom alphaess_modbus import Reader\n\nasync def main():\n reader: Reader = Reader()\n\n definition = await reader.get_definition(\"pv2_voltage\")\n print(definition)\n\nasyncio.run(main())\n```\n\nOptionally change the defaults with:\n\n- `decimalAddress=85`\n- `serial='/dev/serial0'`\n- `debug=False`\n- `baud=9600`\n- `json_file=None`\n- `formatter=None`\n\n### `ReaderTCP()`\n\nCreate a new TCP reader\n\n``` python\nimport asyncio\nfrom alphaess_modbus import ReaderTCP\n\nasync def main():\n reader: ReaderTCP = ReaderTCP(ip=\"192.168.1.100\", port=502)\n\n definition = await reader.get_definition(\"pv2_voltage\")\n print(definition)\n\nasyncio.run(main())\n```\n\nOptionally change the defaults with:\n\n- `ip=None`\n- `port=502`\n- `slave_id=int(0x55)`\n- `json_file=None`\n- `formatter=None`\n\n### `get_value(name) -> int`\n\nRequests a value from the inverter.\n\n``` python\ngrid = await reader.get_value(\"total_active_power_grid_meter\")\nprint(grid)\n\n# 1234\n```\n\nPrints the current grid usage as an integer.\n\n### `get_units(name) -> str`\n\nGet units (if any) for a register name.\n\n``` python\ngrid_units = await reader.get_units(\"total_active_power_grid_meter\")\nprint(grid_units)\n\n# W\n```\n\n### `get_formatted_value(name, use_formatter=True)`\n\nSame as `get_value()` but returns a string with units. If a [formatter](#formatters) is defined for the register, a different return type is possible.\n\n``` python\ngrid = await reader.get_formatted_value(\"total_active_power_grid_meter\")\nprint(grid)\n\n# 1234W\n```\n\nSet `use_formatter` to `False` to prevent a formatter from being invoked.\n\n### `get_definition(name) -> dict`\n\nGet the JSON entry for an item. Useful if you're trying to [write](#writing-values) a register.\n\n``` python\nitem = await reader.get_definition(\"inverter_power_total\")\nprint(item)\n\n# {'name': 'inverter_power_total', 'address': 1036, 'hex': '0x040C', 'type': 'long', 'signed': True, 'decimals': 0, 'units': 'W'}\n```\n\n## Formatters\n\nSome registers are special and not just simple numbers - they could contain ASCII, hex-encoded numbers or another format.\n\nFor example, `0x0809` `Local IP` returns 4 bytes of the current IP, e.g. `0xC0\uff0c0xA8\uff0c0x01\uff0c0x01` (`192.168.1.1`).\n\nTo help, there is a built-in formatter which will be invoked when calling `.get_formatted_value()` e.g:\n\n``` python\nip = await reader.get_formatted_value(\"Local IP\")\nprint(ip)\n\n# 192.168.0.1\n```\n\nNot 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:\n\n``` python\n\nclass my_custom_formatter:\n def local_ip(self, val) -> str:\n bytes = val.to_bytes(4, byteorder='big')\n return f\"IP of device: {int(bytes[0])} - {int(bytes[1])} - {int(bytes[2])} - {int(bytes[3])}\"\n\nreader: Reader = Reader(formatter=my_customer_formatter)\n\nlocal_ip = await reader.get_formatted_value(\"local_ip\")\nprint(local_ip)\n\n# IP of device: 192 - 168 - 0 - 0\n```\n\nEach 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)`\n\n## Writing values\n\n\u2620\ufe0f ModBus gives full control of the inverter. There are device-level protections in place but be very careful \u2620\ufe0f\n\nThis library is intended to read values, but you can get a reference to the [internal ModBus library](https://pypi.org/project/AsyncioMinimalModbus/) with `reader.instrument`:\n\n``` python\n# Using internal reference to read a value\nread = await reader.instrument.read_long(int(0x0021), 3, False)\nprint(read)\n\n# Untested, but should set system language\nawait reader.instrument.write_register(int(0x071D), 1, 0)\n```\n\nRead the library docs for what to do next: https://minimalmodbus.readthedocs.io/en/stable/\n\nUse the [AlphaESS manual](https://www.alpha-ess.de/images/downloads/handbuecher/AlphaESS_Register_Parameter_List.pdf) for how each register works.\n\n## Notes\n\n### Definitions\n\nWhile [my parsing script](https://github.com/SorX14/alphaess_pdf_parser) 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.\n\nSome registers are longer than the default 4 bytes and won't work- you'll have to use the internal reader instead.\n\nPR's are welcome \ud83d\ude42\n\n### Registers always returning 0\n\nThere 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. \n\n### Error handling\n\nI'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.\n\n### My TCP setup\n\nSome 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.\n\nEventually, I purchased a [WaveShare RS485 TO POE Ethernet Converter](https://www.waveshare.com/rs485-to-eth-b.htm) but I'm sure there are alternatives. You want something that converts a RTU device to TCP. \n\nThe WaveShare one is powered by PoE, it was simple to unplug my RTU setup and put this in its place.\n\nAdded a small piece of DIN rail next to my inverter and gave the converter a static IP. \n\n### My RTU setup\n\nI used a [m5stamp RS485 module](https://shop.m5stack.com/products/m5stamp-rs485-module) with a digital isolator and DC/DC isolator.\n\n![RS485 adapter](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/rs485_adapter.png)\n\nInstalled in an enclosure with a PoE adapter to power the Pi and provide connectivity.\n\n![Enclosure](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/enclosure.png)\n\nEnabled ModBus interface on the inverter. You'll need the service password, mine was set to the default of `1111`.\n\n![Modbus enable](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/modbus_enable.png)\n\nThen connected to the CAN port.\n\n![Installed](https://raw.githubusercontent.com/SorX14/alphaess_modbus/main/docs/installed.png)\n\n# Credit and thanks\n\nSpecial thanks go to https://github.com/CharlesGillanders/alphaess where I originally started\nplaying around with my PV system. Their project uses the AlphaESS dashboard backend API to unofficially get inverter values from the cloud.\n\nInvaluable resource for discussing with other users. Highly\nrecommend reading https://github.com/CharlesGillanders/alphaess/issues/9 which ended up with\nAlphaESS creating an official API to retrieve data - https://github.com/alphaess-developer/alphacloud_open_api\n\nAnother great resource is https://github.com/dxoverdy/Alpha2MQTT which uses a ESP8266 instead\nof a Raspberry PI to communicate with the inverter - again - highly recommended.\n\nhttps://github.com/scanapi/scanapi for 'helping' with github actions (I used their workflow actions as templates for this project).\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Async Python 3 library to read ModBus from an AlphaESS inverter",
"version": "0.1.1",
"split_keywords": [
"python",
"alphaess",
"modbus",
"rs485",
"tcp",
"solar",
"pv"
],
"urls": [
{
"comment_text": "",
"digests": {
"md5": "b037669c4bbf72dac7c13a8eacd1413e",
"sha256": "b19d7da89957d73d44e2aeb0f5e3bee5f44ab9af41a5cb6fbabb91382bdcfa47"
},
"downloads": -1,
"filename": "alphaess_modbus-0.1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "b037669c4bbf72dac7c13a8eacd1413e",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8,<4.0",
"size": 22192,
"upload_time": "2022-12-01T22:19:28",
"upload_time_iso_8601": "2022-12-01T22:19:28.498100Z",
"url": "https://files.pythonhosted.org/packages/da/0d/b3372b07820d7894eb7ee46854c81569a0388ee412416eab867846cf2708/alphaess_modbus-0.1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"md5": "d0897173dc187077b424b853e95d393e",
"sha256": "05d147785580acd0f75577936fca42b2939fb46d2901e5b88f596cac0a55ae3b"
},
"downloads": -1,
"filename": "alphaess-modbus-0.1.1.tar.gz",
"has_sig": false,
"md5_digest": "d0897173dc187077b424b853e95d393e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8,<4.0",
"size": 25692,
"upload_time": "2022-12-01T22:19:27",
"upload_time_iso_8601": "2022-12-01T22:19:27.046210Z",
"url": "https://files.pythonhosted.org/packages/b0/e4/ecff5fb4a7004314d0f1d4f7562d56daa229bbb020ef0af00f9db5d294d8/alphaess-modbus-0.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2022-12-01 22:19:27",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "SorX14",
"github_project": "alphaess_modbus",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "alphaess-modbus"
}