* Code Cleanup (#158)


* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes

* optimise versionstring handling (#159)

- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment

* fix linter warning

* exclude *.pyi filese

* ignore some rules for tests

* cleanup (#160)

* Sonar qube 3 (#163)

fix SonarQube warnings in modbus.py

* Sonar qube 3 (#164)


* fix SonarQube warnings

* Sonar qube 3 (#165)

* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings

* Sonar qube 3 (#166)

* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning

* Sonar qube 4 (#169)

* add unit test for inverter.py

* fix SonarQube warning

* Sonar qube 5 (#170)

* fix SonarLints warnings

* use random IP adresses for unit tests

* Docker: The description ist missing (#171)

Fixes #167

* S allius/issue167 (#172)

* cleanup

* Sonar qube 6 (#174)

* test class ModbusConn

* Sonar qube 3 (#178)

* add more unit tests

* GEN3: don't crash on overwritten msg in the receive buffer

* improve test coverage und reduce test delays

* reduce cognitive complexity

* fix merge

* fix merge conflikt

* fix merge conflict

* S allius/issue182 (#183)

* GEN3: After inverter firmware update the 'Unknown Msg Type' increases continuously
Fixes #182

* add support for Controller serial no and MAC

* test hardening

* GEN3: add support for new messages of version 3 firmwares

* bump libraries to latest versions

- bump aiomqtt to version 2.3.0
- bump aiohttp to version 3.10.5

* improve test coverage

* reduce cognective complexity

* fix target preview

* remove dubbled fixtures

* increase test coverage

* Update README.md (#185)

update badges

* S allius/issue186 (#187)

* Parse more values in Server Mode
Fixes #186

* read OUTPUT_COEFFICIENT and MAC_ADDR in SrvMode

* fix unit test

* increase test coverage

* S allius/issue186 (#188)

* increase test coverage

* update changelog

* add dokumentation

* change default config

* Update README.md (#189)

Config file is now foldable
This commit is contained in:
Stefan Allius
2024-09-16 00:45:36 +02:00
committed by GitHub
parent a9dc7e6847
commit bfea38d9da
22 changed files with 1780 additions and 191 deletions

View File

@@ -30,7 +30,33 @@ def test_default_config():
validated = Config.conf_schema.validate(cnf)
except Exception:
assert False
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': False, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 688}}}
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {
'allow_all': False,
'R170000000000001': {
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'modbus_polling': False,
'monitor_sn': 0,
'suggested_area': '',
'sensor_list': 688},
'Y170000000000001': {
'modbus_polling': True,
'monitor_sn': 2000000000,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv3': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'suggested_area': '',
'sensor_list': 688}}}
def test_full_config():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -71,7 +97,37 @@ def test_read_empty():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {
'allow_all': False,
'R170000000000001': {
'suggested_area': '',
'modbus_polling': False,
'monitor_sn': 0,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
},
'Y170000000000001': {
'modbus_polling': True,
'monitor_sn': 2000000000,
'suggested_area': '',
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv3': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
}
}
}
defcnf = TstConfig.def_config.get('solarman')
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
@@ -93,7 +149,37 @@ def test_read_cnf1():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {
'allow_all': False,
'R170000000000001': {
'suggested_area': '',
'modbus_polling': False,
'monitor_sn': 0,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
},
'Y170000000000001': {
'modbus_polling': True,
'monitor_sn': 2000000000,
'suggested_area': '',
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv3': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
}
}
}
cnf = TstConfig.get('solarman')
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
defcnf = TstConfig.def_config.get('solarman')
@@ -106,7 +192,37 @@ def test_read_cnf2():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {
'allow_all': False,
'R170000000000001': {
'suggested_area': '',
'modbus_polling': False,
'monitor_sn': 0,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
},
'Y170000000000001': {
'modbus_polling': True,
'monitor_sn': 2000000000,
'suggested_area': '',
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv3': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
}
}
}
assert True == TstConfig.is_default('solarman')
def test_read_cnf3():
@@ -123,7 +239,37 @@ def test_read_cnf4():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {
'allow_all': False,
'R170000000000001': {
'suggested_area': '',
'modbus_polling': False,
'monitor_sn': 0,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
},
'Y170000000000001': {
'modbus_polling': True,
'monitor_sn': 2000000000,
'suggested_area': '',
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv3': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
}
}
}
assert False == TstConfig.is_default('solarman')
def test_read_cnf5():

View File

@@ -0,0 +1,84 @@
# test_with_pytest.py
import pytest
import asyncio
from mock import patch
from app.src.async_stream import AsyncStream
from app.src.gen3.connection_g3 import ConnectionG3
from app.src.gen3.talent import Talent
@pytest.fixture
def patch_async_init():
with patch.object(AsyncStream, '__init__') as conn:
yield conn
@pytest.fixture
def patch_talent_init():
with patch.object(Talent, '__init__') as conn:
yield conn
@pytest.fixture
def patch_healthy():
with patch.object(AsyncStream, 'healthy') as conn:
yield conn
@pytest.fixture
def patch_async_close():
with patch.object(AsyncStream, 'close') as conn:
yield conn
@pytest.fixture
def patch_talent_close():
with patch.object(Talent, 'close') as conn:
yield conn
class FakeReader():
def __init__(self):
self.on_recv = asyncio.Event()
async def read(self, max_len: int):
await self.on_recv.wait()
return b''
def feed_eof(self):
return
class FakeWriter():
def write(self, buf: bytes):
return
def get_extra_info(self, sel: str):
if sel == 'peername':
return 'remote.intern'
elif sel == 'sockname':
return 'sock:1234'
assert False
def is_closing(self):
return False
def close(self):
return
async def wait_closed(self):
return
def test_method_calls(patch_async_init, patch_talent_init, patch_healthy, patch_async_close, patch_talent_close):
spy1 = patch_async_init
spy2 = patch_talent_init
spy3 = patch_healthy
spy4 = patch_async_close
spy5 = patch_talent_close
reader = FakeReader()
writer = FakeWriter()
id_str = "id_string"
addr = ('proxy.local', 10000)
conn = ConnectionG3(reader, writer, addr,
remote_stream= None, server_side=True, id_str=id_str)
spy1.assert_called_once_with(conn, reader, writer, addr)
spy2.assert_called_once_with(conn, True, id_str)
conn.healthy()
spy3.assert_called_once()
conn.close()
spy4.assert_called_once()
spy5.assert_called_once()

View File

@@ -0,0 +1,89 @@
# test_with_pytest.py
import pytest
import asyncio
from mock import patch
from app.src.singleton import Singleton
from app.src.async_stream import AsyncStream
from app.src.gen3plus.connection_g3p import ConnectionG3P
from app.src.gen3plus.solarman_v5 import SolarmanV5
@pytest.fixture
def patch_async_init():
with patch.object(AsyncStream, '__init__') as conn:
yield conn
@pytest.fixture
def patch_solarman_init():
with patch.object(SolarmanV5, '__init__') as conn:
yield conn
@pytest.fixture(scope="module", autouse=True)
def module_init():
Singleton._instances.clear()
yield
@pytest.fixture
def patch_healthy():
with patch.object(AsyncStream, 'healthy') as conn:
yield conn
@pytest.fixture
def patch_async_close():
with patch.object(AsyncStream, 'close') as conn:
yield conn
@pytest.fixture
def patch_solarman_close():
with patch.object(SolarmanV5, 'close') as conn:
yield conn
class FakeReader():
def __init__(self):
self.on_recv = asyncio.Event()
async def read(self, max_len: int):
await self.on_recv.wait()
return b''
def feed_eof(self):
return
class FakeWriter():
def write(self, buf: bytes):
return
def get_extra_info(self, sel: str):
if sel == 'peername':
return 'remote.intern'
elif sel == 'sockname':
return 'sock:1234'
assert False
def is_closing(self):
return False
def close(self):
return
async def wait_closed(self):
return
def test_method_calls(patch_async_init, patch_solarman_init, patch_healthy, patch_async_close, patch_solarman_close):
spy1 = patch_async_init
spy2 = patch_solarman_init
spy3 = patch_healthy
spy4 = patch_async_close
spy5 = patch_solarman_close
reader = FakeReader()
writer = FakeWriter()
addr = ('proxy.local', 10000)
conn = ConnectionG3P(reader, writer, addr,
remote_stream= None, server_side=True, client_mode=False)
spy1.assert_called_once_with(conn, reader, writer, addr)
spy2.assert_called_once_with(conn, True, False)
conn.healthy()
spy3.assert_called_once()
conn.close()
spy4.assert_called_once()
spy5.assert_called_once()

View File

@@ -1,7 +1,7 @@
# test_with_pytest.py
import pytest, json
from app.src.infos import Register, ClrAtMidnight
from app.src.gen3.infos_g3 import InfosG3
import pytest, json, math
from app.src.infos import Register
from app.src.gen3.infos_g3 import InfosG3, RegisterMap
@pytest.fixture
def contr_data_seq(): # Get Time Request message
@@ -364,12 +364,12 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
if id == 'out_power_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "sn": "T170000000000001", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
if id == 'daily_gen_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "sn": "T170000000000001", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
elif id == 'power_pv1_123':
@@ -388,6 +388,32 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
tests +=1
assert tests==5
def test_build_ha_conf4(contr_data_seq, inv_data_seq):
i = InfosG3()
for key, result in i.parse (contr_data_seq):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq):
pass # side effect in calling i.parse()
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
if id == 'signal_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:56"]]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
assert tests==1
i.set_db_def_value(Register.MAC_ADDR, "00:a0:57:12:34:57")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
if id == 'signal_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:57"]]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
assert tests==1
def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3()
tests = 0
@@ -395,21 +421,21 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
if key == 'total' or key == 'inverter' or key == 'env':
assert update == True
tests +=1
assert tests==5
assert tests==8
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
tests = 0
for key, update in i.parse (inv_data_seq2):
if key == 'total' or key == 'env':
assert update == False
tests +=1
assert tests==3
assert tests==4
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "Max_Designed_Power": -1, "Output_Coefficient": 100.0, "No_Inputs": 2})
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
@@ -417,13 +443,12 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
assert update == False
tests +=1
elif key == 'env':
assert update == True
tests +=1
assert tests==3
assert tests==4
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3()
@@ -436,10 +461,10 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert update == True
tests +=1
assert tests==3
assert tests==4
assert json.dumps(i.db['total']) == json.dumps({})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
@@ -447,18 +472,17 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert update == False
tests +=1
assert tests==3
assert tests==4
assert json.dumps(i.db['total']) == json.dumps({})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
tests = 0
for key, update in i.parse (inv_data_seq2):
if key == 'total' or key == 'env':
assert update == True
tests +=1
assert tests==3
assert tests==4
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
@@ -496,3 +520,15 @@ def test_invalid_data_type(invalid_data_seq):
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
assert val == 1
def test_result_eval(inv_data_seq2: bytes):
# add eval to convert temperature from °F to °C
RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8'
i = InfosG3()
for _, _ in i.parse (inv_data_seq2):
pass # side effect is calling generator i.parse()
assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
del RegisterMap.map[0x00000514]['eval'] # remove eval

View File

@@ -86,7 +86,7 @@ def test_parse_4110(str_test_ip, device_data: bytes):
assert json.dumps(i.db) == json.dumps({
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"},
})
def test_parse_4210(inverter_data: bytes):
@@ -98,7 +98,7 @@ def test_parse_4210(inverter_data: bytes):
assert json.dumps(i.db) == json.dumps({
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000},
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},

View File

@@ -0,0 +1,235 @@
# test_with_pytest.py
import pytest
import asyncio
from mock import patch
from enum import Enum
from app.src.infos import Infos
from app.src.config import Config
from app.src.inverter import Inverter
from app.src.singleton import Singleton
from app.src.gen3.connection_g3 import ConnectionG3
from app.src.gen3.inverter_g3 import InverterG3
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
pytest_plugins = ('pytest_asyncio',)
# initialize the proxy statistics
Infos.static_init()
@pytest.fixture
def config_conn():
Config.act_config = {
'mqtt':{
'host': test_hostname,
'port': test_port,
'user': '',
'passwd': ''
},
'ha':{
'auto_conf_prefix': 'homeassistant',
'discovery_prefix': 'homeassistant',
'entity_prefix': 'tsun',
'proxy_node_id': 'test_1',
'proxy_unique_id': ''
},
'tsun':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True}
}
@pytest.fixture(scope="module", autouse=True)
def module_init():
Singleton._instances.clear()
yield
@pytest.fixture
def patch_conn_init():
with patch.object(ConnectionG3, '__init__', return_value= None) as conn:
yield conn
@pytest.fixture
def patch_conn_close():
with patch.object(ConnectionG3, 'close') as conn:
yield conn
class FakeReader():
def __init__(self):
self.on_recv = asyncio.Event()
async def read(self, max_len: int):
await self.on_recv.wait()
return b''
def feed_eof(self):
return
class FakeWriter():
def write(self, buf: bytes):
return
def get_extra_info(self, sel: str):
if sel == 'peername':
return 'remote.intern'
elif sel == 'sockname':
return 'sock:1234'
assert False
def is_closing(self):
return False
def close(self):
return
async def wait_closed(self):
return
class TestType(Enum):
RD_TEST_0_BYTES = 1
RD_TEST_TIMEOUT = 2
RD_TEST_EXCEPT = 3
test = TestType.RD_TEST_0_BYTES
@pytest.fixture
def patch_open_connection():
async def new_conn(conn):
await asyncio.sleep(0)
return FakeReader(), FakeWriter()
def new_open(host: str, port: int):
global test
if test == TestType.RD_TEST_TIMEOUT:
raise ConnectionRefusedError
elif test == TestType.RD_TEST_EXCEPT:
raise ValueError("Value cannot be negative") # Compliant
return new_conn(None)
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
def test_method_calls(patch_conn_init, patch_conn_close):
spy1 = patch_conn_init
spy2 = patch_conn_close
reader = FakeReader()
writer = FakeWriter()
addr = ('proxy.local', 10000)
inverter = InverterG3(reader, writer, addr)
inverter.l_addr = ''
inverter.r_addr = ''
spy1.assert_called_once()
spy1.assert_called_once_with(reader, writer, addr, None, True)
inverter.close()
spy2.assert_called_once()
@pytest.mark.asyncio
async def test_remote_conn(config_conn, patch_open_connection, patch_conn_close):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
spy1 = patch_conn_close
inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
await inverter.async_create_remote()
await asyncio.sleep(0)
assert inverter.remote_stream
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_remote_except(config_conn, patch_open_connection, patch_conn_close):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
spy1 = patch_conn_close
global test
test = TestType.RD_TEST_TIMEOUT
inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
await inverter.async_create_remote()
await asyncio.sleep(0)
assert inverter.remote_stream==None
test = TestType.RD_TEST_EXCEPT
await inverter.async_create_remote()
await asyncio.sleep(0)
assert inverter.remote_stream==None
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_mqtt_publish(config_conn, patch_open_connection, patch_conn_close):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
spy1 = patch_conn_close
Inverter.class_init()
inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
inverter._Talent__set_serial_no(serial_no= "123344")
inverter.new_data['inverter'] = True
inverter.db.db['inverter'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['inverter'] == False
inverter.new_data['env'] = True
inverter.db.db['env'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['env'] == False
Infos.new_stat_data['proxy'] = True
await inverter.async_publ_mqtt()
assert Infos.new_stat_data['proxy'] == False
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err, patch_conn_close):
_ = config_conn
_ = patch_open_connection
_ = patch_mqtt_err
assert asyncio.get_running_loop()
spy1 = patch_conn_close
Inverter.class_init()
inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
inverter._Talent__set_serial_no(serial_no= "123344")
inverter.new_data['inverter'] = True
inverter.db.db['inverter'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['inverter'] == True
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except, patch_conn_close):
_ = config_conn
_ = patch_open_connection
_ = patch_mqtt_except
assert asyncio.get_running_loop()
spy1 = patch_conn_close
Inverter.class_init()
inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
inverter._Talent__set_serial_no(serial_no= "123344")
inverter.new_data['inverter'] = True
inverter.db.db['inverter'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['inverter'] == True
inverter.close()
spy1.assert_called_once()

View File

@@ -0,0 +1,236 @@
# test_with_pytest.py
import pytest
import asyncio
from mock import patch
from enum import Enum
from app.src.infos import Infos
from app.src.config import Config
from app.src.inverter import Inverter
from app.src.singleton import Singleton
from app.src.gen3plus.connection_g3p import ConnectionG3P
from app.src.gen3plus.inverter_g3p import InverterG3P
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
pytest_plugins = ('pytest_asyncio',)
# initialize the proxy statistics
Infos.static_init()
@pytest.fixture
def config_conn():
Config.act_config = {
'mqtt':{
'host': test_hostname,
'port': test_port,
'user': '',
'passwd': ''
},
'ha':{
'auto_conf_prefix': 'homeassistant',
'discovery_prefix': 'homeassistant',
'entity_prefix': 'tsun',
'proxy_node_id': 'test_1',
'proxy_unique_id': ''
},
'solarman':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True}
}
@pytest.fixture(scope="module", autouse=True)
def module_init():
Singleton._instances.clear()
yield
@pytest.fixture
def patch_conn_init():
with patch.object(ConnectionG3P, '__init__', return_value= None) as conn:
yield conn
@pytest.fixture
def patch_conn_close():
with patch.object(ConnectionG3P, 'close') as conn:
yield conn
class FakeReader():
def __init__(self):
self.on_recv = asyncio.Event()
async def read(self, max_len: int):
await self.on_recv.wait()
return b''
def feed_eof(self):
return
class FakeWriter():
def write(self, buf: bytes):
return
def get_extra_info(self, sel: str):
if sel == 'peername':
return 'remote.intern'
elif sel == 'sockname':
return 'sock:1234'
assert False
def is_closing(self):
return False
def close(self):
return
async def wait_closed(self):
return
class TestType(Enum):
RD_TEST_0_BYTES = 1
RD_TEST_TIMEOUT = 2
RD_TEST_EXCEPT = 3
test = TestType.RD_TEST_0_BYTES
@pytest.fixture
def patch_open_connection():
async def new_conn(conn):
await asyncio.sleep(0)
return FakeReader(), FakeWriter()
def new_open(host: str, port: int):
global test
if test == TestType.RD_TEST_TIMEOUT:
raise ConnectionRefusedError
elif test == TestType.RD_TEST_EXCEPT:
raise ValueError("Value cannot be negative") # Compliant
return new_conn(None)
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
def test_method_calls(patch_conn_init, patch_conn_close):
spy1 = patch_conn_init
spy2 = patch_conn_close
reader = FakeReader()
writer = FakeWriter()
addr = ('proxy.local', 10000)
inverter = InverterG3P(reader, writer, addr, client_mode=False)
inverter.l_addr = ''
inverter.r_addr = ''
spy1.assert_called_once()
spy1.assert_called_once_with(reader, writer, addr, None, server_side=True, client_mode=False)
inverter.close()
spy2.assert_called_once()
@pytest.mark.asyncio
async def test_remote_conn(config_conn, patch_open_connection, patch_conn_close):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
spy1 = patch_conn_close
inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
await inverter.async_create_remote()
await asyncio.sleep(0)
assert inverter.remote_stream
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_remote_except(config_conn, patch_open_connection, patch_conn_close):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
spy1 = patch_conn_close
global test
test = TestType.RD_TEST_TIMEOUT
inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
await inverter.async_create_remote()
await asyncio.sleep(0)
assert inverter.remote_stream==None
test = TestType.RD_TEST_EXCEPT
await inverter.async_create_remote()
await asyncio.sleep(0)
assert inverter.remote_stream==None
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_mqtt_publish(config_conn, patch_open_connection, patch_conn_close):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
spy1 = patch_conn_close
Inverter.class_init()
inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
inverter._SolarmanV5__set_serial_no(snr= 123344)
inverter.new_data['inverter'] = True
inverter.db.db['inverter'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['inverter'] == False
inverter.new_data['env'] = True
inverter.db.db['env'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['env'] == False
Infos.new_stat_data['proxy'] = True
await inverter.async_publ_mqtt()
assert Infos.new_stat_data['proxy'] == False
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err, patch_conn_close):
_ = config_conn
_ = patch_open_connection
_ = patch_mqtt_err
assert asyncio.get_running_loop()
spy1 = patch_conn_close
Inverter.class_init()
inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
inverter._SolarmanV5__set_serial_no(snr= 123344)
inverter.new_data['inverter'] = True
inverter.db.db['inverter'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['inverter'] == True
inverter.close()
spy1.assert_called_once()
@pytest.mark.asyncio
async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except, patch_conn_close):
_ = config_conn
_ = patch_open_connection
_ = patch_mqtt_except
assert asyncio.get_running_loop()
spy1 = patch_conn_close
Inverter.class_init()
inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
inverter._SolarmanV5__set_serial_no(snr= 123344)
inverter.new_data['inverter'] = True
inverter.db.db['inverter'] = {}
await inverter.async_publ_mqtt()
assert inverter.new_data['inverter'] == True
inverter.close()
spy1.assert_called_once()

View File

@@ -1,10 +1,10 @@
# test_with_pytest.py
import pytest
import asyncio
from aiomqtt import MqttCodeError
from mock import patch
from enum import Enum
from enum import Enum
from app.src.singleton import Singleton
from app.src.config import Config
from app.src.infos import Infos
@@ -134,10 +134,20 @@ def patch_no_mqtt():
yield conn
@pytest.fixture
def patch_no_mqtt():
with patch.object(Mqtt, 'publish') as conn:
def patch_mqtt_err():
def new_publish(self, key, data):
raise MqttCodeError(None)
with patch.object(Mqtt, 'publish', new_publish) as conn:
yield conn
@pytest.fixture
def patch_mqtt_except():
def new_publish(self, key, data):
raise ValueError("Test")
with patch.object(Mqtt, 'publish', new_publish) as conn:
yield conn
@pytest.mark.asyncio
async def test_modbus_conn(patch_open):
@@ -205,10 +215,6 @@ async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open):
assert 1 == test
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
# check that the connection is released
for m in Message:
if (m.node_id == 'inv_2'):
assert False
@pytest.mark.asyncio
async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
@@ -242,3 +248,67 @@ async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
assert 2 == test
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_mqtt_err(config_conn, patch_mqtt_err, patch_open):
_ = config_conn
_ = patch_open
_ = patch_mqtt_err
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_0_BYTES
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
await asyncio.sleep(0.01)
test = 0
for m in Message:
if (m.node_id == 'inv_2'):
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
test += 1
if test == 1:
m.shutdown_started = False
m.reader.on_recv.set()
await asyncio.sleep(0.1)
assert m.state == State.closed
await asyncio.sleep(0.1)
else:
m.shutdown_started = True
m.reader.on_recv.set()
del m
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_mqtt_except(config_conn, patch_mqtt_except, patch_open):
_ = config_conn
_ = patch_open
_ = patch_mqtt_except
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_0_BYTES
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
await asyncio.sleep(0.01)
test = 0
for m in Message:
if (m.node_id == 'inv_2'):
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
test += 1
if test == 1:
m.shutdown_started = False
m.reader.on_recv.set()
await asyncio.sleep(0.1)
assert m.state == State.closed
await asyncio.sleep(0.1)
else:
m.shutdown_started = True
m.reader.on_recv.set()
del m
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0

View File

@@ -184,6 +184,35 @@ def device_rsp_msg(): # 0x1110
msg += b'\x15'
return msg
@pytest.fixture
def device_ind_msg2(): # 0x4110
msg = b'\xa5\xd4\x00\x10\x41\x02\x03' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def device_rsp_msg2(): # 0x1110
msg = b'\xa5\x0a\x00\x10\x11\x03\x03' +get_sn() +b'\x02\x01'
msg += total()
msg += hb()
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def invalid_start_byte(): # 0x4110
msg = b'\xa4\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
@@ -901,6 +930,54 @@ def test_read_two_messages2(config_tsun_allow_all, inverter_ind_msg, inverter_in
assert m._send_buffer==b''
m.close()
def test_read_two_messages3(config_tsun_allow_all, device_ind_msg2, device_rsp_msg2, inverter_ind_msg, inverter_rsp_msg):
# test device message received after the inverter masg
_ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg, (0,))
m.append_msg(device_ind_msg2)
assert 0 == m.sensor_list
m._init_new_client_conn()
m.read() # read complete msg, and dispatch msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 2
assert m.header_len==11
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.msg_recvd[0]['control']==0x4210
assert m.msg_recvd[0]['seq']=='02:02'
assert m.msg_recvd[0]['data_len']==0x199
assert m.msg_recvd[1]['control']==0x4110
assert m.msg_recvd[1]['seq']=='03:03'
assert m.msg_recvd[1]['data_len']==0xd4
assert '02b0' == m.db.get_db_value(Register.SENSOR_LIST, None)
assert 0x02b0 == m.sensor_list
assert m._forward_buffer==inverter_ind_msg+device_ind_msg2
assert m._send_buffer==inverter_rsp_msg+device_rsp_msg2
m._send_buffer = bytearray(0) # clear send buffer for next test
m._init_new_client_conn()
assert m._send_buffer==b''
m.close()
def test_unkown_frame_code(config_tsun_inv1, inverter_ind_msg_81, inverter_rsp_msg_81):
_ = config_tsun_inv1
m = MemoryStream(inverter_ind_msg_81, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.header_len==11
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x4210
assert str(m.seq) == '03:03'
assert m.data_len == 0x199
assert m._recv_buffer==b''
assert m._send_buffer==inverter_rsp_msg_81
assert m._forward_buffer==inverter_ind_msg_81
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
def test_unkown_message(config_tsun_inv1, unknown_msg):
_ = config_tsun_inv1
m = MemoryStream(unknown_msg, (0,))

View File

@@ -41,6 +41,7 @@ class MemoryStream(Talent):
self.send_msg_ofs = 0
self.test_exception_async_write = False
self.msg_recvd = []
self.remote_stream = None
def append_msg(self, msg):
self.__msg += msg
@@ -138,6 +139,26 @@ def msg_time_rsp_inv(): # Get Time Resonse message
def msg_time_invalid(): # Get Time Request message
return b'\x00\x00\x00\x13\x10R170000000000001\x94\x22'
@pytest.fixture
def msg_act_time(): # Act Time Indication message
return b'\x00\x00\x00\x1c\x10R170000000000001\x91\x99\x01\x00\x00\x01\x89\xc6\x53\x4d\x80'
@pytest.fixture
def msg_act_time_ofs(): # Act Time Indication message withoffset 3600
return b'\x00\x00\x00\x1c\x10R170000000000001\x91\x99\x01\x00\x00\x01\x89\xc6\x53\x5b\x90'
@pytest.fixture
def msg_act_time_ack(): # Act Time Response message
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x99\x02'
@pytest.fixture
def msg_act_time_cmd(): # Act Time Response message
return b'\x00\x00\x00\x14\x10R170000000000001\x70\x99\x02'
@pytest.fixture
def msg_act_time_inv(): # Act Time Indication message withoffset 3600
return b'\x00\x00\x00\x1b\x10R170000000000001\x91\x99\x00\x00\x01\x89\xc6\x53\x5b\x90'
@pytest.fixture
def msg_controller_ind(): # Data indication from the controller
msg = b'\x00\x00\x01\x2f\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
@@ -442,6 +463,26 @@ def msg_modbus_rsp21():
msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef'
return msg
@pytest.fixture
def msg_modbus_cmd_new():
msg = b'\x00\x00\x00\x20\x10R170000000000001'
msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x03\x30\x00'
msg += b'\x00\x30\x4a\xde'
return msg
@pytest.fixture
def msg_modbus_rsp20_new():
msg = b'\x00\x00\x00\x7e\x10R170000000000001'
msg += b'\x91\x87\x00\x01\xa3\x28\x00\x65\x01\x03\x60'
msg += b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x51\x09\x09\x17\x00\x17\x13\x88\x00\x40\x00\x00\x02\x58\x02\x23'
msg += b'\x00\x07\x00\x00\x00\x00\x01\x4f\x00\xab\x02\x40\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\xc0\x93\x00\x00'
msg += b'\x00\x00\x33\xad\x00\x09\x00\x00\x98\x1c\x00\x00\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\xa7\xab'
return msg
@pytest.fixture
def broken_recv_buf(): # There are two message in the buffer, but the second has overwritten the first partly
msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
@@ -892,6 +933,7 @@ def test_msg_contact_invalid(config_tsun_inv1, msg_contact_invalid):
def test_msg_get_time(config_tsun_inv1, msg_get_time):
_ = config_tsun_inv1
m = MemoryStream(msg_get_time, (0,))
m.state = State.up
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -903,6 +945,7 @@ def test_msg_get_time(config_tsun_inv1, msg_get_time):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==0
assert m.state==State.pend
assert m._forward_buffer==msg_get_time
assert m._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00'
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
@@ -911,6 +954,7 @@ def test_msg_get_time(config_tsun_inv1, msg_get_time):
def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
_ = config_no_tsun_inv1
m = MemoryStream(msg_get_time, (0,))
m.state = State.received
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -922,14 +966,19 @@ def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==0
assert m.state==State.received
assert m._forward_buffer==b''
assert m._send_buffer==bytearray(b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00')
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
# test if ts_offset will be set on client and server side
_ = config_tsun_inv1
m = MemoryStream(msg_time_rsp, (0,), False)
s = MemoryStream(b'', (0,), True)
assert s.ts_offset==0
m.remote_stream = s
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -940,10 +989,13 @@ def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
assert m.msg_id==34
assert m.header_len==23
assert m.ts_offset==3600000
assert s.ts_offset==3600000
assert m.data_len==8
assert m._forward_buffer==b''
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.remote_stream = None
s.close()
m.close()
def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp):
@@ -1022,6 +1074,169 @@ def test_msg_time_invalid_autark(config_no_tsun_inv1, msg_time_invalid):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
def test_msg_act_time(config_no_modbus_poll, msg_act_time, msg_act_time_ack):
_ = config_no_modbus_poll
m = MemoryStream(msg_act_time, (0,))
m.ts_offset=0
m.mb_timeout = 124
m.db.set_db_def_value(Register.POLLING_INTERVAL, 125)
m.state = State.received
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==153
assert m.ts_offset==0
assert m.header_len==23
assert m.data_len==9
assert m.state == State.up
assert m._forward_buffer==msg_act_time
assert m._send_buffer==msg_act_time_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert 125 == m.db.get_db_value(Register.POLLING_INTERVAL, 0)
m.close()
def test_msg_act_time2(config_tsun_inv1, msg_act_time, msg_act_time_ack):
_ = config_tsun_inv1
m = MemoryStream(msg_act_time, (0,))
m.ts_offset=0
m.modbus_polling = True
m.mb_timeout = 123
m.db.set_db_def_value(Register.POLLING_INTERVAL, 125)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==153
assert m.ts_offset==0
assert m.header_len==23
assert m.data_len==9
assert m._forward_buffer==msg_act_time
assert m._send_buffer==msg_act_time_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert 123 == m.db.get_db_value(Register.POLLING_INTERVAL, 0)
m.close()
def test_msg_act_time_ofs(config_tsun_inv1, msg_act_time, msg_act_time_ofs, msg_act_time_ack):
_ = config_tsun_inv1
m = MemoryStream(msg_act_time, (0,))
m.ts_offset=3600
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==153
assert m.ts_offset==3600
assert m.header_len==23
assert m.data_len==9
assert m._forward_buffer==msg_act_time_ofs
assert m._send_buffer==msg_act_time_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_act_time_ofs2(config_tsun_inv1, msg_act_time, msg_act_time_ofs, msg_act_time_ack):
_ = config_tsun_inv1
m = MemoryStream(msg_act_time_ofs, (0,))
m.ts_offset=-3600
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==153
assert m.ts_offset==-3600
assert m.header_len==23
assert m.data_len==9
assert m._forward_buffer==msg_act_time
assert m._send_buffer==msg_act_time_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_act_time_autark(config_no_tsun_inv1, msg_act_time, msg_act_time_ack):
_ = config_no_tsun_inv1
m = MemoryStream(msg_act_time, (0,))
m.ts_offset=0
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==153
assert m.ts_offset==0
assert m.header_len==23
assert m.data_len==9
assert m._forward_buffer==b''
assert m._send_buffer==msg_act_time_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_act_time_ack(config_tsun_inv1, msg_act_time_ack):
_ = config_tsun_inv1
m = MemoryStream(msg_act_time_ack, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==153
assert m.msg_id==153
assert m.header_len==23
assert m.data_len==1
assert m._forward_buffer==b''
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_act_time_cmd(config_tsun_inv1, msg_act_time_cmd):
_ = config_tsun_inv1
m = MemoryStream(msg_act_time_cmd, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==112
assert m.msg_id==153
assert m.header_len==23
assert m.data_len==1
assert m._forward_buffer==msg_act_time_cmd
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
def test_msg_act_time_inv(config_tsun_inv1, msg_act_time_inv):
_ = config_tsun_inv1
m = MemoryStream(msg_act_time_inv, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==153
assert m.header_len==23
assert m.data_len==8
assert m._forward_buffer==msg_act_time_inv
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_ts_offs, msg_controller_ack):
_ = config_tsun_inv1
m = MemoryStream(msg_controller_ind, (0,))
@@ -1183,7 +1398,7 @@ def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
m._update_header(m._forward_buffer)
assert m._forward_buffer==msg_inverter_ind_0w
assert m._send_buffer==msg_inverter_ack
assert m.db.get_db_value(Register.INVERTER_STATUS) == None
assert m.db.get_db_value(Register.INVERTER_STATUS) == 1
assert isclose(m.db.db['grid']['Output_Power'], 0.5)
m.close()
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
@@ -1583,7 +1798,7 @@ def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
assert m.msg_count == 2
assert m._forward_buffer==msg_modbus_rsp20
assert m._send_buffer==b''
assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.db == {'collector': {'Serial_Number': 'R170000000000001'}, 'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == True
@@ -1613,13 +1828,64 @@ def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
assert m.msg_count == 2
assert m._forward_buffer==msg_modbus_rsp21
assert m._send_buffer==b''
assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.db == {'collector': {'Serial_Number': 'R170000000000001'}, 'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E'
assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == True
m.close()
def test_msg_modbus_rsp4(config_tsun_inv1, msg_modbus_rsp21):
'''Modbus response with a valid Modbus but no new values request must be forwarded'''
_ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp21)
m.mb.rsp_handler = m.msg_forward
m.mb.last_addr = 1
m.mb.last_fcode = 3
m.mb.last_len = 20
m.mb.last_reg = 0x3008
m.mb.req_pend = True
m.mb.err = 0
db_values = {'collector': {'Serial_Number': 'R170000000000001'}, 'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
m.db.db = db_values
m.new_data['inverter'] = False
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.mb.err == 0
assert m.msg_count == 1
assert m._forward_buffer==msg_modbus_rsp21
assert m.modbus_elms == 19
assert m._send_buffer==b''
assert m.db.db == db_values
assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E'
assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == False
m.close()
def test_msg_modbus_rsp_new(config_tsun_inv1, msg_modbus_rsp20_new):
'''Modbus response in new format with a valid Modbus request must be forwarded'''
_ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp20_new)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==135
assert m.header_len==23
assert m.data_len==107
assert m._forward_buffer==b''
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv):
_ = config_tsun_inv1
m = MemoryStream(msg_modbus_inv, (0,), False)