S allius/issue334 (#335)

* move forward_at_cmd_resp into InfosG3P class

- the variable is shared between the two connections
of an inverter. One is for the TSUN cloud and the
other for the device.

* use inverter class to share values between
the two protocol instances of a proxy
- move forward_at_cmd_resp into class InverterG3P
- store inverter ptr in Solarman_V5 instances
- add inverter ptr to all constructurs of protocols
- adapt doku and unit tests-
- add integration tests for AT+ commands which
  check the forwarding from and to the TSUN cloud

* adapt and improve the unit tests
- fix node_id declaration, which always has a / at
  the end. See config grammar for this rule
- set global var test to default after test run
This commit is contained in:
Stefan Allius
2025-04-05 14:37:52 +02:00
committed by GitHub
parent 4988a29a34
commit 6974672ba0
16 changed files with 912 additions and 739 deletions

View File

@@ -155,6 +155,7 @@ async def test_remote_except(config_conn, patch_open_connection):
await asyncio.sleep(0)
assert inverter.remote.stream==None
del inverter
test = MockType.RD_TEST_0_BYTES
cnt = 0
for inv in InverterBase:

View File

@@ -133,6 +133,9 @@ async def test_remote_except(config_conn, patch_open_connection):
await asyncio.sleep(0)
assert inverter.remote.stream==None
test = MockType.RD_TEST_0_BYTES
@pytest.mark.asyncio
async def test_mqtt_publish(config_conn, patch_open_connection):
_ = config_conn

View File

@@ -190,7 +190,8 @@ def patch_mqtt_except():
yield conn
@pytest.mark.asyncio
async def test_modbus_conn(patch_open):
async def test_modbus_conn(config_conn, patch_open):
_ = config_conn
_ = patch_open
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@@ -210,6 +211,7 @@ async def test_modbus_conn(patch_open):
@pytest.mark.asyncio
async def test_modbus_no_cnf():
_ = config_conn
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
loop = asyncio.get_event_loop()
ModbusTcp(loop)

View File

@@ -47,7 +47,7 @@ def config_no_conn(test_port):
@pytest.fixture
def spy_at_cmd():
conn = SolarmanV5(('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
conn = SolarmanV5(None, ('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
conn.node_id = 'inv_2/'
with patch.object(conn, 'send_at_cmd', wraps=conn.send_at_cmd) as wrapped_conn:
yield wrapped_conn
@@ -55,7 +55,7 @@ def spy_at_cmd():
@pytest.fixture
def spy_modbus_cmd():
conn = SolarmanV5(('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
conn = SolarmanV5(None, ('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
conn.node_id = 'inv_1/'
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
yield wrapped_conn
@@ -63,7 +63,7 @@ def spy_modbus_cmd():
@pytest.fixture
def spy_modbus_cmd_client():
conn = SolarmanV5(('test.local', 1234), server_side=False, client_mode= False, ifc=AsyncIfcImpl())
conn = SolarmanV5(None, ('test.local', 1234), server_side=False, client_mode= False, ifc=AsyncIfcImpl())
conn.node_id = 'inv_1/'
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
yield wrapped_conn

View File

@@ -4,7 +4,9 @@ import time
import asyncio
import logging
import random
from asyncio import StreamReader, StreamWriter
from math import isclose
from async_stream import AsyncIfcImpl, StreamPtr
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
from cnf.config import Config
@@ -12,6 +14,11 @@ from infos import Infos, Register
from modbus import Modbus
from messages import State, Message
from proxy import Proxy
from test_inverter_g3p import FakeReader, FakeWriter, patch_open_connection
from inverter_base import InverterBase
from test_modbus_tcp import test_port, test_hostname
pytest_plugins = ('pytest_asyncio',)
@@ -43,10 +50,14 @@ class FakeIfc(AsyncIfcImpl):
async def create_remote(self):
await asyncio.sleep(0)
class FakeInverter():
def __init__(self):
self.forward_at_cmd_resp = False
class MemoryStream(SolarmanV5):
def __init__(self, msg, chunks = (0,), server_side: bool = True):
def __init__(self, msg, chunks = (0,), server_side: bool = True, inverter=FakeInverter()):
_ifc = FakeIfc()
super().__init__(('test.local', 1234), _ifc, server_side, client_mode=False)
super().__init__(inverter, ('test.local', 1234), _ifc, server_side, client_mode=False)
if server_side:
self.mb.timeout = 0.4 # overwrite for faster testing
self.mb_first_timeout = 0.5
@@ -816,7 +827,7 @@ def config_tsun_allow_all():
@pytest.fixture
def config_no_tsun_inv1():
Config.act_config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}}
Config.act_config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}}
@pytest.fixture
def config_tsun_inv1():
@@ -828,21 +839,21 @@ def config_tsun_inv1():
'proxy_node_id': 'test_1',
'proxy_unique_id': ''
},
'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
'solarman':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
Proxy.class_init()
Proxy.mqtt = Mqtt()
@pytest.fixture
def config_tsun_scan():
Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0xffc0, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}}
Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1/', 'modbus_polling': True, 'modbus_scanning': {'start': 0xffc0, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}}
@pytest.fixture
def config_tsun_scan_dcu():
Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, 'client_mode': {'host': '192.168.1.1.'}, 'suggested_area':'roof', 'sensor_list': 0}}}
Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1/', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, 'client_mode': {'host': '192.168.1.1.'}, 'suggested_area':'roof', 'sensor_list': 0}}}
@pytest.fixture
def config_tsun_dcu1():
Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
def test_read_message(device_ind_msg):
Config.act_config = {'solarman':{'enabled': True}}
@@ -1432,9 +1443,9 @@ def test_build_logger_modell(config_tsun_allow_all, device_ind_msg):
def test_msg_iterator():
Message._registry.clear()
m1 = SolarmanV5(('test1.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m2 = SolarmanV5(('test2.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m3 = SolarmanV5(('test3.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m1 = SolarmanV5(None, ('test1.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m2 = SolarmanV5(None, ('test2.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m3 = SolarmanV5(None, ('test3.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m3.close()
del m3
test1 = 0
@@ -1452,7 +1463,7 @@ def test_msg_iterator():
assert test2 == 1
def test_proxy_counter():
m = SolarmanV5(('test.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
m = SolarmanV5(None, ('test.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
assert m.new_data == {}
m.db.stat['proxy']['Unknown_Msg'] = 0
Infos.new_stat_data['proxy'] = False
@@ -1559,7 +1570,7 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
assert m.ifc.fwd_fifo.get()==b''
assert m.sent_pdu == b''
assert str(m.seq) == '03:04'
assert m.forward_at_cmd_resp == False
assert m.inverter.forward_at_cmd_resp == False
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.close()
@@ -1594,12 +1605,12 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_
assert m.ifc.tx_fifo.get()==b''
assert m.ifc.fwd_fifo.get()==b''
assert str(m.seq) == '02:02'
assert m.forward_at_cmd_resp == False
assert m.inverter.forward_at_cmd_resp == False
assert Proxy.mqtt.key == 'tsun/at_resp'
assert Proxy.mqtt.data == "'AT+WEBU' is forbidden"
m.close()
def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg, at_command_rsp_msg):
_ = config_tsun_inv1
m = MemoryStream(at_command_ind_msg, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1621,6 +1632,17 @@ def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
assert m.db.stat['proxy']['AT_Command'] == 1
assert m.db.stat['proxy']['AT_Command_Blocked'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.append_msg(at_command_rsp_msg)
m.read() # read at resp
assert m.control == 0x1510
assert str(m.seq) == '03:03'
assert m.ifc.rx_get()==b''
assert m.ifc.tx_fifo.get()==b''
assert m.ifc.fwd_fifo.get()==at_command_rsp_msg
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.close()
def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block):
@@ -1630,6 +1652,7 @@ def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block):
m.db.stat['proxy']['AT_Command'] = 0
m.db.stat['proxy']['AT_Command_Blocked'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.inverter.forward_at_cmd_resp = 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.msg_count == 1
@@ -1645,6 +1668,9 @@ def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block):
assert m.db.stat['proxy']['AT_Command'] == 0
assert m.db.stat['proxy']['AT_Command_Blocked'] == 1
assert m.db.stat['proxy']['Modbus_Command'] == 0
assert m.inverter.forward_at_cmd_resp == False
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.close()
def test_msg_at_command_rsp1(config_tsun_inv1, at_command_rsp_msg):
@@ -1652,7 +1678,7 @@ def test_msg_at_command_rsp1(config_tsun_inv1, at_command_rsp_msg):
m = MemoryStream(at_command_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.forward_at_cmd_resp = True
m.inverter.forward_at_cmd_resp = True
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
@@ -1671,7 +1697,7 @@ def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
m = MemoryStream(at_command_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.forward_at_cmd_resp = False
m.inverter.forward_at_cmd_resp = 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.msg_count == 1
@@ -1683,6 +1709,8 @@ def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
assert m.ifc.tx_fifo.get()==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
assert Proxy.mqtt.key == 'tsun/inv1/at_resp'
assert Proxy.mqtt.data == "+ok"
m.close()
def test_msg_at_command_rsp3(config_tsun_inv1, at_command_interim_rsp_msg):
@@ -1692,7 +1720,7 @@ def test_msg_at_command_rsp3(config_tsun_inv1, at_command_interim_rsp_msg):
m.db.stat['proxy']['Modbus_Command'] = 0
m.db.stat['proxy']['Invalid_Msg_Format'] = 0
m.db.stat['proxy']['Unknown_Msg'] = 0
m.forward_at_cmd_resp = True
m.inverter.forward_at_cmd_resp = True
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
@@ -1706,6 +1734,8 @@ def test_msg_at_command_rsp3(config_tsun_inv1, at_command_interim_rsp_msg):
assert m.db.stat['proxy']['Modbus_Command'] == 0
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert m.db.stat['proxy']['Unknown_Msg'] == 0
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.close()
def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
@@ -2218,4 +2248,100 @@ def test_timestamp():
m = MemoryStream(b'')
ts = m._timestamp()
ts_emu = m._emu_timestamp()
assert ts == ts_emu + 24*60*60
assert ts == ts_emu + 24*60*60
class MemoryStream2(MemoryStream):
def __init__(self, inverter, addr, ifc,
server_side: bool, client_mode: bool):
super().__init__(b'', inverter=inverter)
class InverterTest(InverterBase):
def __init__(self, reader: StreamReader, writer: StreamWriter,
client_mode: bool = False):
remote_prot = None
super().__init__(reader, writer, 'solarman',
MemoryStream2, client_mode, remote_prot)
def forward(self, src, dst) -> None:
"""forward handler transmits data over the remote connection"""
# dst.ifc.update_header_cb(src.fwd_fifo.peek())
dst.ifc.tx_add(src.ifc.fwd_fifo.get())
@pytest.mark.asyncio
async def test_proxy_at_cmd(config_tsun_inv1, patch_open_connection, at_command_ind_msg, at_command_rsp_msg):
_ = config_tsun_inv1
_ = patch_open_connection
assert asyncio.get_running_loop()
with InverterTest(FakeReader(), FakeWriter(), client_mode=False) as inverter:
await inverter.create_remote()
await asyncio.sleep(0)
r = inverter.remote.stream
l = inverter.local.stream
l.db.stat['proxy']['AT_Command'] = 0
l.db.stat['proxy']['Unknown_Ctrl'] = 0
l.db.stat['proxy']['AT_Command_Blocked'] = 0
l.db.stat['proxy']['Modbus_Command'] = 0
inverter.forward_at_cmd_resp = False
r.append_msg(at_command_ind_msg)
r.read() # read complete msg, and dispatch msg
assert inverter.forward_at_cmd_resp
inverter.forward(r,l)
assert l.ifc.tx_fifo.get()==at_command_ind_msg
assert l.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert l.db.stat['proxy']['AT_Command'] == 1
assert l.db.stat['proxy']['AT_Command_Blocked'] == 0
assert l.db.stat['proxy']['Modbus_Command'] == 0
l.append_msg(at_command_rsp_msg)
l.read() # read at resp
assert l.ifc.fwd_fifo.peek()==at_command_rsp_msg
inverter.forward(l,r)
assert r.ifc.tx_fifo.get()==at_command_rsp_msg
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
@pytest.mark.asyncio
async def test_proxy_at_blocked(config_tsun_inv1, patch_open_connection, at_command_ind_msg_block, at_command_rsp_msg):
_ = config_tsun_inv1
_ = patch_open_connection
assert asyncio.get_running_loop()
with InverterTest(FakeReader(), FakeWriter(), client_mode=False) as inverter:
await inverter.create_remote()
await asyncio.sleep(0)
r = inverter.remote.stream
l = inverter.local.stream
l.db.stat['proxy']['AT_Command'] = 0
l.db.stat['proxy']['Unknown_Ctrl'] = 0
l.db.stat['proxy']['AT_Command_Blocked'] = 0
l.db.stat['proxy']['Modbus_Command'] = 0
inverter.forward_at_cmd_resp = False
r.append_msg(at_command_ind_msg_block)
r.read() # read complete msg, and dispatch msg
assert not inverter.forward_at_cmd_resp
inverter.forward(r,l)
assert l.ifc.tx_fifo.get()==b''
assert l.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert l.db.stat['proxy']['AT_Command'] == 0
assert l.db.stat['proxy']['AT_Command_Blocked'] == 1
assert l.db.stat['proxy']['Modbus_Command'] == 0
l.append_msg(at_command_rsp_msg)
l.read() # read at resp
assert l.ifc.fwd_fifo.peek()==b''
inverter.forward(l,r)
assert r.ifc.tx_fifo.get()==b''
assert Proxy.mqtt.key == 'tsun/inv1/at_resp'
assert Proxy.mqtt.data == "+ok"

View File

@@ -6,7 +6,7 @@ from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
from gen3plus.solarman_emu import SolarmanEmu
from infos import Infos, Register
from test_solarman import FakeIfc, MemoryStream, get_sn_int, get_sn, correct_checksum, config_tsun_inv1, msg_modbus_rsp
from test_solarman import FakeIfc, FakeInverter, MemoryStream, get_sn_int, get_sn, correct_checksum, config_tsun_inv1, msg_modbus_rsp
from test_infos_g3p import str_test_ip, bytes_test_ip
timestamp = 0x3224c8bc
@@ -19,10 +19,10 @@ class InvStream(MemoryStream):
return timestamp
class CldStream(SolarmanEmu):
def __init__(self, inv: InvStream):
def __init__(self, inv: InvStream, inverter=FakeInverter()):
_ifc = FakeIfc()
_ifc.remote.stream = inv
super().__init__(('test.local', 1234), _ifc, server_side=False, client_mode=False)
super().__init__(inverter, ('test.local', 1234), _ifc, server_side=False, client_mode=False)
self.__msg = b''
self.__msg_len = 0
self.__offs = 0

View File

@@ -25,7 +25,7 @@ class FakeIfc(AsyncIfcImpl):
class MemoryStream(Talent):
def __init__(self, msg, chunks = (0,), server_side: bool = True):
self.ifc = FakeIfc()
super().__init__(('test.local', 1234), self.ifc, server_side)
super().__init__(None, ('test.local', 1234), self.ifc, server_side)
if server_side:
self.mb.timeout = 0.4 # overwrite for faster testing
self.mb_first_timeout = 0.5
@@ -2026,9 +2026,9 @@ def test_ctrl_byte():
def test_msg_iterator():
m1 = Talent(('test1.local', 1234), ifc=AsyncIfcImpl(), server_side=True)
m2 = Talent(('test2.local', 1234), ifc=AsyncIfcImpl(), server_side=True)
m3 = Talent(('test3.local', 1234), ifc=AsyncIfcImpl(), server_side=True)
m1 = Talent(None, ('test1.local', 1234), ifc=AsyncIfcImpl(), server_side=True)
m2 = Talent(None, ('test2.local', 1234), ifc=AsyncIfcImpl(), server_side=True)
m3 = Talent(None, ('test3.local', 1234), ifc=AsyncIfcImpl(), server_side=True)
m3.close()
del m3
test1 = 0