Compare commits

..

1 Commits

Author SHA1 Message Date
Stefan Allius
bca026bb64 initial implementation 2025-08-14 17:25:28 +02:00
11 changed files with 46 additions and 16 deletions

View File

@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up Python 3.13

View File

@@ -102,3 +102,7 @@ class AsyncIfc(ABC):
@abstractmethod
def prot_set_update_header_cb(self, callback):
pass # pragma: no cover
@abstractmethod
def prot_set_disc_cb(self, callback):
pass # pragma: no cover

View File

@@ -29,6 +29,7 @@ class AsyncIfcImpl(AsyncIfc):
self.timeout_cb = None
self.init_new_client_conn_cb = None
self.update_header_cb = None
self.inv_disc_cb = None
def close(self):
self.timeout_cb = None
@@ -106,6 +107,9 @@ class AsyncIfcImpl(AsyncIfc):
def prot_set_update_header_cb(self, callback):
self.update_header_cb = callback
def prot_set_disc_cb(self, callback):
self.inv_disc_cb = callback
class StreamPtr():
'''Descr StreamPtr'''
@@ -330,6 +334,8 @@ class AsyncStreamServer(AsyncStream):
Infos.inc_counter('ServerMode_Cnt')
await self.publish_outstanding_mqtt()
await self.loop()
if self.inv_disc_cb:
self.inv_disc_cb()
Infos.dec_counter('ServerMode_Cnt')
Infos.dec_counter('Inverter_Cnt')
await self.publish_outstanding_mqtt()
@@ -386,6 +392,8 @@ class AsyncStreamClient(AsyncStream):
Infos.inc_counter('ProxyMode_Cnt')
await self.publish_outstanding_mqtt()
await self.loop()
if self.inv_disc_cb:
self.inv_disc_cb()
if self.emu_mode:
Infos.dec_counter('EmuMode_Cnt')
else:

View File

@@ -36,6 +36,7 @@ class Talent(Message):
def __init__(self, inverter, addr, ifc: "AsyncIfc", server_side: bool,
client_mode: bool = False, id_str=b''):
self.db = InfosG3()
super().__init__('G3', ifc, server_side, self.send_modbus_cb,
mb_timeout=15)
_ = inverter
@@ -51,7 +52,6 @@ class Talent(Message):
self.contact_name = b''
self.contact_mail = b''
self.ts_offset = 0 # time offset between tsun cloud and local
self.db = InfosG3()
self.switch = {
0x00: self.msg_contact_info,
0x13: self.msg_ota_update,

View File

@@ -256,11 +256,11 @@ class SolarmanV5(SolarmanBase):
def __init__(self, inverter, addr, ifc: "AsyncIfc",
server_side: bool, client_mode: bool):
self.db = InfosG3P(client_mode)
super().__init__(addr, ifc, server_side, self.send_modbus_cb,
mb_timeout=8)
self.inverter = inverter
self.db = InfosG3P(client_mode)
self.no_forwarding = False
'''not allowed to connect to TSUN cloud by connection type'''
self.establish_inv_emu = False

View File

@@ -31,6 +31,7 @@ class Register(Enum):
GRID_VOLT_CAL_COEF = 29
OUTPUT_COEFFICIENT = 30
PROD_COMPL_TYPE = 31
AVAIL_STATUS = 32
INVERTER_CNT = 50
UNKNOWN_SNR = 51
UNKNOWN_MSG = 52
@@ -577,6 +578,7 @@ class Infos:
__output_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Output_Coefficient'] != None %}{{value_json['Output_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
__info_defs = {
Register.AVAIL_STATUS: {'name': ['status', 'status']},
# collector values used for device registration:
Register.COLLECTOR_FW_VERSION: {'name': ['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
Register.CHIP_TYPE: {'name': ['collector', 'Chip_Type'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -946,6 +948,9 @@ class Infos:
attr['dev_cla'] = ha['dev_cla']
attr['stat_cla'] = ha['stat_cla']
attr['uniq_id'] = ha['id']+snr
# attr['availability_topic'] = prfx + "status"
# attr['payload_available'] = "online"
# attr['payload_not_available'] = "offline"
if 'val_tpl' in ha:
attr['val_tpl'] = ha['val_tpl']
elif 'fmt' in ha:

View File

@@ -167,6 +167,8 @@ class InverterBase(InverterIfc, Proxy):
stream.new_data['batterie'])
or ('collector' in stream.new_data and
stream.new_data['collector'])
or ('status' in stream.new_data and
stream.new_data['status'])
or self.mqtt.ha_restarts != self.__ha_restarts):
await self._register_proxy_stat_home_assistant()
await self.__register_home_assistant(stream)

View File

@@ -98,7 +98,11 @@ class Message(ProtocolIfc):
self.server_side = server_side
self.ifc = ifc
self.node_id = node_id
self.new_data = {}
if server_side:
ifc.prot_set_disc_cb(self._inv_disc)
self.db.set_db_def_value(Register.AVAIL_STATUS, "on")
self.new_data['status'] = True
self.mb = Modbus(send_modbus_cb, mb_timeout)
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
else:
@@ -110,7 +114,6 @@ class Message(ProtocolIfc):
self.unique_id = 0
self.inv_serial = ''
self.sug_area = ''
self.new_data = {}
self.state = State.init
self.shutdown_started = False
self.modbus_elms = 0 # for unit tests
@@ -220,6 +223,11 @@ class Message(ProtocolIfc):
f'(reg: 0x{self.mb.last_reg:04x}):',
data[hdr_len:], modbus_msg_len)
def _inv_disc(self):
logging.warning(f"Un-Available: [{self.node_id}]")
self.db.set_db_def_value(Register.AVAIL_STATUS, "off")
self.new_data['status'] = True
'''
Our puplic methods
'''
@@ -237,6 +245,7 @@ class Message(ProtocolIfc):
self.ifc.prot_set_timeout_cb(None)
self.ifc.prot_set_init_new_client_conn_cb(None)
self.ifc.prot_set_update_header_cb(None)
self.ifc.prot_set_disc_cb(None)
self.ifc = None
if self.mb:

View File

@@ -35,6 +35,8 @@ class ModbusConn():
async def __aexit__(self, exc_type, exc, tb):
Infos.dec_counter('ClientMode_Cnt')
Infos.dec_counter('Inverter_Cnt')
if self.inverter.local.ifc.inv_disc_cb:
self.inverter.local.ifc.inv_disc_cb()
await self.inverter.local.ifc.publish_outstanding_mqtt()
self.inverter.__exit__(exc_type, exc, tb)

View File

@@ -1598,18 +1598,18 @@ async def test_msg_iterator(my_loop, config_tsun_inv1):
@pytest.mark.asyncio
async def test_proxy_counter(my_loop, config_tsun_inv1):
m = SolarmanV5(None, ('test.local', 1234), ifc=AsyncIfcImpl(), server_side=True, client_mode=False)
assert m.new_data == {}
assert m.new_data == {'status': True}
m.db.stat['proxy']['Unknown_Msg'] = 0
Infos.new_stat_data['proxy'] = False
m.inc_counter('Unknown_Msg')
assert m.new_data == {}
assert m.new_data == {'status': True}
assert Infos.new_stat_data == {'proxy': True}
assert 1 == m.db.stat['proxy']['Unknown_Msg']
Infos.new_stat_data['proxy'] = False
m.dec_counter('Unknown_Msg')
assert m.new_data == {}
assert m.new_data == {'status': True}
assert Infos.new_stat_data == {'proxy': True}
assert 0 == m.db.stat['proxy']['Unknown_Msg']
m.close()

View File

@@ -2070,7 +2070,7 @@ def test_proxy_counter():
m.id_str = b"R170000000000001"
c = m.createClientStream(b'')
assert m.new_data == {}
assert m.new_data == {'status': True}
m.db.stat['proxy']['Unknown_Msg'] = 0
c.db.stat['proxy']['Unknown_Msg'] = 0
Infos.new_stat_data['proxy'] = False
@@ -2079,7 +2079,7 @@ def test_proxy_counter():
m.close()
m = MemoryStream(b'')
assert m.new_data == {}
assert m.new_data == {'status': True}
assert Infos.new_stat_data == {'proxy': True}
assert m.db.new_stat_data == {'proxy': True}
assert c.db.new_stat_data == {'proxy': True}
@@ -2088,7 +2088,7 @@ def test_proxy_counter():
Infos.new_stat_data['proxy'] = False
c.inc_counter('Unknown_Msg')
assert m.new_data == {}
assert m.new_data == {'status': True}
assert Infos.new_stat_data == {'proxy': True}
assert m.db.new_stat_data == {'proxy': True}
assert c.db.new_stat_data == {'proxy': True}
@@ -2097,7 +2097,7 @@ def test_proxy_counter():
Infos.new_stat_data['proxy'] = False
c.inc_counter('Modbus_Command')
assert m.new_data == {}
assert m.new_data == {'status': True}
assert Infos.new_stat_data == {'proxy': True}
assert m.db.new_stat_data == {'proxy': True}
assert c.db.new_stat_data == {'proxy': True}
@@ -2106,7 +2106,7 @@ def test_proxy_counter():
Infos.new_stat_data['proxy'] = False
m.dec_counter('Unknown_Msg')
assert m.new_data == {}
assert m.new_data == {'status': True}
assert Infos.new_stat_data == {'proxy': True}
assert 1 == m.db.stat['proxy']['Unknown_Msg']
m.close()
@@ -2258,7 +2258,7 @@ def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
m.mb.req_pend = True
m.mb.err = 0
assert m.db.db == {}
assert m.db.db == {'status': {'status': 'on'}}
m.new_data['inverter'] = False
m.read() # read complete msg, and dispatch msg
@@ -2267,7 +2267,7 @@ def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
assert m.msg_count == 2
assert m.ifc.fwd_fifo.get()==msg_modbus_rsp20
assert m.ifc.tx_fifo.get()==b''
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.db == {'status': {'status': 'on'}, '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
@@ -2288,7 +2288,7 @@ def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
m.mb.req_pend = True
m.mb.err = 0
assert m.db.db == {}
assert m.db.db == {'status': {'status': 'on'}}
m.new_data['inverter'] = False
m.read() # read complete msg, and dispatch msg
@@ -2297,7 +2297,7 @@ def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
assert m.msg_count == 2
assert m.ifc.fwd_fifo.get()==msg_modbus_rsp21
assert m.ifc.tx_fifo.get()==b''
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.db == {'status': {'status': 'on'}, '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