add source

This commit is contained in:
Norbert
2023-07-04 09:32:58 +02:00
parent ea08f00fdf
commit f8d1716a8b
16 changed files with 3560 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

114
debug.py Normal file
View File

@@ -0,0 +1,114 @@
from solaxx3.rs485 import SolaxX3
solax_items=[
"power_dc1",
"power_dc2",
"battery_capacity",
"radiator_temperature",
"grid_power",
"grid_voltage",
"grid_current",
"pv_voltage_1",
"pv_voltage_2",
"pv_current_1",
"pv_current_2",
"consumption_energy_today",
"feed_in_power",
"energy_to_grid_total",
"feed_in_energy_today",
"consumption_energy_today",
"solar_energy_today",
"battery_power_charge1",
"temperature_battery",
"output_energy_charge_today",
"grid_current_r",
"grid_current_s",
"grid_current_t",
"grid_power_r",
"grid_power_s",
"grid_power_t",
"grid_voltage_r",
"grid_voltage_s",
"grid_voltage_t"
]
openhab_items=[
"Solax_PowerDC1",
"Solax_PowerDC2",
"Solax_BatterySOC",
"Solax_TempRadiator",
"Solax_ACPower",
"Solax_NetworkVoltage",
"Solax_OutputCurrent",
"Solax_PV1Voltage",
"Solax_PV2Voltage",
"Solax_PV1Current",
"Solax_PV2Current",
"Solax_ConsumeFromGridToday",
"Solax_FeedInPower",
"Solax_FeedInTotal",
"Solax_FeedinEnergyToday",
"Solax_HouseConsumption",
"Solax_DailyYield",
"Solax_BatteryPowerCharge",
"Solax_BatteryTemp",
"Solax_BatteryOutputEnergyToday",
"Solax_GridCurrent_1",
"Solax_GridCurrent_2",
"Solax_GridCurrent_3",
"Solax_GridPower_1",
"Solax_GridPower_2",
"Solax_GridPower_3",
"Solax_GridVoltage_1",
"Solax_GridVoltage_2",
"Solax_GridVoltage_3"
]
# adjust the serial port and baud rate as necessary
s = SolaxX3(port="/dev/ttySOLAX", baudrate=19200)
data =[]
if s.connect():
s.read_all_registers()
available_stats = s.list_register_names()
for stat in available_stats:
print(stat + " " + str(s.read(stat)[0]))
battery_capacity = s.read("battery_capacity")[0]
feed_in_today = s.read("feed_in_energy_today")[0]
consumtion_today = s.read("consumption_energy_today")[0]
battery_charging = s.read("battery_power_charge1")[0]
grid_voltage_r = s.read("grid_voltage_r")[0]
grid_voltage_s = s.read("grid_voltage_s")[0]
grid_voltage_t = s.read("grid_voltage_t")[0]
run_mode = s.read("run_mode")[0]
time_count_down = s.read("time_count_down")[0]
inverter_ac_power = s.read("grid_power")[0]
etoday_togrid = s.read("energy_to_grid_today")[0]
solar_energy_today = s.read("solar_energy_today")[0]
echarge_today = s.read("echarge_today")[0]
energy_from_grid = s.read("energy_from_grid_meter")[0]
energy_to_grid = s.read("energy_to_grid_meter")[0]
power_to_ev = s.read("power_to_ev")[0]
feed_in_power = s.read("feed_in_power")[0]
output_energy_charge = s.read("output_energy_charge")[0]
output_energy_today = s.read("output_energy_charge_today")[0]
input_energy_today = s.read("input_energy_charge_today")[0]
power_dc1 = s.read("power_dc1")[0]
power_dc2 = s.read("power_dc2")[0]
total_power = power_dc1 + power_dc2
#battery_temperature = s.read("temperature_battery")
#print(f"\n\nBattery temperature: {s.read('temperature_battery')}")
# for i, item in enumerate(solax_items):
# print (item + " " + str(s.read(item)[0]) + str(s.read(item)[1])+ " " + openhab_items[i])
# for i, item in enumerate(solax_items):
# try:
# act_item=items.get(openhab_items[i])
# act_item.state=float(s.read(item)[0])
# data += [openhab_items[i]+" value="+str(s.read(item)[0])]
# except:
# print("error in" + openhab_items[i] + " " + item )
else:
print("Cannot connect to the Modbus Server/Slave")
exit()

View File

@@ -127,6 +127,8 @@ if s.connect():
data += ["Solax_SUM_DC"+" value="+str(total_power)] data += ["Solax_SUM_DC"+" value="+str(total_power)]
data += ["Solax_eToGridToday"+" value="+str(etoday_togrid)] data += ["Solax_eToGridToday"+" value="+str(etoday_togrid)]
data += ["Solax_feed_in_today"+" value="+str(feed_in_today)] data += ["Solax_feed_in_today"+" value="+str(feed_in_today)]
data += ["Solax_runMode"+" value="+str(run_mode)]
data += ["Solax_time_count_down"+" value="+str(time_count_down)]
write_api = client.write_api(write_options=SYNCHRONOUS) write_api = client.write_api(write_options=SYNCHRONOUS)
write_api.write(bucket, org, data) write_api.write(bucket, org, data)

8
source/LICENSE Normal file
View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright © 2022 <copyright Flavius Moldovan>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

50
source/README.md Normal file
View File

@@ -0,0 +1,50 @@
# solax-x3
#### Read in real-time all parameters provided by Solax X3 solar inverter via its Modbus S-485 serial interface.
<br />
## Prerequisites
* Solax X3 inverter
* Modbus RS-485 serial adapter/interface
* [Modbus cable](https://github.com/mkfam7/solaxx3/blob/main/diagrams/rs485_cable.png)
* python version >= 3.8
* This python module
## Installation
```
pip install solaxx3
```
## Usage
```
from solaxx3.rs485 import SolaxX3
# adjust the serial port and baud rate as necessary
s = SolaxX3(port="/dev/ttyUSB0", baudrate=115200)
if s.connect():
s.read_all_registers()
available_stats = s.list_register_names()
for stat in available_stats:
print(stat)
battery_temperature = s.read("temperature_battery")
print(f"\n\nBattery temperature: {s.read('temperature_battery')}")
else:
print("Cannot connect to the Modbus Server/Slave")
exit()
```
Project Link: [https://github.com/mkfam7/solaxx3](https://github.com/mkfam7/solaxx3)

View File

@@ -0,0 +1,108 @@
from datetime import datetime, timedelta
from pymodbus.client import ModbusSerialClient
from solaxx3.rs485 import SolaxX3
import mysql.connector
s = SolaxX3(port="/dev/ttyUSB0", baudrate=115200)
database_ip = "172.17.7.77"
if s.connect():
s.read_all_registers()
# read the stats from the inverter
battery_capacity = s.read("battery_capacity")[0]
feed_in_today = s.read("feed_in_energy_today")[0]
consumtion_today = s.read("consumption_energy_today")[0]
battery_charging = s.read("battery_power_charge1")[0]
grid_voltage_r = s.read("grid_voltage_r")[0]
grid_voltage_s = s.read("grid_voltage_s")[0]
grid_voltage_t = s.read("grid_voltage_t")[0]
run_mode = s.read("run_mode")[0]
time_count_down = s.read("time_count_down")[0]
inverter_ac_power = s.read("grid_power")[0]
etoday_togrid = s.read("energy_to_grid_today")[0]
solar_energy_today = s.read("solar_energy_today")[0]
echarge_today = s.read("echarge_today")[0]
energy_from_grid = s.read("energy_from_grid_meter")[0]
energy_to_grid = s.read("energy_to_grid_meter")[0]
power_to_ev = s.read("power_to_ev")[0]
feed_in_power = s.read("feed_in_power")[0]
output_energy_charge = s.read("output_energy_charge")[0]
output_energy_today = s.read("output_energy_charge_today")[0]
input_energy_today = s.read("input_energy_charge_today")[0]
power_dc1 = s.read("power_dc1")[0]
power_dc2 = s.read("power_dc2")[0]
total_power = power_dc1 + power_dc2
uploadTime = s.read("rtc_datetime")[0]
uploadDate = uploadTime.date()
timezone_difference_from_utc = 2
uploadTime = uploadTime - timedelta(hours=timezone_difference_from_utc, minutes=0)
# store the stats in the database
mydb = mysql.connector.connect(
host=database_ip, user="root", passwd="rootroot", database="solax"
)
mycursor = mydb.cursor()
try:
# create the sql statement
sql = """REPLACE INTO solax_local (
uploadTime,
inverter_status,
dc_solar_power,
grid_voltage_r,
grid_voltage_s,
grid_voltage_t,
battery_capacity,
battery_power,
feed_in_power,
time_count_down,
inverter_ac_power,
consumeenergy,
feedinenergy,
power_dc1,
power_dc2
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
values = (
uploadTime,
run_mode,
total_power,
grid_voltage_r,
grid_voltage_s,
grid_voltage_t,
battery_capacity,
battery_charging,
feed_in_power,
time_count_down,
inverter_ac_power,
energy_from_grid,
energy_to_grid,
power_dc1,
power_dc2,
)
mycursor.execute(sql, values)
mydb.commit()
# update daily values
sql = """REPLACE INTO solax_daily (
uploadDate,
feed_in,
total_yield
) VALUES (%s, %s, %s)
"""
values = (uploadDate, feed_in_today, etoday_togrid)
mycursor.execute(sql, values)
mydb.commit()
except mysql.connector.Error as error:
print("parameterized query failed {}".format(error))
else:
print("Cannot connect to the Modbus Server/Slave")
exit()

View File

@@ -0,0 +1,25 @@
CREATE TABLE `solax_daily` (
`uploadDate` date NOT NULL,
`feed_in` float(6,1) DEFAULT NULL,
`total_yield` float(6,1) DEFAULT NULL,
PRIMARY KEY (`uploadDate`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `solax_local` (
`uploadTime` datetime NOT NULL,
`inverter_status` tinyint(4) DEFAULT NULL,
`dc_solar_power` smallint(6) DEFAULT NULL,
`grid_voltage_r` smallint(6) DEFAULT NULL,
`grid_voltage_s` smallint(6) DEFAULT NULL,
`grid_voltage_t` smallint(6) DEFAULT NULL,
`battery_capacity` tinyint(4) DEFAULT NULL,
`battery_power` smallint(6) DEFAULT NULL,
`feed_in_power` smallint(6) DEFAULT NULL,
`time_count_down` smallint(6) DEFAULT NULL,
`inverter_ac_power` smallint(6) DEFAULT NULL,
`consumeenergy` float(7,1) DEFAULT NULL,
`feedinenergy` float(7,1) DEFAULT NULL,
`power_dc1` smallint(6) DEFAULT NULL,
`power_dc2` smallint(6) DEFAULT NULL,
PRIMARY KEY (`uploadTime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

19
source/example.py Normal file
View File

@@ -0,0 +1,19 @@
from solaxx3.rs485 import SolaxX3
# adjust the serial port and baud rate as necessary
s = SolaxX3(port="/dev/ttyUSB0", baudrate=115200)
if s.connect():
s.read_all_registers()
available_stats = s.list_register_names()
for stat in available_stats:
print(stat)
battery_temperature = s.read("temperature_battery")
print(f"\n\nBattery temperature: {s.read('temperature_battery')}")
else:
print("Cannot connect to the Modbus Server/Slave")
exit()

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

26
source/pyproject.toml Normal file
View File

@@ -0,0 +1,26 @@
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "solaxx3"
version = "0.1.0"
description = "Read Solax X3 inverter registers via modbus interface (RS-485)"
readme = "README.md"
authors = [{ name = "Flavius Moldovan", email = "mkfam@protonmail.com" }]
license = { file = "LICENSE" }
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
keywords = ["Solax", "solax-x3", "solaxx3", "solar inverter", "RTU", "MODBUS"]
dependencies = [
"pymodbus[serial] >= 3.0.0",
]
requires-python = ">=3.8"
[project.urls]
Homepage = "https://github.com/mkfam7/solaxx3"

3
source/setup.py Normal file
View File

@@ -0,0 +1,3 @@
from setuptools import setup
setup()

2
src/solaxx3/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Version of the Solax RTU package
__version__ = "0.0.6"

589
src/solaxx3/registers.py Normal file
View File

@@ -0,0 +1,589 @@
class SolaxRegistersInfo:
__registers = {
# input registers
"grid_voltage": {
"address": 0x0000,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "Grid voltage",
},
"grid_current": {
"address": 0x0001,
"register_type": "input",
"data_format": "int16",
"si_adj": 10,
"signed": True,
"data_unit": "A",
"data_length": 1,
"description": "Grid current",
},
"grid_power": {
"address": 0x0002,
"register_type": "input",
"data_format": "int16",
"si_adj": 1,
"signed": True,
"data_unit": "W",
"data_length": 1,
"description": "Grid power",
},
"pv_voltage_1": {
"address": 0x0003,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "PV Voltage 1",
},
"pv_voltage_2": {
"address": 0x0004,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "PV Voltage 2",
},
"pv_current_1": {
"address": 0x0005,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "A",
"data_length": 1,
"description": "PV Current 1",
},
"pv_current_2": {
"address": 0x0006,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "A",
"data_length": 1,
"description": "PV Current 2",
},
"grid_frequency": {
"address": 0x0007,
"register_type": "input",
"data_format": "uint16",
"si_adj": 100,
"signed": False,
"data_unit": "Hz",
"data_length": 1,
"description": "Grid frequency",
},
"radiator_temperature": {
"address": 0x0008,
"register_type": "input",
"data_format": "int16",
"si_adj": 1,
"signed": True,
"data_unit": "C",
"data_length": 1,
"description": "Radiator temperature",
},
"run_mode": {
"address": 0x0009,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_unit": "",
"data_length": 1,
"description": "Inverter status",
},
"power_dc1": {
"address": 0x000A,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_unit": "W",
"data_length": 1,
"description": "DC Power String 1",
},
"power_dc2": {
"address": 0x000B,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_unit": "W",
"data_length": 1,
"description": "DC Power String 2",
},
"temperature_fault_value": {
"address": 0x000C,
"register_type": "input",
"data_format": "int16",
"si_adj": 1,
"signed": True,
"data_unit": "C",
"data_length": 1,
"description": "Temperature fault value",
},
"pv1_volt_fault_value": {
"address": 0x000D,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "PV1 volt fault value",
},
"pv2_volt_fault_value": {
"address": 0x000E,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "PV2 volt fault value",
},
"gfci_fault_value": {
"address": 0x000F,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1000,
"signed": False,
"data_unit": "A",
"data_length": 1,
"description": "GFCI fault value",
},
"grid_fault_value": {
"address": 0x0010,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "Grid fault value",
},
"grid_freq__fault__value_t": {
"address": 0x0011,
"register_type": "input",
"data_format": "uint16",
"si_adj": 100,
"signed": False,
"data_unit": "Hz",
"data_length": 1,
"description": "Grid frequency fault value t",
},
"dci_fault_value": {
"address": 0x0012,
"register_type": "input",
"data_format": "uint16",
"si_adj": 100,
"signed": False,
"data_unit": "A",
"data_length": 1,
"description": "DCI fault value",
},
"time_count_down": {
"address": 0x0013,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1000,
"signed": False,
"data_unit": "seconds",
"data_length": 1,
"description": "Time remaining until the current operation finishes (e.g. checking)",
},
"battery_voltage_charge_1": {
"address": 0x0014,
"register_type": "input",
"data_format": "int16",
"si_adj": 10,
"signed": True,
"data_unit": "V",
"data_length": 1,
"description": "Battery voltage charge 1",
},
"battery_current_charge_1": {
"address": 0x0015,
"register_type": "input",
"data_format": "int16",
"si_adj": 10,
"signed": True,
"data_unit": "V",
"data_length": 1,
"description": "Battery current charge 1",
},
"battery_power_charge1": {
"address": 0x0016,
"register_type": "input",
"data_format": "int16",
"si_adj": 1,
"signed": True,
"data_unit": "W",
"data_length": 1,
"description": "Battery power charge 1",
},
"bms_connect_state": {
"address": 0x0017,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_unit": "",
"data_length": 1,
"description": "BMS connect state",
},
"temperature_battery": {
"address": 0x0018,
"register_type": "input",
"data_format": "int16",
"si_adj": 1,
"signed": True,
"data_unit": "C",
"data_length": 1,
"description": "Temperature of the battery",
},
"battery_capacity": {
"address": 0x001C,
"register_type": "input",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_unit": "%",
"data_length": 1,
"description": "Battery charge percentage",
},
"output_energy_charge": {
"address": 0x001D,
"register_type": "input",
"data_format": "uint32",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Output energy charge",
},
"output_energy_charge_today": {
"address": 0x0020,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 1,
"description": "Output energy charge today",
},
"input_energy_charge": {
"address": 0x0021,
"register_type": "input",
"data_format": "uint32",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Input energy charge",
},
"input_energy_charge_today": {
"address": 0x0023,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 1,
"description": "Input energy charge today",
},
"bms_charge_max_current": {
"address": 0x0024,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "A",
"data_length": 1,
"description": "BMS charge max current",
},
"bms_discharge_max_current": {
"address": 0x0025,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "A",
"data_length": 1,
"description": "BMS discharge max current",
},
"power_to_ev": {
"address": 0x0026,
"register_type": "input",
"data_format": "int32",
"si_adj": 1,
"signed": True,
"data_unit": "W",
"data_length": 2,
"description": "Power to EV",
},
"ref_power_to_ev": {
"address": 0x0028,
"register_type": "input",
"data_format": "int16",
"si_adj": 1,
"signed": False,
"data_unit": "W",
"data_length": 1,
"description": "ref_power_to_ev",
},
"feed_in_power": {
"address": 0x0046,
"register_type": "input",
"data_format": "int32",
"si_adj": 1,
"signed": True,
"data_unit": "W ",
"data_length": 2,
"description": "Feed-in power obtained from Meter or CT",
},
"energy_to_grid_meter": {
"address": 0x0048,
"register_type": "input",
"data_format": "uint32",
"si_adj": 1,
"signed": False,
"data_unit": "KWh",
"si_adj": 100,
"data_length": 2,
"description": "Energy to grid meter value",
},
"energy_from_grid_meter": {
"address": 0x004A,
"register_type": "input",
"data_format": "uint32",
"si_adj": 100,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Energy from grid meter value",
},
"energy_to_grid_today": {
"address": 0x0050,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 1,
"description": "The quantity of energy sent to grid today",
},
"energy_to_grid_total": {
"address": 0x0052,
"register_type": "input",
"data_format": "uint32",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Total energy sent to grid",
},
"grid_voltage_r": {
"address": 0x006A,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "Grid voltage for R phase",
},
"grid_voltage_s": {
"address": 0x006E,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "Grid voltage for S phase",
},
"grid_voltage_t": {
"address": 0x0072,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "V",
"data_length": 1,
"description": "Grid voltage for T phase",
},
"echarge_today": {
"address": 0x0091,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 1,
"description": "Echarge energy today",
},
"solar_energy_total": {
"address": 0x0094,
"register_type": "input",
"data_format": "uint32",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Total solar energy",
},
"solar_energy_today": {
"address": 0x0096,
"register_type": "input",
"data_format": "uint16",
"si_adj": 10,
"signed": False,
"data_unit": "KWh",
"data_length": 1,
"description": "Solar energy today",
},
"feed_in_energy_today": {
"address": 0x0098,
"register_type": "input",
"data_format": "uint32",
"si_adj": 100,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Energy to grid meter for today",
},
"consumption_energy_today": {
"address": 0x009A,
"register_type": "input",
"data_format": "uint32",
"si_adj": 100,
"signed": False,
"data_unit": "KWh",
"data_length": 2,
"description": "Energy from grid meter for today",
},
# holding registers
"serial_number": {
"address": 0x0000,
"register_type": "holding",
"data_format": "varchar",
"signed": False,
"data_length": 7,
"description": "Inverter' serial number",
},
"firmware_version_dsp": {
"address": 0x007D,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Firmware version DSP",
},
"hardware_version_dsp": {
"address": 0x007E,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Hardware version DSP",
},
"firmware_version_modbus_rtu": {
"address": 0x0082,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Firmware version MODBUS RTU",
},
"firmware_version_arm": {
"address": 0x0083,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Firmware version ARM",
},
"firmware_version_arm_bootloader": {
"address": 0x0084,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Firmware version ARM Bootloader",
},
"rtc_datetime": {
"address": 0x0085,
"register_type": "holding",
"data_format": "datetime",
"si_adj": 1,
"signed": False,
"data_length": 6,
"description": "RTC datetime",
},
"registration_code": {
"address": 0x00AA,
"register_type": "holding",
"data_format": "varchar",
"si_adj": 1,
"signed": False,
"data_length": 6,
"description": "Registration code for external module",
},
"inverter_power_type": {
"address": 0x00BA,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Inverter power type",
},
"user_password": {
"address": 0x00E0,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "User password",
},
"advanced_password": {
"address": 0x00E1,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Advanced password",
},
"machine_type": {
"address": 0x0105,
"register_type": "holding",
"data_format": "uint16",
"si_adj": 1,
"signed": False,
"data_length": 1,
"description": "Machine type: 1:X1, 3:X3",
},
}
def get_register_info(self, name: str):
return self.__registers[name]
def list_register_names(self):
return list(self.__registers.keys())

169
src/solaxx3/rs485.py Normal file
View File

@@ -0,0 +1,169 @@
from typing import Any
# from pymodbus.client.sync import ModbusSerialClient
from pymodbus.client import ModbusSerialClient
from datetime import date, datetime, timedelta
from struct import *
from solaxx3.registers import SolaxRegistersInfo
from time import sleep, perf_counter
class SolaxX3:
connected: bool = False
def __init__(
self,
method="rtu",
port="/dev/ttyUSB0",
baudrate=115200,
timeout=3,
parity="N",
stopbits=1,
bytesize=8,
) -> None:
self._input_registers_values_list = []
self._holding_registers_values_list = []
self.client = ModbusSerialClient(
method=method,
port=port,
baudrate=baudrate,
timeout=timeout,
parity=parity,
stopbits=stopbits,
bytesize=bytesize,
)
def connect(self) -> bool:
self.connected = self.client.connect()
return self.connected
def _join_msb_lsb(self, msb: int, lsb: int) -> int:
return (msb << 16) | lsb
def _unsigned16(self, type: str, addr: int, count: int = 1, unit: int = 1) -> int:
self._input_registers_values_list
if type == "input":
return self._input_registers_values_list[addr]
elif type == "holding":
return self._holding_registers_values_list[addr]
def _readRegisterRange(
self, type: str, addr: int, count: int = 1, unit: int = 1
) -> list:
if type == "input":
return self._input_registers_values_list[addr : addr + count]
elif type == "holding":
return self._holding_registers_values_list[addr : addr + count]
def _twos_complement(self, number: int, bits: int) -> int:
"""
Compute the 2's complement of the int value val
"""
# if sign bit is set e.g., 8bit: 128-255
if (number & (1 << (bits - 1))) != 0:
# compute negative value
number = number - (1 << bits)
return number
def _read_register(self, register_type: str, register_info: dict) -> Any:
"""Read the values from a register based on length and sign
Parameters:
register_info:dict - dictionary with register definition fields
"""
if "int" in register_info["data_format"]:
if register_info["data_length"] == 1:
val = self._unsigned16(register_type, register_info["address"])
if register_info["data_length"] == 2:
val = self._join_msb_lsb(
self._unsigned16(register_type, register_info["address"] + 1),
self._unsigned16(register_type, register_info["address"]),
)
if register_info["signed"]:
val = self._twos_complement(val, register_info["data_length"] * 16)
val = val / register_info["si_adj"]
elif "varchar" in register_info["data_format"]:
block = self._readRegisterRange(
register_type, register_info["address"], register_info["data_length"]
)
sn = []
for i in range(register_info["data_length"]):
first_byte, second_byte = unpack(
"BB", int.to_bytes(block[i], 2, "little")
)
if not second_byte == 0x0:
sn.append(chr(second_byte))
if not first_byte == 0x0:
sn.append(chr(first_byte))
val = "".join(sn)
elif "datetime" in register_info["data_format"]:
sec, min, hr, day, mon, year = self._readRegisterRange(
register_type, register_info["address"], register_info["data_length"]
)
inverter_datetime = (
f"{(year+2000):02}-{mon:02}-{day:02} {hr:02}:{min:02}:{sec:02}"
)
val = datetime.strptime(inverter_datetime, "%Y-%m-%d %H:%M:%S")
return val
def read_register(self, register_info: dict) -> tuple:
"""Read the values from a register based on length and sign
Parameters:
register_info:dict - dictionary with register definition fields
"""
val = self._read_register(register_info["register_type"], register_info)
if not "data_unit" in register_info:
return (val, "N/A")
return (val, register_info["data_unit"])
def read(self, name: str):
"""Retrieve the value for the register with the provided name"""
r = SolaxRegistersInfo()
register_info = r.get_register_info(name)
value = self.read_register(register_info)
return value
def list_register_names(self):
r = SolaxRegistersInfo()
return r.list_register_names()
def read_all_registers(self) -> None:
self._input_registers_values_list = []
self._holding_registers_values_list = []
read_block_length = 100
for i in range(3):
address = i * read_block_length
values_list = self.client.read_input_registers(
address=address, count=read_block_length, slave=1
).registers
self._input_registers_values_list.extend(values_list)
for i in range(3):
address = i * read_block_length
values_list = self.client.read_holding_registers(
address=address, count=read_block_length, slave=1
).registers
self._holding_registers_values_list.extend(values_list)