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

View File

@@ -0,0 +1,361 @@
"""The formatted register values."""
import datetime
altered_input_register_values = [
236.4,
4.0,
1243.0,
629.5,
737.1,
1.4,
0.6,
49.96,
44.0,
2.0,
940.0,
484.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
428.1,
0.2,
128.0,
1.0,
29.0,
1.0,
0.0,
2.0,
98.0,
3914.1,
0.4,
4535.7,
8.2,
5.2,
30.0,
12288.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
20792.6,
3776.9,
0.0,
0.0,
0.0,
0.0,
56.9,
33161.0,
1.0,
0.0,
0.0,
0.0,
0.0,
235.9,
4.0,
937.0,
49.97,
232.3,
1.3,
45.0,
49.98,
235.4,
1.5,
261.0,
49.98,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
2.0,
2.0,
13476.8,
107.5,
69.1,
0.0,
0.0,
0.2,
35229.0,
68.0,
41.3,
1.75,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
21.3,
19.0,
3.344,
3.329,
0.0,
94.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
-1247.0,
-1.0,
2338.0,
-9973.0,
5000.0,
-5000.0,
0.0,
0.0,
245.0,
12042.0,
0.0,
0.0,
100.0,
40.0,
4.0,
0.0,
1.0,
]
altered_holding_register_values = [
"H34T15H9022043",
"Solax ",
" ",
900.0,
15.0,
900.0,
195.5,
264.5,
49.5,
50.5,
1.0,
0.0,
259.9,
195.5,
264.5,
47.5,
51.5,
100.0,
0.0,
1.0,
1.0,
1.0,
0.94,
0.9,
20.0,
50.0,
80.0,
100.0,
1.05,
1.0,
6.0,
0.0,
0.0,
213.9,
223.1,
236.9,
246.1,
0.0,
10.0,
0.0,
0.0,
20.0,
5.0,
0.0,
7500.0,
-7500.0,
47.55,
50.05,
195.5,
253.0,
0.0,
0.0,
47.55,
50.05,
195.5,
253.0,
0.0,
1.0,
900.0,
29.0,
0.0,
1.0,
1.0,
6.0,
27.0,
8.0,
datetime.datetime(2024, 3, 19, 14, 58, 43),
0.0,
0.0,
1.0,
0.0,
0.0,
20.0,
21.0,
0.0,
30.0,
0.0,
30.0,
0.0,
0.0,
0.0,
245.0,
0.0,
"SPNXLAQHWX",
1.0,
0.0,
0.0,
0.0,
60000.0,
1500.0,
0.0,
20.0,
0.0,
15000.0,
0.0,
0.0,
0.3,
0.1,
0.1,
0.1,
0.3,
0.01,
0.01,
0.01,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
32.0,
1.0,
50.2,
5.0,
0.0,
0.0,
0.0,
0.0,
2014.0,
49.8,
2.0,
0.005,
0.0,
30.0,
50.2,
49.8,
0.0,
0.0,
0.0,
0.0,
3000.0,
900.0,
900.0,
3000.0,
3000.0,
1.0,
0.09,
220.0,
253.0,
257.6,
207.0,
0.1,
0.0,
100.0,
100.0,
100.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
3.0,
1.0,
0.0,
1.0,
1.0,
2.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
1.0,
0.0,
266.8,
181.8,
0.0,
0.0,
0.0,
0.0,
30.0,
0.0,
0.0,
0.0,
2000.0,
80.0,
1000.0,
40.0,
30.0,
800.0,
0.0,
0.0,
0.0,
0.0,
0.0,
50.0,
0.0,
]

View File

@@ -0,0 +1,51 @@
"""Module containing a connection class."""
from pathlib import Path
from .cursor import Cursor
from .error import Error
open_connections_file = Path(__file__).parent / "open_connections.txt"
class Connection:
"""Class mocking a connection to a MySQL server."""
def __init__(self, *args, consider_open=True, **kwargs):
self.open_connections += consider_open
self.open: bool = consider_open
def cursor(self) -> Cursor:
"""Return the cursor for the connection."""
self._cursor = Cursor()
return self._cursor
def close(self):
"""Close connection."""
if not self.open:
raise Error("Connection already closed")
self.open = False
self.open_connections -= 1
def commit(self):
"""Save all changes to the database."""
def is_connected(self) -> bool:
"""Return if the connection is open."""
return self.open
@property
def open_connections(self):
with open(open_connections_file, "r", encoding="utf-8") as f:
contents = f.read()
return int(contents)
@open_connections.setter
def open_connections(self, value: int):
value = value if value > 0 else 0
with open(open_connections_file, "w", encoding="utf-8") as f:
f.write(str(value))

View File

@@ -0,0 +1,10 @@
"""Module with mock implementation of interacting with a MySQL server."""
from .connection import Connection
from .error import Error
def connect(*args, **kwargs) -> Connection:
"""Mock connect to a MySQL server."""
return Connection()

View File

@@ -0,0 +1,56 @@
"""Module with cursor implementation."""
import json
from pathlib import Path
from .error import Error
config_file = Path(__file__).parent / "cursor_config.json"
class Cursor:
"""Cursor of a MySQL connection."""
def __init__(self, raise_err: bool = True):
if self._raise_error_in_init and raise_err:
raise Error
def execute(self, *args, **kwargs) -> None:
"""Mock execute a query."""
if self._raise_error:
raise Error
def executemany(self, *args, **kwargs) -> None:
"""Mock execute a query multiple times with different data."""
if self._raise_error:
raise Error
@property
def _raise_error(self):
return self._read_configs()["raise_error"]
@property
def _raise_error_in_init(self):
return self._read_configs()["raise_error_in_init"]
@_raise_error.setter
def _raise_error(self, value: int):
data = self._read_configs()
data["raise_error"] = value
self._save_configs(data)
@_raise_error_in_init.setter
def _raise_error_in_init(self, value: int):
data = self._read_configs()
data["raise_error_in_init"] = value
self._save_configs(data)
def _read_configs(self):
with open(config_file, "r", encoding="utf-8") as f:
return json.loads(f.read())
def _save_configs(self, data):
with open(config_file, "w", encoding="utf-8") as f:
f.write(json.dumps(data, indent=4))

View File

@@ -0,0 +1,4 @@
{
"raise_error_in_init": false,
"raise_error": false
}

View File

@@ -0,0 +1,5 @@
"""Module containing the basic error class of this package."""
class Error(Exception):
"""Basic error class of this package."""

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,30 @@
"""Class to mimick `pymodbus.client`. It is used when running the tests."""
from collections import namedtuple
from .registers_output import raw_holding_register_values, raw_input_register_values
from .utils import getnext
Registers = namedtuple("Registers", ["registers"])
class ModbusSerialClient:
"""Class mimicking `pymodbus.client.ModbusSerialClient`."""
def __init__(self, *_, **__) -> None:
self._holding_registers_gen = getnext(raw_holding_register_values)
self._input_registers_gen = getnext(raw_input_register_values)
def connect(self):
"""Mimick connecting the inverter and return the success code."""
return True
def read_holding_registers(self, count, *_, **__):
"""Read holding register values from inverter."""
return Registers([next(self._holding_registers_gen) for _ in range(count)])
def read_input_registers(self, count, *_, **__):
"""Read input register values from inverter."""
return Registers([next(self._input_registers_gen) for _ in range(count)])

View File

@@ -0,0 +1,806 @@
"""Raw register values, just read from the inverter."""
raw_input_register_values = [
2364,
40,
1243,
6295,
7371,
14,
6,
4996,
44,
2,
940,
484,
0,
0,
0,
0,
0,
0,
0,
0,
4281,
2,
128,
1,
29,
1,
0,
2,
98,
39141,
0,
0,
4,
45357,
0,
82,
52,
300,
12288,
0,
1,
0,
52,
10,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
47644,
31,
50010,
5,
0,
0,
0,
0,
569,
0,
3930,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2359,
40,
937,
4997,
2323,
13,
45,
4998,
2354,
15,
261,
4998,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2,
0,
2,
0,
3696,
2,
1075,
0,
0,
0,
691,
0,
0,
0,
2,
0,
24610,
5,
680,
0,
4130,
0,
175,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
213,
190,
3344,
3329,
0,
94,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
64289,
65535,
65535,
65535,
2338,
0,
55563,
65535,
5000,
0,
60536,
65535,
0,
0,
0,
0,
245,
0,
12042,
0,
0,
0,
100,
40,
4,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]
raw_holding_register_values = [
18483,
13396,
12597,
18489,
12338,
12848,
13363,
21359,
27745,
30752,
8224,
8224,
8224,
8224,
8224,
8224,
8224,
8224,
8224,
8224,
8224,
1800,
900,
15,
900,
1955,
2645,
4950,
5050,
1,
0,
2599,
1955,
2645,
4750,
5150,
275,
100,
0,
100,
100,
100,
94,
90,
20,
50,
80,
100,
105,
100,
6,
0,
0,
2139,
2231,
2369,
2461,
0,
10,
0,
0,
20,
5,
0,
7500,
58036,
4755,
5005,
1955,
2530,
0,
0,
4755,
5005,
1955,
2530,
0,
1,
900,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
29,
0,
1,
1,
0,
6,
27,
8,
43,
58,
14,
19,
3,
24,
0,
0,
1,
0,
0,
200,
210,
0,
10240,
30,
25610,
12830,
0,
0,
0,
15127,
0,
0,
0,
0,
0,
30,
0,
0,
0,
0,
0,
0,
0,
2450,
0,
21328,
20056,
19521,
20808,
22360,
1,
0,
0,
0,
0,
0,
60000,
1500,
0,
20,
0,
15000,
0,
0,
300,
100,
100,
100,
300,
10,
10,
10,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
32,
0,
0,
1,
5020,
5,
0,
0,
0,
0,
2014,
4980,
2,
5,
0,
30,
5020,
4980,
0,
0,
0,
0,
30000,
900,
900,
30000,
30000,
1000,
900,
2200,
2530,
2576,
2070,
10,
0,
100,
100,
100,
0,
10,
1,
0,
0,
0,
0,
0,
3,
1,
0,
1,
1,
2,
0,
0,
0,
100,
0,
1,
0,
0,
0,
0,
1,
0,
2668,
1818,
0,
0,
0,
0,
0,
0,
30,
0,
0,
0,
2000,
80,
1000,
40,
30,
800,
0,
0,
0,
0,
0,
0,
0,
0,
0,
500,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
20,
95,
1000,
30,
60,
0,
15127,
0,
2000,
0,
0,
2000,
30,
80,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]

View File

@@ -0,0 +1,11 @@
"""Mock `pymodbus` utils."""
from typing import Any
def getnext(iterable: list, default: Any = 0):
"""Yield one item at a time and yield `default` when iterable is exhausted."""
yield from iterable
while True:
yield default

View File

@@ -0,0 +1,68 @@
import sys
from os import environ
from pathlib import Path
from unittest import TestCase
from tests.mock_packages.mysql.connection import Connection
from tests.mock_packages.mysql.cursor import Cursor
environ["MYSQL_DB_USERNAME"] = ""
environ["MYSQL_DB_HOST_IP"] = ""
environ["MYSQL_DB_PASSWORD"] = ""
environ["MYSQL_DB_DATABASE"] = ""
current: Path = Path().resolve()
paths_to_extend = [current / "tests" / "mock_packages", current / "src"]
for path in paths_to_extend:
path_as_string = str(path)
if path_as_string not in sys.path:
sys.path.append(path_as_string)
from database import read_and_save
class MysqlConnectionLeakageTests(TestCase):
"""Tests to see if the class leaves any connections open after its processes."""
def _open_connections(self):
return Connection(consider_open=False).open_connections
def setUp(self) -> None:
"""Set up the data after each test case."""
Cursor(raise_err=False)._raise_error = False
Cursor(raise_err=False)._raise_error_in_init = False
Connection().open_connections = 0
def test_bulk_transfer_without_error(self):
"""Test if `bulk_transfer` leaves any hanging connections in normal operations."""
for _ in range(3):
read_and_save.bulk_save()
self.assertEqual(self._open_connections(), 0)
def test_bulk_transfer_with_error_in_cursor_init(self):
"""Test if `bulk_transfer` leaves any hanging connections with errors when instantiating
cursor."""
Cursor()._raise_error_in_init = True
for _ in range(3):
try:
read_and_save.bulk_save()
except:
pass
self.assertEqual(self._open_connections(), 0)
def test_bulk_transfer_with_error_in_cursor(self):
"""Test if `bulk_transfer` leaves any hanging connections with errors when executing
a query in the cursor."""
Cursor()._raise_error = True
for _ in range(3):
try:
read_and_save.bulk_save()
except:
pass
self.assertEqual(self._open_connections(), 0)

View File

@@ -0,0 +1,254 @@
"""
Test scenarios:
Report any of the following anomalies:
1. Not valid dictionary syntax [done]
2. Dictionary key duplicates [done]
3. "address" duplicates
4. The value field has different keys than: [done]
* address
* register_type
* data_format
* si_adj
* signed
* data_unit
* data_length
* description
5. "dataformat" starts with 'uint' but "signed" is different than "False"
6. "dataformat" starts with 'int' but "signed" is different than "True"
7. If "data_format" contains "int" in the name,
"data_length" != <the number inside "data_format"> / 16 or 1 if no number is found
in "data_length"
8. "data_unit" is not one of following:
{%, A, C, Hx, KWh, N/A, V, VA, Var, W, Wh, hour, second}
Note: values are case sensitive
9. "register_type" is different than {input, holding}
Note: no values are case sensitive
10. the key name contains "volt" but the "data_unit" is different than "V"
11. the key name contains "current" but the "data_unit" is different than "A"
12. "address" overlapping
"""
import re
import sys
import unittest
from itertools import filterfalse
from pathlib import Path
from src.solaxx3.solax_registers_info import SolaxRegistersInfo
import_path = Path().resolve() / "tests" / "mock_packages"
if str(import_path) not in sys.path:
sys.path.append(str(import_path))
REGISTER_FILE = "src/solaxx3/solax_registers_info.py"
class RegisterTests(unittest.TestCase):
"""Tests for SolaxRegistersInfo."""
registers = SolaxRegistersInfo._registers
def test_tc1_valid_dictionary(self):
"""Prints an error if `registers` is a dictionary."""
self.assertIsInstance(self.registers, dict)
def test_tc2_duplicate_keys(self):
"""Prints an error if 'registers' has any duplicate keys."""
PATTERN = r"""
"(?P<key>.*?)" # The key of the dictionary
\s*: # colon
\s*{ # start of value
"""
regex = re.compile(PATTERN, re.VERBOSE)
with open(REGISTER_FILE, "r", encoding="utf-8") as dictionary:
list_keys = []
for line_number, line in filterfalse(
lambda x: x[1].lstrip().startswith("#"), enumerate(dictionary, start=1)
):
re_match = regex.search(line)
if re_match is not None:
key = re_match.group("key")
self.assertNotIn(key, list_keys, f"line={line_number}")
list_keys.append(key)
def test_tc4_correct_value_field(self):
"""Prints an error if the keys do not have the following keys:
- address, register_type, data_format, si_adj, signed, data_unit, data_length,
description"""
VALID_SUBKEYS = [
"address",
"data_format",
"data_length",
"data_unit",
"description",
"register_type",
"si_adj",
"signed",
]
for register, register_value in self.registers.items():
self.assertListEqual(
sorted(list(register_value.keys())), VALID_SUBKEYS, f"dict={register!r}"
)
def test_tc5_correctly_unsigned(self):
"""Prints an error if subkey `data_format` starts with `uint` but `signed` is
different than False."""
for register, register_value in self.registers.items():
self.assertFalse(
register_value["data_format"].startswith("uint")
and register_value["signed"] is not False,
f"not correctly signed: dict='{register}'",
)
def test_tc6_correctly_signed(self):
"""Prints an error if subkey `data_format` starts with 'int' but `signed`
is different than True."""
for register, register_value in self.registers.items():
self.assertFalse(
register_value["data_format"].startswith("int")
and register_value["signed"] is not True,
f"not correctly signed: dict='{register}'",
)
def test_tc7_correct_data_length(self):
"""Prints an error if 'data_format' does not match 'data_length'."""
PATTERN = r"int(?P<bits>\d+)"
regex = re.compile(PATTERN)
for register, register_value in self.registers.items():
match = regex.search(register_value["data_format"])
if match:
bits = int(match.group("bits"))
self.assertEqual(
bits / 16, register_value["data_length"], f"dict={register!r}"
)
def test_tc8_correct_data_unit_value(self):
"""Prints an error if `data_unit` is not %, C, Hz, KWh, N/A, V, VA, Var,
W, Wh, hour, second."""
POSSIBLE_DATA_UNITS = (
"min",
"bps",
"%",
"A",
"C",
"Hz",
"KWh",
"N/A",
"V",
"VA",
"Var",
"W",
"Wh",
"hour",
"second",
)
for register, register_value in self.registers.items():
self.assertGreater(
len(register_value["data_unit"]),
0,
f"empty 'data_unit': dict={register!r}",
)
self.assertIn(
register_value["data_unit"],
POSSIBLE_DATA_UNITS,
f"invalid data_unit: '{register_value['data_unit']}'; 'dict='{register}'",
)
def test_tc9_correct_register_type(self):
"""Prints an error if `register_type` is not 'input' or 'holding'."""
POSSIBLE_REGISTER_TYPES = ("input", "holding")
for register, register_value in self.registers.items():
self.assertIn(
register_value["register_type"],
POSSIBLE_REGISTER_TYPES,
f"dict='{register}'",
)
def test_tc10_correct_data_unit_with_volt(self):
"""Prints an error if the key contains 'volt' but `data_unit` is differrent
than 'V'."""
for register, register_value in self.registers.items():
self.assertFalse(
"volt" in register
and register_value["data_unit"] != "V"
and "percent" not in register
and "ratio" not in register,
f"'data_unit' does not match name: dict={register!r}",
)
def test_tc11_correct_data_unit_with_current(self):
"""Prints an error if the key contains 'current' but `data_unit` is
differrent than 'A'."""
for register, register_value in self.registers.items():
self.assertFalse(
"current" in register and register_value["data_unit"] != "A",
f"'data_unit' does not match name: dict='{register}'",
)
def test_tc12_correct_addresses(self):
"""Prints an error if there is any address overlapping."""
holding_addresses: list = []
input_addresses: list = []
for register, register_value in self.registers.items():
addresses = (
holding_addresses
if register_value["register_type"] == "holding"
else input_addresses
)
data_length = register_value["data_length"]
for x in range(data_length):
address = register_value["address"] + x
self.assertNotIn(address, addresses, f"dict={register!r}")
addresses.append(address)
def test_tc13_subkey_duplicates(self):
"""Prints an error if there are any subkey duplicates."""
PATTERN = r"""
"(?P<subkey>.*?)" # dictionary subkey
\s*: # colon
\s*?[^[]+$ # value different than a dict
"""
regex = re.compile(PATTERN, re.VERBOSE)
with open(REGISTER_FILE, "r", encoding="utf-8") as dictionary:
subkeys = []
for line_number, line in filterfalse(
lambda x: x[1].lstrip().startswith("#"), enumerate(dictionary, start=1)
):
match = regex.search(line)
if match:
subkey = match.group("subkey")
self.assertNotIn(subkey, subkeys, f"line={line_number}")
subkeys.append(subkey)
else:
subkeys.clear()
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,48 @@
"""Module to test solaxx3."""
import sys
import unittest
from pathlib import Path
from src.solaxx3 import solaxx3
from tests.final_result import (
altered_holding_register_values,
altered_input_register_values,
)
import_path = Path().resolve() / "tests" / "mock_packages"
if str(import_path) not in sys.path:
sys.path.append(str(import_path))
class Solaxx3Tests(unittest.TestCase):
"""Tests for SolaxX3."""
def test_connection(self):
"""Test if SolaxX3 connected."""
s = solaxx3.SolaxX3()
s.connect()
self.assertEqual(s.connected, True)
def test_format_registers(self):
"""Test if the module correctly formats raw register values."""
result = []
s = solaxx3.SolaxX3()
s.read_all_registers()
regs = solaxx3.SolaxRegistersInfo().list_register_names()
for reg in regs:
result.append(s.read(reg)[0])
self.assertEqual(
result, altered_input_register_values + altered_holding_register_values
)
def test_are_registers_identical(self):
"""Test if holding registers' values are not equal to input registers' values."""
s = solaxx3.SolaxX3()
s.read_all_registers()
self.assertFalse(s._holding_registers_values == s._input_registers_values)