update to 1.0.2

This commit is contained in:
Norbert
2024-07-12 12:13:55 +02:00
parent 3a0fc1f9cd
commit 577596d9f3
44 changed files with 5860 additions and 1957 deletions

BIN
source/src/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,82 @@
Metadata-Version: 2.1
Name: solaxx3
Version: 1.0.2
Summary: Read Solax X3 inverter registers via modbus interface (RS-485)
Author-email: Flavius Moldovan <mkfam@protonmail.com>
License: 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.
Project-URL: Homepage, https://github.com/mkfam7/solaxx3
Keywords: Solax,solax-x3,solaxx3,solar inverter,RTU,MODBUS
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pymodbus[serial]>=3.0.0
![Build badge](https://github.com/mkfam7/solaxx3/actions/workflows/python-package.yml/badge.svg)
# 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.solaxx3 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,16 @@
LICENSE
README.md
pyproject.toml
setup.py
src/solaxx3/__init__.py
src/solaxx3/solax_registers_info.py
src/solaxx3/solaxx3.py
src/solaxx3/utils.py
src/solaxx3.egg-info/PKG-INFO
src/solaxx3.egg-info/SOURCES.txt
src/solaxx3.egg-info/dependency_links.txt
src/solaxx3.egg-info/requires.txt
src/solaxx3.egg-info/top_level.txt
tests/test_mysql_connector.py
tests/test_register_data.py
tests/test_solaxx3.py

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
pymodbus[serial]>=3.0.0

View File

@@ -0,0 +1 @@
solaxx3

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,168 +0,0 @@
from typing import Any
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)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
"""Class loading and storing data from the inverter."""
from datetime import datetime
from struct import unpack
from typing import Dict, Iterable, List, Literal, Tuple, Union
from pymodbus.client import ModbusSerialClient
from .solax_registers_info import FIELD_VALUES, FIELDS, SolaxRegistersInfo
from .utils import join_msb_lsb, twos_complement
REGISTER_VALUE = Union[float, str, datetime]
REGISTER_INFO = Dict[FIELDS, FIELD_VALUES]
class SolaxX3:
"""
Class interacting with values from the inverter.
Initialization parameters:
- method (default: rtu)
- port (default: /dev/ttyUSB0 )
- baudrate: Bits per second (default: 115,200 )
- timeout: Timeout for a request, in seconds. (default: 3)
- parity: 'E'ven, 'O'dd, 'N'one (default: N)
- stopbits: Number of stop bits 0-2 (default: 1)
- bytesize: Number of bits per byte (7 or 8) (default: 8)
"""
connected: bool = False
READ_BLOCK_LENGTH = 100
def __init__(
self,
method: str = "rtu",
port: str = "/dev/ttyUSB0",
baudrate: int = 115200,
timeout: int = 3,
parity: Literal["E", "O", "N"] = "N",
stopbits: Literal[0, 1, 2] = 1,
bytesize: Literal[7, 8] = 8,
) -> None:
self._input_registers_values: List[int] = []
self._holding_registers_values: List[int] = []
self.client: ModbusSerialClient = ModbusSerialClient(
method=method,
port=port,
baudrate=baudrate,
timeout=timeout,
parity=parity,
stopbits=stopbits,
bytesize=bytesize,
)
def connect(self) -> bool:
"""Connect to the inverter and return if it was successful."""
self.connected: bool = self.client.connect()
return self.connected
def _get_unsigned_16(self, value_type: str, address: int) -> int:
if value_type == "input":
return self._input_registers_values[address]
return self._holding_registers_values[address]
def _read_register_range(
self, value_type: str, address: int, count: int = 1
) -> list:
if value_type == "input":
return self._input_registers_values[address : address + count]
return self._holding_registers_values[address : address + count]
def _read_format_register_value(
self, register_info: REGISTER_INFO
) -> REGISTER_VALUE:
"""Read the values from a register based on length and sign
Parameters:
register_info:dict - dictionary with register definition fields
"""
if self._is_register_type_integer(register_info):
value = self._get_integer_value(register_info)
value = value / register_info["si_adj"]
elif self._is_register_type_string(register_info):
value = self._get_string_value(register_info)
else:
value = self._get_datetime_value(register_info)
return value
def _get_datetime_value(self, register_info: REGISTER_INFO) -> datetime:
register_type = register_info["register_type"]
sec, minute, hr, day, mon, year = self._read_register_range(
register_type, register_info["address"], register_info["data_length"]
)
inverter_datetime = f"{year:02}-{mon:02}-{day:02} {hr:02}:{minute:02}:{sec:02}"
value = datetime.strptime(inverter_datetime, "%y-%m-%d %H:%M:%S")
return value
def _is_register_type_datetime(self, register_info: REGISTER_INFO) -> bool:
return "datetime" in register_info["data_format"]
def _get_string_value(self, register_info: REGISTER_INFO) -> str:
characters: List[str] = []
register_type: Literal["input", "holding"] = register_info["register_type"]
block = self._read_register_range(
register_type, register_info["address"], register_info["data_length"]
)
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:
characters.append(chr(second_byte))
if not first_byte == 0x0:
characters.append(chr(first_byte))
return "".join(characters)
def _is_register_type_string(self, register_info: REGISTER_INFO) -> bool:
return "varchar" in register_info["data_format"]
def _get_integer_value(self, register_info: REGISTER_INFO) -> int:
register_type = register_info["register_type"]
val = 0
if register_info["data_length"] == 1:
val = self._get_unsigned_16(register_type, register_info["address"])
if register_info["data_length"] == 2:
val = join_msb_lsb(
self._get_unsigned_16(register_type, register_info["address"] + 1),
self._get_unsigned_16(register_type, register_info["address"]),
)
if register_info["signed"]:
val = twos_complement(val, register_info["data_length"] * 16)
return val
def _is_register_type_integer(self, register_info: REGISTER_INFO) -> bool:
return "int" in register_info["data_format"]
def _read_register_value(
self, register_info: REGISTER_INFO
) -> Tuple[REGISTER_VALUE, str]:
"""Read the values from a register based on length and sign.
Parameters:
:param register_info: dictionary with register definition fields
:return: Tuple containing the value and data unit
"""
val = self._read_format_register_value(register_info)
return (val, register_info["data_unit"])
def read(self, name: str) -> Tuple[REGISTER_VALUE, str]:
"""Retrieve the value for the register with the provided name"""
registers = SolaxRegistersInfo()
register_info = registers.get_register_info(name)
value_data_unit = self._read_register_value(register_info)
return value_data_unit
def list_register_names(self) -> list:
"""Return all registers defined in register info."""
r = SolaxRegistersInfo()
return r.list_register_names()
def read_all_registers(self) -> None:
"""Read all register values from inverter."""
self._input_registers_values: List[int] = []
self._holding_registers_values: List[int] = []
self._read_input_registers()
self._read_holding_registers()
def _read_holding_registers(self):
for count in range(4):
address: int = count * self.READ_BLOCK_LENGTH
values: Iterable = self.client.read_holding_registers(
address=address, count=self.READ_BLOCK_LENGTH, slave=1
).registers
self._holding_registers_values.extend(values)
def _read_input_registers(self):
for count in range(4):
address: int = count * self.READ_BLOCK_LENGTH
values: Iterable = self.client.read_input_registers(
address=address, count=self.READ_BLOCK_LENGTH, slave=1
).registers
self._input_registers_values.extend(values)

View File

@@ -0,0 +1,18 @@
"""SolaxX3 utils module performing binary operations."""
def join_msb_lsb(msb: int, lsb: int) -> int:
"""Join two 16-bit registers into a 32-bit register."""
return (msb << 16) | lsb
def twos_complement(number: int, bits: int) -> int:
"""Compute the 2's complement of the provided integer."""
# 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