Compare commits
4 Commits
s-allius/i
...
s-allius/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb9344a476 | ||
|
|
7aab01bcfc | ||
|
|
60971be000 | ||
|
|
9304407348 |
@@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [unreleased]
|
||||
|
||||
- add-on: bump python to version 3.12.10-r1
|
||||
- set no of pv modules for MS800 GEN3PLUS inverters
|
||||
- fix the paths to copy the config.example.toml file during proxy start
|
||||
- add MQTT topic `dcu_power` for setting output power on DCUs
|
||||
- Update ghcr.io/hassio-addons/base Docker tag to v17.2.5
|
||||
- fix a lot of pytest-asyncio problems in the unit tests
|
||||
- Cleanup startup code for Quart and the Proxy
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
pytest-cov==6.1.1
|
||||
python-dotenv==1.1.0
|
||||
mock==5.2.0
|
||||
coverage==7.8.2
|
||||
coverage==7.8.0
|
||||
jinja2-cli==0.8.2
|
||||
@@ -162,8 +162,7 @@ class Config():
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def init(cls, def_reader: ConfigIfc, log_path: str = '',
|
||||
cnf_path: str = 'config') -> None | str:
|
||||
def init(cls, def_reader: ConfigIfc, log_path: str = '') -> None | str:
|
||||
'''Initialise the Proxy-Config
|
||||
|
||||
Copy the internal default config file into the config directory
|
||||
@@ -174,13 +173,12 @@ and initialise the Config with the default configuration '''
|
||||
try:
|
||||
# make the default config transparaent by copying it
|
||||
# in the config.example file
|
||||
logging.info(
|
||||
f'Copy Default Config to {cnf_path}config.example.toml')
|
||||
logging.debug('Copy Default Config to config.example.toml')
|
||||
|
||||
shutil.copy2("cnf/default_config.toml",
|
||||
cnf_path + "config.example.toml")
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
shutil.copy2("default_config.toml",
|
||||
"config/config.example.toml")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# read example config file as default configuration
|
||||
try:
|
||||
|
||||
@@ -216,7 +216,7 @@ class InfosG3P(Infos):
|
||||
self.set_db_def_value(Register.MANUFACTURER, 'TSUN')
|
||||
self.set_db_def_value(Register.EQUIPMENT_MODEL, 'TSOL-MSxx00')
|
||||
self.set_db_def_value(Register.CHIP_TYPE, 'IGEN TECH')
|
||||
self.set_db_def_value(Register.NO_INPUTS, 2)
|
||||
self.set_db_def_value(Register.NO_INPUTS, 4)
|
||||
|
||||
def __hide_topic(self, row: dict) -> bool:
|
||||
if 'dep' in row:
|
||||
|
||||
44
app/src/gen3plus/solarman_v5.py
Executable file → Normal file
44
app/src/gen3plus/solarman_v5.py
Executable file → Normal file
@@ -247,7 +247,6 @@ class SolarmanBase(Message):
|
||||
class SolarmanV5(SolarmanBase):
|
||||
AT_CMD = 1
|
||||
MB_RTU_CMD = 2
|
||||
DCU_CMD = 5
|
||||
AT_CMD_RSP = 8
|
||||
MB_CLIENT_DATA_UP = 30
|
||||
'''Data up time in client mode'''
|
||||
@@ -533,26 +532,6 @@ class SolarmanV5(SolarmanBase):
|
||||
except Exception:
|
||||
self.ifc.tx_clear()
|
||||
|
||||
def send_dcu_cmd(self, pdu: bytearray):
|
||||
if self.sensor_list != 0x3026:
|
||||
logger.debug(f'[{self.node_id}] DCU CMD not allowed,'
|
||||
f' for sensor: {self.sensor_list:#04x}')
|
||||
return
|
||||
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore DCU CMD,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
|
||||
self.inverter.forward_dcu_cmd_resp = False
|
||||
self._build_header(0x4510)
|
||||
self.ifc.tx_add(struct.pack('<BHLLL', self.DCU_CMD,
|
||||
self.sensor_list, 0, 0, 0))
|
||||
self.ifc.tx_add(pdu)
|
||||
self._finish_send_msg()
|
||||
self.ifc.tx_log(logging.INFO, f'Send DCU CMD :{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
def __forward_msg(self):
|
||||
self.forward(self.ifc.rx_peek(), self.header_len+self.data_len+2)
|
||||
|
||||
@@ -562,17 +541,12 @@ class SolarmanV5(SolarmanBase):
|
||||
rated = db.get_db_value(Register.RATED_POWER, 0)
|
||||
model = None
|
||||
if max_pow == 2000:
|
||||
db.set_db_def_value(Register.NO_INPUTS, 4)
|
||||
if rated == 800 or rated == 600:
|
||||
model = f'TSOL-MS{max_pow}({rated})'
|
||||
else:
|
||||
model = f'TSOL-MS{max_pow}'
|
||||
elif max_pow == 1800 or max_pow == 1600:
|
||||
db.set_db_def_value(Register.NO_INPUTS, 4)
|
||||
model = f'TSOL-MS{max_pow}'
|
||||
elif max_pow <= 800:
|
||||
model = f'TSOL-MS{max_pow}'
|
||||
|
||||
if model:
|
||||
logger.info(f'Model: {model}')
|
||||
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
|
||||
@@ -673,10 +647,6 @@ class SolarmanV5(SolarmanBase):
|
||||
self.inc_counter('AT_Command')
|
||||
self.inverter.forward_at_cmd_resp = True
|
||||
|
||||
if ftype == self.DCU_CMD:
|
||||
self.inc_counter('DCU_Command')
|
||||
self.inverter.forward_dcu_cmd_resp = True
|
||||
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
rstream = self.ifc.remote.stream
|
||||
if rstream.mb.recv_req(data[15:],
|
||||
@@ -700,10 +670,6 @@ class SolarmanV5(SolarmanBase):
|
||||
if self.inverter.forward_at_cmd_resp:
|
||||
return logging.INFO
|
||||
return logging.DEBUG
|
||||
elif ftype == self.DCU_CMD:
|
||||
if self.inverter.forward_dcu_cmd_resp:
|
||||
return logging.INFO
|
||||
return logging.DEBUG
|
||||
elif ftype == self.MB_RTU_CMD \
|
||||
and self.server_side:
|
||||
return self.mb.last_log_lvl
|
||||
@@ -723,16 +689,6 @@ class SolarmanV5(SolarmanBase):
|
||||
logger.info(f'{key}: {data_json}')
|
||||
self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
|
||||
elif ftype == self.DCU_CMD:
|
||||
if not self.inverter.forward_dcu_cmd_resp:
|
||||
data_json = '+ok'
|
||||
node_id = self.node_id
|
||||
key = 'dcu_resp'
|
||||
logger.info(f'{key}: {data_json}')
|
||||
self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
self.__modbus_command_rsp(data)
|
||||
return
|
||||
|
||||
@@ -44,7 +44,6 @@ class Register(Enum):
|
||||
MODBUS_COMMAND = 60
|
||||
AT_COMMAND_BLOCKED = 61
|
||||
CLOUD_CONN_CNT = 62
|
||||
DCU_COMMAND = 63
|
||||
OUTPUT_POWER = 83
|
||||
RATED_POWER = 84
|
||||
INVERTER_TEMP = 85
|
||||
@@ -626,7 +625,6 @@ class Infos:
|
||||
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': FMT_INT, 'name': 'Invalid Message Format', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': FMT_INT, 'name': 'AT Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': FMT_INT, 'name': 'AT Command Blocked', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.DCU_COMMAND: {'name': ['proxy', 'DCU_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'dcu_cmd_', 'fmt': FMT_INT, 'name': 'DCU Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': FMT_INT, 'name': 'Modbus Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
|
||||
|
||||
|
||||
136
app/src/mqtt.py
Executable file → Normal file
136
app/src/mqtt.py
Executable file → Normal file
@@ -2,8 +2,6 @@ import asyncio
|
||||
import logging
|
||||
import aiomqtt
|
||||
import traceback
|
||||
import struct
|
||||
import inspect
|
||||
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
@@ -29,27 +27,14 @@ class Mqtt(metaclass=Singleton):
|
||||
loop = asyncio.get_event_loop()
|
||||
self.task = loop.create_task(self.__loop())
|
||||
self.ha_restarts = 0
|
||||
self.topic_defs = [
|
||||
{'prefix': 'auto_conf_prefix', 'topic': '/status',
|
||||
'fnc': self._ha_status, 'args': []},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/rated_load',
|
||||
'fnc': self._modbus_cmd,
|
||||
'args': [Modbus.WRITE_SINGLE_REG, 1, 0x2008]},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/out_coeff',
|
||||
'fnc': self._out_coeff, 'args': []},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/dcu_power',
|
||||
'fnc': self._dcu_cmd, 'args': []},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_regs',
|
||||
'fnc': self._modbus_cmd, 'args': [Modbus.READ_REGS, 2]},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_inputs',
|
||||
'fnc': self._modbus_cmd, 'args': [Modbus.READ_INPUTS, 2]},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/at_cmd',
|
||||
'fnc': self._at_cmd, 'args': []},
|
||||
]
|
||||
|
||||
ha = Config.get('ha')
|
||||
for entry in self.topic_defs:
|
||||
entry['full_topic'] = f"{ha[entry['prefix']]}{entry['topic']}"
|
||||
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
||||
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
|
||||
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
|
||||
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
|
||||
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
|
||||
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
|
||||
|
||||
@property
|
||||
def ha_restarts(self):
|
||||
@@ -90,7 +75,19 @@ class Mqtt(metaclass=Singleton):
|
||||
try:
|
||||
async with self.__client:
|
||||
logger_mqtt.info('MQTT broker connection established')
|
||||
await self._init_new_conn()
|
||||
self.ctime = datetime.now()
|
||||
self.published = 0
|
||||
self.received = 0
|
||||
|
||||
if self.__cb_mqtt_is_up:
|
||||
await self.__cb_mqtt_is_up()
|
||||
|
||||
await self.__client.subscribe(self.ha_status_topic)
|
||||
await self.__client.subscribe(self.mb_rated_topic)
|
||||
await self.__client.subscribe(self.mb_out_coeff_topic)
|
||||
await self.__client.subscribe(self.mb_reads_topic)
|
||||
await self.__client.subscribe(self.mb_inputs_topic)
|
||||
await self.__client.subscribe(self.mb_at_cmd_topic)
|
||||
|
||||
async for message in self.__client.messages:
|
||||
await self.dispatch_msg(message)
|
||||
@@ -120,51 +117,47 @@ class Mqtt(metaclass=Singleton):
|
||||
f"Exception:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def _init_new_conn(self):
|
||||
self.ctime = datetime.now()
|
||||
self.published = 0
|
||||
self.received = 0
|
||||
if self.__cb_mqtt_is_up:
|
||||
await self.__cb_mqtt_is_up()
|
||||
for entry in self.topic_defs:
|
||||
await self.__client.subscribe(entry['full_topic'])
|
||||
|
||||
async def dispatch_msg(self, message):
|
||||
self.received += 1
|
||||
|
||||
for entry in self.topic_defs:
|
||||
if message.topic.matches(entry['full_topic']) \
|
||||
and 'fnc' in entry:
|
||||
fnc = entry['fnc']
|
||||
|
||||
if inspect.iscoroutinefunction(fnc):
|
||||
await entry['fnc'](message, *entry['args'])
|
||||
elif callable(fnc):
|
||||
entry['fnc'](message, *entry['args'])
|
||||
|
||||
async def _ha_status(self, message):
|
||||
status = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info('Home-Assistant Status:'
|
||||
f' {status}')
|
||||
if status == 'online':
|
||||
self.ha_restarts += 1
|
||||
if self.__cb_mqtt_is_up:
|
||||
if message.topic.matches(self.ha_status_topic):
|
||||
status = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info('Home-Assistant Status:'
|
||||
f' {status}')
|
||||
if status == 'online':
|
||||
self.ha_restarts += 1
|
||||
await self.__cb_mqtt_is_up()
|
||||
|
||||
async def _out_coeff(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 1024/100)
|
||||
if val < 0 or val > 1024:
|
||||
logger_mqtt.error('out_coeff: value must be in'
|
||||
'the range 0..100,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
await self._modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
0, 0x202c, val)
|
||||
except Exception:
|
||||
pass
|
||||
if message.topic.matches(self.mb_rated_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
1, 0x2008)
|
||||
|
||||
if message.topic.matches(self.mb_out_coeff_topic):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 1024/100)
|
||||
if val < 0 or val > 1024:
|
||||
logger_mqtt.error('out_coeff: value must be in'
|
||||
'the range 0..100,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
0, 0x202c, val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if message.topic.matches(self.mb_reads_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_REGS, 2)
|
||||
|
||||
if message.topic.matches(self.mb_inputs_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_INPUTS, 2)
|
||||
|
||||
if message.topic.matches(self.mb_at_cmd_topic):
|
||||
await self.at_cmd(message)
|
||||
|
||||
def each_inverter(self, message, func_name: str):
|
||||
topic = str(message.topic)
|
||||
@@ -182,7 +175,7 @@ class Mqtt(metaclass=Singleton):
|
||||
else:
|
||||
logger_mqtt.warning(f'Node_id: {node_id} not found')
|
||||
|
||||
async def _modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
for fnc in self.each_inverter(message, "send_modbus_cmd"):
|
||||
res = payload.split(',')
|
||||
@@ -197,22 +190,7 @@ class Mqtt(metaclass=Singleton):
|
||||
val = int(res[1]) # lenght
|
||||
await fnc(func, addr, val, logging.INFO)
|
||||
|
||||
async def _at_cmd(self, message):
|
||||
async def at_cmd(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
for fnc in self.each_inverter(message, "send_at_cmd"):
|
||||
await fnc(payload)
|
||||
|
||||
def _dcu_cmd(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 10)
|
||||
if val < 1000 or val > 8000:
|
||||
logger_mqtt.error('dcu_power: value must be in'
|
||||
'the range 100..800,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
pdu = struct.pack('>BBBBBBH', 1, 1, 6, 1, 0, 1, val)
|
||||
for fnc in self.each_inverter(message, "send_dcu_cmd"):
|
||||
fnc(pdu)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -12,7 +12,7 @@ class Schedule:
|
||||
count = 0
|
||||
|
||||
@classmethod
|
||||
def start(cls) -> None: # pragma: no cover
|
||||
def start(cls) -> None:
|
||||
'''Start the scheduler and schedule the tasks (cron jobs)'''
|
||||
logging.debug("Scheduler init")
|
||||
cls.mqtt = Mqtt(None)
|
||||
@@ -20,7 +20,7 @@ class Schedule:
|
||||
crontab('0 0 * * *', func=cls.atmidnight, start=True)
|
||||
|
||||
@classmethod
|
||||
async def atmidnight(cls) -> None: # pragma: no cover
|
||||
async def atmidnight(cls) -> None:
|
||||
'''Clear daily counters at midnight'''
|
||||
logging.info("Clear daily counters at midnight")
|
||||
|
||||
|
||||
@@ -127,8 +127,7 @@ class Server():
|
||||
def build_config(self):
|
||||
# read config file
|
||||
Config.init(ConfigReadToml(self.src_dir + "cnf/default_config.toml"),
|
||||
log_path=self.log_path,
|
||||
cnf_path=self.config_path)
|
||||
log_path=self.log_path)
|
||||
ConfigReadEnv()
|
||||
ConfigReadJson(self.config_path + "config.json")
|
||||
ConfigReadToml(self.config_path + "config.toml")
|
||||
|
||||
@@ -10,40 +10,39 @@ from .log_handler import LogHandler
|
||||
def _get_device_icon(client_mode: bool):
|
||||
'''returns the icon for the device conntection'''
|
||||
if client_mode:
|
||||
return 'fa-download fa-rotate-180', 'Server Mode'
|
||||
return 'fa-download fa-rotate-180'
|
||||
|
||||
return 'fa-upload fa-rotate-180', 'Client Mode'
|
||||
return 'fa-upload fa-rotate-180'
|
||||
|
||||
|
||||
def _get_cloud_icon(emu_mode: bool):
|
||||
'''returns the icon for the cloud conntection'''
|
||||
if emu_mode:
|
||||
return 'fa-cloud-arrow-up-alt', 'Emu Mode'
|
||||
return 'fa-cloud-arrow-up-alt'
|
||||
|
||||
return 'fa-cloud', 'Proxy Mode'
|
||||
return 'fa-cloud'
|
||||
|
||||
|
||||
def _get_row(inv: InverterBase):
|
||||
'''build one row for the connection table'''
|
||||
client_mode = inv.client_mode
|
||||
inv_serial = inv.local.stream.inv_serial
|
||||
icon1, descr1 = _get_device_icon(client_mode)
|
||||
icon1 = _get_device_icon(client_mode)
|
||||
ip1, port1 = inv.addr
|
||||
icon2 = ''
|
||||
descr2 = ''
|
||||
ip2 = '--'
|
||||
port2 = '--'
|
||||
|
||||
if inv.remote.ifc:
|
||||
ip2, port2 = inv.remote.ifc.r_addr
|
||||
icon2, descr2 = _get_cloud_icon(client_mode)
|
||||
icon2 = _get_cloud_icon(client_mode)
|
||||
|
||||
row = []
|
||||
row.append(f'<i class="fa {icon1}" title="{_(descr1)}"></i> {ip1}:{port1}')
|
||||
row.append(f'<i class="fa {icon1}" title="{_(descr1)}"></i> {ip1}')
|
||||
row.append(f'<i class="fa {icon1}"></i> {ip1}:{port1}')
|
||||
row.append(f'<i class="fa {icon1}"></i> {ip1}')
|
||||
row.append(inv_serial)
|
||||
row.append(f'<i class="fa {icon2}" title="{_(descr2)}"></i> {ip2}:{port2}')
|
||||
row.append(f'<i class="fa {icon2}" title="{_(descr2)}"></i> {ip2}')
|
||||
row.append(f'<i class="fa {icon2}"></i> {ip2}:{port2}')
|
||||
row.append(f'<i class="fa {icon2}"></i> {ip2}')
|
||||
return row
|
||||
|
||||
|
||||
|
||||
@@ -46,13 +46,10 @@ def get_table_data():
|
||||
@web.route('/mqtt-fetch')
|
||||
async def mqtt_fetch():
|
||||
mqtt = Mqtt(None)
|
||||
cdatetime = format_datetime(dt=mqtt.ctime, format='d.MM. HH:mm')
|
||||
ctime = format_datetime(dt=mqtt.ctime, format='short')
|
||||
data = {
|
||||
"update-time": format_datetime(format="medium"),
|
||||
"mqtt-ctime": f"""
|
||||
<h3 class="w3-hide-small w3-hide-medium">{cdatetime}</h3>
|
||||
<h4 class="w3-hide-large">{cdatetime}</h4>
|
||||
""",
|
||||
"mqtt-ctime": f"<h3>{ctime}</h3>",
|
||||
"mqtt-tx": f"<h3>{mqtt.published}</h3>",
|
||||
"mqtt-rx": f"<h3>{mqtt.received}</h3>",
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i> Close Menu</button>
|
||||
<a href="{{ url_for('.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-network-wired fa-fw"></i> {{_('Connections')}}</a>
|
||||
<a href="{{ url_for('.mqtt')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-database fa-fw"></i> MQTT</a>
|
||||
<a href="{{ url_for('.notes')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-info fa-fw"></i> {{_('Important Messages')}}</a>
|
||||
<a href="{{ url_for('.notes')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-exclamation-triangle fa-fw"></i> {{_('Important Messages')}}</a>
|
||||
<a href="{{ url_for('.logging')}}" class="w3-bar-item w3-button w3-padding {% block menu4_class %}{% endblock %}"><i class="fa fa-file-export fa-fw"></i> {{_('Log Files')}}</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<div id="id01" class="w3-modal">
|
||||
<div class="w3-modal-content" style="width:600px">
|
||||
<div class="w3-container w3-padding-24">
|
||||
<h2>{{_('Do you really want to delete the log file: <br>%(file)s ?', file='<b><span id="id03"></span></b>')}}</h2>
|
||||
<h2>{{_("Do you really want to delete the log file")}}:<br><b><span id="id03"></span></b> ?</h2>
|
||||
<div class="w3-bar">
|
||||
<button id="id02" class="w3-button w3-red" onclick="deleteFile(); document.getElementById('id01').style.display='none'">{{_('Delete File')}}</button>
|
||||
<button id="id02" class="w3-button w3-red" onclick="deleteFile(); document.getElementById('id01').style.display='none'">{{_('Delete File</button')}}>
|
||||
<button class="w3-button w3-grey w3-right" onclick="document.getElementById('id01').style.display='none'">{{_('Abort')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="w3-third">
|
||||
<div class="w3-card-4">
|
||||
<div class="w3-container w3-indigo w3-padding-16">
|
||||
<div class="w3-left"><i class="fa fa-business-time w3-xxxlarge"></i></div>
|
||||
<div class="w3-left"><i class="fa fa-link w3-xxxlarge"></i></div>
|
||||
<div id = "mqtt-ctime" class="w3-right">
|
||||
<h3>-</h3>
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="w3-third">
|
||||
<div class="w3-card-4">
|
||||
<div class="w3-container w3-purple w3-padding-16">
|
||||
<div class="w3-left"><i class="fa fa-angle-double-right w3-xxxlarge"></i></div>
|
||||
<div class="w3-left"><i class="fa fa-server w3-xxxlarge"></i></div>
|
||||
<div id = "mqtt-tx" class="w3-right">
|
||||
<h3>-</h3>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="w3-third">
|
||||
<div class="w3-card-4">
|
||||
<div class="w3-container w3-orange w3-text-white w3-padding-16">
|
||||
<div class="w3-left"><i class="fa fa-angle-double-left w3-xxxlarge"></i></div>
|
||||
<div class="w3-left"><i class="fa fa-user w3-xxxlarge"></i></div>
|
||||
<div id = "mqtt-rx" class="w3-right">
|
||||
<h3>-</h3>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block title %}{{_("TSUN Proxy - Important Messages")}}{% endblock title %}
|
||||
{% block menu3_class %}w3-blue{% endblock %}
|
||||
{% block headline %}<i class="fa fa-info fa-fw"></i> {{_('Important Messages')}}{% endblock headline %}
|
||||
{% block headline %}<i class="fa fa-exclamation-triangle fa-fw"></i> {{_('Important Messages')}}{% endblock headline %}
|
||||
{% block content %}
|
||||
<div id="notes-list"></div>
|
||||
{% endblock content%}
|
||||
|
||||
@@ -17,13 +17,13 @@ def test_statistic_counter():
|
||||
assert val == None or val == 0
|
||||
|
||||
i.static_init() # initialize counter
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "DCU_Command": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
|
||||
val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr
|
||||
assert val == 0
|
||||
|
||||
i.inc_counter('Inverter_Cnt')
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "DCU_Command": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
val = i.dev_value(Register.INVERTER_CNT)
|
||||
assert val == 1
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ def test_default_db():
|
||||
i = InfosG3P(client_mode=False)
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
"inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 2},
|
||||
"inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 4},
|
||||
"collector": {"Chip_Type": "IGEN TECH"},
|
||||
})
|
||||
|
||||
@@ -271,7 +271,7 @@ def test_build_ha_conf1():
|
||||
elif id == 'inv_count_456':
|
||||
assert False
|
||||
|
||||
assert tests==5
|
||||
assert tests==7
|
||||
|
||||
def test_build_ha_conf2():
|
||||
i = InfosG3P(client_mode=False)
|
||||
@@ -346,7 +346,7 @@ def test_build_ha_conf3():
|
||||
elif id == 'inv_count_456':
|
||||
assert False
|
||||
|
||||
assert tests==5
|
||||
assert tests==7
|
||||
|
||||
def test_build_ha_conf4():
|
||||
i = InfosG3P(client_mode=True)
|
||||
|
||||
247
app/tests/test_mqtt.py
Executable file → Normal file
247
app/tests/test_mqtt.py
Executable file → Normal file
@@ -3,10 +3,8 @@ import pytest
|
||||
import asyncio
|
||||
import aiomqtt
|
||||
import logging
|
||||
from aiomqtt import MqttError, MessagesIterator
|
||||
from aiomqtt import Message as AiomqttMessage
|
||||
from mock import patch, Mock
|
||||
|
||||
from mock import patch, Mock
|
||||
from async_stream import AsyncIfcImpl
|
||||
from singleton import Singleton
|
||||
from mqtt import Mqtt
|
||||
@@ -19,7 +17,7 @@ NO_MOSQUITTO_TEST = False
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def module_init():
|
||||
Singleton._instances.clear()
|
||||
yield
|
||||
@@ -35,26 +33,6 @@ def test_hostname():
|
||||
# else:
|
||||
return 'test.mosquitto.org'
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def aiomqtt_mock(monkeypatch):
|
||||
recv_que = asyncio.Queue()
|
||||
|
||||
async def my_aenter(self):
|
||||
return self
|
||||
async def my_subscribe(self, *arg):
|
||||
return
|
||||
async def my_anext(self):
|
||||
return await recv_que.get()
|
||||
async def my_receive(self, topic: str, payload: bytes):
|
||||
msg = AiomqttMessage(topic, payload,qos=0, retain=False, mid=0, properties=None)
|
||||
await recv_que.put(msg)
|
||||
await asyncio.sleep(0) # dispath the msg
|
||||
|
||||
monkeypatch.setattr(aiomqtt.Client, "__aenter__", my_aenter)
|
||||
monkeypatch.setattr(aiomqtt.Client, "subscribe", my_subscribe)
|
||||
monkeypatch.setattr(MessagesIterator, "__anext__", my_anext)
|
||||
monkeypatch.setattr(Mqtt, "receive", my_receive, False)
|
||||
|
||||
@pytest.fixture
|
||||
def config_mqtt_conn(test_hostname, test_port):
|
||||
Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''},
|
||||
@@ -66,14 +44,6 @@ def config_no_conn(test_port):
|
||||
Config.act_config = {'mqtt':{'host': "", 'port': test_port, 'user': '', 'passwd': ''},
|
||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
||||
}
|
||||
Config.def_config = {}
|
||||
|
||||
@pytest.fixture
|
||||
def config_def_conn(test_port):
|
||||
Config.act_config = {'mqtt':{'host': "unknown_url", 'port': test_port, 'user': '', 'passwd': ''},
|
||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
||||
}
|
||||
Config.def_config = Config.act_config
|
||||
|
||||
@pytest.fixture
|
||||
def spy_at_cmd():
|
||||
@@ -99,14 +69,6 @@ def spy_modbus_cmd_client():
|
||||
yield wrapped_conn
|
||||
conn.close()
|
||||
|
||||
@pytest.fixture
|
||||
def spy_dcu_cmd():
|
||||
conn = SolarmanV5(None, ('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
|
||||
conn.node_id = 'inv_3/'
|
||||
with patch.object(conn, 'send_dcu_cmd', wraps=conn.send_dcu_cmd) as wrapped_conn:
|
||||
yield wrapped_conn
|
||||
conn.close()
|
||||
|
||||
def test_native_client(test_hostname, test_port):
|
||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
||||
MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable
|
||||
@@ -182,17 +144,13 @@ async def test_ha_reconnect(config_mqtt_conn):
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_no_config(config_no_conn, monkeypatch):
|
||||
async def test_mqtt_no_config(config_no_conn):
|
||||
_ = config_no_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
async def my_publish(*args):
|
||||
return
|
||||
|
||||
monkeypatch.setattr(aiomqtt.Client, "publish", my_publish)
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
@@ -201,193 +159,74 @@ async def test_mqtt_no_config(config_no_conn, monkeypatch):
|
||||
assert not on_connect.is_set()
|
||||
try:
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
assert m.published == 1
|
||||
assert False
|
||||
except Exception:
|
||||
assert False
|
||||
pass
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_except_no_config(config_no_conn, monkeypatch, caplog):
|
||||
_ = config_no_conn
|
||||
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
async def my_aenter(self):
|
||||
raise MqttError('TestException') from None
|
||||
|
||||
monkeypatch.setattr(aiomqtt.Client, "__aenter__", my_aenter)
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
m = Mqtt(None)
|
||||
assert m.task
|
||||
await asyncio.sleep(0)
|
||||
try:
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
assert False
|
||||
except MqttError:
|
||||
pass
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
assert 'Connection lost; Reconnecting in 5 seconds' in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_except_def_config(config_def_conn, monkeypatch, caplog):
|
||||
_ = config_def_conn
|
||||
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
async def my_aenter(self):
|
||||
raise MqttError('TestException') from None
|
||||
|
||||
monkeypatch.setattr(aiomqtt.Client, "__aenter__", my_aenter)
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
await asyncio.sleep(0)
|
||||
assert not on_connect.is_set()
|
||||
try:
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
assert False
|
||||
except MqttError:
|
||||
pass
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
assert 'MQTT is unconfigured; Check your config.toml!' in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_dispatch(config_mqtt_conn, aiomqtt_mock, spy_modbus_cmd):
|
||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
_ = aiomqtt_mock
|
||||
spy = spy_modbus_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
assert m.ha_restarts == 0
|
||||
await m.receive('homeassistant/status', b'online') # send the message
|
||||
assert m.ha_restarts == 1
|
||||
|
||||
await m.receive(topic= 'tsun/inv_1/rated_load', payload= b'2')
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO)
|
||||
|
||||
|
||||
spy.reset_mock()
|
||||
await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'100')
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'100', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 1024, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'50')
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'50', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 512, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
await m.receive(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10')
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.READ_REGS, 0x3000, 10, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
await m.receive(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10')
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO)
|
||||
|
||||
# test dispatching with empty mapping table
|
||||
m.topic_defs.clear()
|
||||
spy.reset_mock()
|
||||
await m.receive(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10')
|
||||
spy.assert_not_called()
|
||||
|
||||
# test dispatching with incomplete mapping table - invalid fnc defined
|
||||
m.topic_defs.append(
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_inputs',
|
||||
'full_topic': 'tsun/+/modbus_read_inputs', 'fnc': 'addr'}
|
||||
)
|
||||
spy.reset_mock()
|
||||
await m.receive(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10')
|
||||
spy.assert_not_called()
|
||||
|
||||
except MqttError:
|
||||
assert False
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_dispatch_cb(config_mqtt_conn, aiomqtt_mock):
|
||||
async def test_msg_dispatch_err(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
_ = aiomqtt_mock
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.ha_restarts == 0
|
||||
await m.receive('homeassistant/status', b'online') # send the message
|
||||
assert on_connect.is_set()
|
||||
assert m.ha_restarts == 1
|
||||
|
||||
except MqttError:
|
||||
assert False
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_dispatch_err(config_mqtt_conn, aiomqtt_mock, spy_modbus_cmd, caplog):
|
||||
_ = config_mqtt_conn
|
||||
_ = aiomqtt_mock
|
||||
spy = spy_modbus_cmd
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
|
||||
# test out of range param
|
||||
await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'-1')
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'-1', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
# test unknown node_id
|
||||
await m.receive(topic= 'tsun/inv_2/out_coeff', payload= b'2')
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_2/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
# test invalid fload param
|
||||
await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3')
|
||||
spy.assert_not_called()
|
||||
|
||||
await m.receive(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7')
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
await m.receive(topic= 'tsun/inv_1/dcu_power', payload= b'100W')
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
for _ in m.each_inverter(msg, "addr"):
|
||||
pass # do nothing here
|
||||
assert 'Cmd not supported by: inv_1/' in caplog.text
|
||||
except MqttError:
|
||||
assert False
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@@ -428,31 +267,3 @@ async def test_at_cmd_dispatch(config_mqtt_conn, spy_at_cmd):
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dcu_dispatch(config_mqtt_conn, spy_dcu_cmd):
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_dcu_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_3/dcu_power', payload= b'100.0', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_called_once_with(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dcu_inv_value(config_mqtt_conn, spy_dcu_cmd):
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_dcu_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_3/dcu_power', payload= b'99.9', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_3/dcu_power', payload= b'800.1', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@@ -4,10 +4,6 @@ import logging
|
||||
import os
|
||||
from mock import patch
|
||||
from server import app, Server, ProxyState, HypercornLogHndl
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
|
||||
from test_inverter_base import FakeReader, FakeWriter
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -112,20 +108,20 @@ class TestServerClass:
|
||||
assert logging.getLogger('hypercorn.access').level == logging.INFO
|
||||
assert logging.getLogger('hypercorn.error').level == logging.INFO
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
|
||||
s.parse_args(['--log_backups', '3'])
|
||||
s.init_logging_system()
|
||||
assert s.log_backups == 3
|
||||
assert s.log_level == logging.WARNING
|
||||
assert logging.handlers.log_backups == 3
|
||||
assert logging.getLogger().level == s.log_level
|
||||
assert logging.getLogger('msg').level == s.log_level
|
||||
assert logging.getLogger('conn').level == s.log_level
|
||||
assert logging.getLogger('data').level == s.log_level
|
||||
assert logging.getLogger('tracer').level == s.log_level
|
||||
assert logging.getLogger('asyncio').level == s.log_level
|
||||
assert logging.getLogger('hypercorn.access').level == logging.INFO
|
||||
assert logging.getLogger('hypercorn.error').level == logging.INFO
|
||||
os.environ["LOG_LVL"] = "WARN"
|
||||
s.parse_args(['--log_backups', '3'])
|
||||
s.init_logging_system()
|
||||
assert s.log_backups == 3
|
||||
assert s.log_level == logging.WARNING
|
||||
assert logging.handlers.log_backups == 3
|
||||
assert logging.getLogger().level == s.log_level
|
||||
assert logging.getLogger('msg').level == s.log_level
|
||||
assert logging.getLogger('conn').level == s.log_level
|
||||
assert logging.getLogger('data').level == s.log_level
|
||||
assert logging.getLogger('tracer').level == s.log_level
|
||||
assert logging.getLogger('asyncio').level == s.log_level
|
||||
assert logging.getLogger('hypercorn.access').level == logging.INFO
|
||||
assert logging.getLogger('hypercorn.error').level == logging.INFO
|
||||
|
||||
def test_build_config_error(self, caplog):
|
||||
s = self.FakeServer()
|
||||
@@ -206,81 +202,17 @@ class TestApp:
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthy(self):
|
||||
"""Test the healthy route."""
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent):
|
||||
ProxyState.set_up(False)
|
||||
client = app.test_client()
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
ProxyState.set_up(False)
|
||||
client = app.test_client()
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
|
||||
ProxyState.set_up(True)
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
ProxyState.set_up(True)
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unhealthy(self, monkeypatch, caplog):
|
||||
"""Test the healthy route."""
|
||||
def result_false(self):
|
||||
return False
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
monkeypatch.setattr(InverterBase, "healthy", result_false)
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with caplog.at_level(logging.INFO) and InverterBase(reader, writer, 'tsun', Talent):
|
||||
ProxyState.set_up(False)
|
||||
client = app.test_client()
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
assert "" == caplog.text
|
||||
|
||||
ProxyState.set_up(True)
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 503
|
||||
result = await response.get_data()
|
||||
assert result == b"I have a problem"
|
||||
assert "" == caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthy_exception(self, monkeypatch, caplog):
|
||||
"""Test the healthy route."""
|
||||
def result_except(self):
|
||||
raise ValueError
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
monkeypatch.setattr(InverterBase, "healthy", result_except)
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with caplog.at_level(logging.INFO) and InverterBase(reader, writer, 'tsun', Talent):
|
||||
ProxyState.set_up(False)
|
||||
client = app.test_client()
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
assert "" == caplog.text
|
||||
|
||||
ProxyState.set_up(True)
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
assert "Exception:" in caplog.text
|
||||
|
||||
273
app/tests/test_solarman.py
Executable file → Normal file
273
app/tests/test_solarman.py
Executable file → Normal file
@@ -462,39 +462,6 @@ def inverter_ind_msg800(): # 0x4210 rated Power 800W
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inverter_ind_msg900(): # 0x4210 rated Power 900W
|
||||
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||
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\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\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x03\x84\x06\x7a'
|
||||
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\xff\xff\x03\x84\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inverter_ind_msg_81(): # 0x4210 fcode 0x81
|
||||
msg = b'\xa5\x99\x01\x10\x42\x02\x03' +get_sn() +b'\x81\xb0\x02\xbc\xc8'
|
||||
@@ -709,19 +676,6 @@ def msg_modbus_rsp(): # 0x1510
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_modbus_rsp_mb_4(): # 0x1510, MODBUS Type:4
|
||||
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
|
||||
msg += total()
|
||||
msg += hb()
|
||||
msg += b'\x0a\xe2\xfa\x33\x01\x04\x28\x40\x10\x08\xd8'
|
||||
msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02'
|
||||
msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04'
|
||||
msg += b'\x00\x01\x00\x00\x9e\xa4'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_modbus_interim_rsp(): # 0x0510
|
||||
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
|
||||
@@ -858,26 +812,6 @@ def dcu_data_rsp_msg(): # 0x1210
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def dcu_command_ind_msg(): # 0x4510
|
||||
msg = b'\xa5\x17\x00\x10\x45\x94\x02' +get_dcu_sn() +b'\x05\x26\x30'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x01\x01\x06\x01\x00\x01\x03\xe8'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def dcu_command_rsp_msg(): # 0x1510
|
||||
msg = b'\xa5\x11\x00\x10\x15\x94\x03' +get_dcu_sn() +b'\x05\x01'
|
||||
msg += total()
|
||||
msg += hb()
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += b'\x01\x01\x01'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def config_tsun_allow_all():
|
||||
Config.act_config = {
|
||||
@@ -920,17 +854,7 @@ def config_tsun_scan_dcu():
|
||||
|
||||
@pytest.fixture
|
||||
def config_tsun_dcu1():
|
||||
Config.act_config = {
|
||||
'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},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||
Proxy.class_init()
|
||||
Proxy.mqtt = Mqtt()
|
||||
Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_message(device_ind_msg):
|
||||
@@ -1481,7 +1405,6 @@ async def test_build_modell_600(my_loop, config_tsun_allow_all, inverter_ind_msg
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert 600 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||
assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
assert 'TSOL-MS2000(600)' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||
assert '02b0' == m.db.get_db_value(Register.SENSOR_LIST, None)
|
||||
assert 0 == m.sensor_list # must not been set by an inverter data ind
|
||||
@@ -1501,7 +1424,6 @@ async def test_build_modell_1600(my_loop, config_tsun_allow_all, inverter_ind_ms
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert 1600 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert 1600 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||
assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
assert 'TSOL-MS1600' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||
m.close()
|
||||
|
||||
@@ -1515,7 +1437,6 @@ async def test_build_modell_1800(my_loop, config_tsun_allow_all, inverter_ind_ms
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert 1800 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert 1800 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||
assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
assert 'TSOL-MS1800' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||
m.close()
|
||||
|
||||
@@ -1529,7 +1450,6 @@ async def test_build_modell_2000(my_loop, config_tsun_allow_all, inverter_ind_ms
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert 2000 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||
assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
assert 'TSOL-MS2000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||
m.close()
|
||||
|
||||
@@ -1543,21 +1463,6 @@ async def test_build_modell_800(my_loop, config_tsun_allow_all, inverter_ind_msg
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert 800 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert 800 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||
assert 2 == m.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
assert 'TSOL-MS800' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||
m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_modell_900(my_loop, config_tsun_allow_all, inverter_ind_msg900):
|
||||
_ = config_tsun_allow_all
|
||||
m = MemoryStream(inverter_ind_msg900, (0,))
|
||||
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert None == m.db.get_db_value(Register.RATED_POWER, None)
|
||||
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert 900 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
assert 900 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||
assert 2 == m.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
assert 'TSOL-MSxx00' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||
m.close()
|
||||
|
||||
@@ -2254,61 +2159,6 @@ async def test_modbus_scaning(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp
|
||||
assert next(m.mb_timer.exp_count) == 3
|
||||
m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_scaning_inv_rsp(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp_msg, msg_modbus_rsp_mb_4):
|
||||
_ = config_tsun_scan
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
m = MemoryStream(heartbeat_ind_msg, (0x15,0x56,0))
|
||||
m.append_msg(msg_modbus_rsp_mb_4)
|
||||
assert m.mb_scan == False
|
||||
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
assert m.mb_timer.tim == None
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert m.mb_scan == True
|
||||
assert m.mb_start_reg == 0xff80
|
||||
assert m.mb_step == 0x40
|
||||
assert m.mb_bytes == 0x14
|
||||
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert m.msg_count == 1
|
||||
assert m.snr == 2070233889
|
||||
assert m.control == 0x4710
|
||||
|
||||
assert m.msg_recvd[0]['control']==0x4710
|
||||
assert m.msg_recvd[0]['seq']=='84:11'
|
||||
assert m.msg_recvd[0]['data_len']==0x1
|
||||
|
||||
assert m.ifc.tx_fifo.get()==heartbeat_rsp_msg
|
||||
assert m.ifc.fwd_fifo.get()==heartbeat_ind_msg
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
|
||||
m.ifc.tx_clear() # clear send buffer for next test
|
||||
assert isclose(m.mb_timeout, 0.5)
|
||||
assert next(m.mb_timer.exp_count) == 0
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
assert m.sent_pdu==b'\xa5\x17\x00\x10E\x12\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \
|
||||
b'\x00\x00\x00\x00\x00\x00\x01\x03\xff\xc0\x00\x14\x75\xed\x33\x15'
|
||||
assert m.ifc.tx_fifo.get()==b''
|
||||
|
||||
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 == 2
|
||||
assert m.msg_recvd[1]['control']==0x1510
|
||||
assert m.msg_recvd[1]['seq']=='03:03'
|
||||
assert m.msg_recvd[1]['data_len']==0x3b
|
||||
assert m.mb.last_addr == 1
|
||||
assert m.mb.last_fcode == 3
|
||||
assert m.mb.last_reg == 0xffc0 # mb_start_reg + mb_step
|
||||
assert m.mb.last_len == 20
|
||||
assert m.mb.err == 3
|
||||
|
||||
assert next(m.mb_timer.exp_count) == 2
|
||||
m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_client_mode(my_loop, config_tsun_inv1, str_test_ip):
|
||||
_ = config_tsun_inv1
|
||||
@@ -2552,124 +2402,3 @@ async def test_proxy_at_blocked(my_loop, config_tsun_inv1, patch_open_connection
|
||||
|
||||
assert Proxy.mqtt.key == 'tsun/inv1/at_resp'
|
||||
assert Proxy.mqtt.data == "+ok"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dcu_cmd(my_loop, config_tsun_allow_all, dcu_dev_ind_msg, dcu_dev_rsp_msg, dcu_data_ind_msg, dcu_data_rsp_msg, dcu_command_ind_msg, dcu_command_rsp_msg):
|
||||
'''test dcu_power command fpr a DCU device with sensor 0x3026'''
|
||||
_ = config_tsun_allow_all
|
||||
m = MemoryStream(dcu_dev_ind_msg, (0,), True)
|
||||
m.read() # read device ind
|
||||
assert m.control == 0x4110
|
||||
assert str(m.seq) == '01:92'
|
||||
assert m.ifc.tx_fifo.get()==dcu_dev_rsp_msg
|
||||
assert m.ifc.fwd_fifo.get()==dcu_dev_ind_msg
|
||||
|
||||
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
|
||||
assert m.ifc.tx_fifo.get()==b''
|
||||
assert m.ifc.fwd_fifo.get()==b''
|
||||
assert m.sent_pdu == b''
|
||||
assert str(m.seq) == '01:92'
|
||||
assert Proxy.mqtt.key == ''
|
||||
assert Proxy.mqtt.data == ""
|
||||
|
||||
m.append_msg(dcu_data_ind_msg)
|
||||
m.read() # read inverter ind
|
||||
assert m.control == 0x4210
|
||||
assert str(m.seq) == '02:93'
|
||||
assert m.ifc.tx_fifo.get()==dcu_data_rsp_msg
|
||||
assert m.ifc.fwd_fifo.get()==dcu_data_ind_msg
|
||||
|
||||
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
|
||||
assert m.ifc.fwd_fifo.get() == b''
|
||||
assert m.ifc.tx_fifo.get()== b''
|
||||
assert m.sent_pdu == dcu_command_ind_msg
|
||||
m.sent_pdu = bytearray()
|
||||
|
||||
assert str(m.seq) == '02:94'
|
||||
assert Proxy.mqtt.key == ''
|
||||
assert Proxy.mqtt.data == ""
|
||||
|
||||
m.append_msg(dcu_command_rsp_msg)
|
||||
m.read() # read at resp
|
||||
assert m.control == 0x1510
|
||||
assert str(m.seq) == '03:94'
|
||||
assert m.ifc.rx_get()==b''
|
||||
assert m.ifc.tx_fifo.get()==b''
|
||||
assert m.ifc.fwd_fifo.get()==b''
|
||||
assert Proxy.mqtt.key == 'tsun/dcu_resp'
|
||||
assert Proxy.mqtt.data == "+ok"
|
||||
Proxy.mqtt.clear() # clear last test result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dcu_cmd_not_supported(my_loop, config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg):
|
||||
'''test that an inverter don't accept the dcu_power command'''
|
||||
_ = config_tsun_allow_all
|
||||
m = MemoryStream(device_ind_msg, (0,), True)
|
||||
m.read() # read device ind
|
||||
assert m.control == 0x4110
|
||||
assert str(m.seq) == '01:01'
|
||||
assert m.ifc.tx_fifo.get()==device_rsp_msg
|
||||
assert m.ifc.fwd_fifo.get()==device_ind_msg
|
||||
|
||||
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
|
||||
assert m.ifc.tx_fifo.get()==b''
|
||||
assert m.ifc.fwd_fifo.get()==b''
|
||||
assert m.sent_pdu == b''
|
||||
assert str(m.seq) == '01:01'
|
||||
assert Proxy.mqtt.key == ''
|
||||
assert Proxy.mqtt.data == ""
|
||||
|
||||
m.append_msg(inverter_ind_msg)
|
||||
m.read() # read inverter ind
|
||||
assert m.control == 0x4210
|
||||
assert str(m.seq) == '02:02'
|
||||
assert m.ifc.tx_fifo.get()==inverter_rsp_msg
|
||||
assert m.ifc.fwd_fifo.get()==inverter_ind_msg
|
||||
|
||||
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
|
||||
assert m.ifc.fwd_fifo.get() == b''
|
||||
assert m.ifc.tx_fifo.get()== b''
|
||||
assert m.sent_pdu == b''
|
||||
Proxy.mqtt.clear() # clear last test result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_dcu_cmd(my_loop, config_tsun_dcu1, patch_open_connection, dcu_command_ind_msg, dcu_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']['DCU_Command'] = 0
|
||||
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_dcu_cmd_resp = False
|
||||
r.append_msg(dcu_command_ind_msg)
|
||||
r.read() # read complete msg, and dispatch msg
|
||||
assert inverter.forward_dcu_cmd_resp
|
||||
inverter.forward(r,l)
|
||||
|
||||
assert l.ifc.tx_fifo.get()==dcu_command_ind_msg
|
||||
|
||||
assert l.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||
assert l.db.stat['proxy']['DCU_Command'] == 1
|
||||
assert l.db.stat['proxy']['AT_Command'] == 0
|
||||
assert l.db.stat['proxy']['AT_Command_Blocked'] == 0
|
||||
assert l.db.stat['proxy']['Modbus_Command'] == 0
|
||||
assert 2 == l.db.get_db_value(Register.NO_INPUTS, 0)
|
||||
|
||||
l.append_msg(dcu_command_rsp_msg)
|
||||
l.read() # read at resp
|
||||
assert l.ifc.fwd_fifo.peek()==dcu_command_rsp_msg
|
||||
inverter.forward(l,r)
|
||||
assert r.ifc.tx_fifo.get()==dcu_command_rsp_msg
|
||||
|
||||
assert Proxy.mqtt.key == ''
|
||||
assert Proxy.mqtt.data == ""
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-05-13 22:34+0200\n"
|
||||
"POT-Creation-Date: 2025-05-04 18:16+0200\n"
|
||||
"PO-Revision-Date: 2025-04-18 16:24+0200\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de\n"
|
||||
@@ -19,34 +19,30 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: src/web/conn_table.py:53 src/web/templates/base.html.j2:58
|
||||
#: src/web/conn_table.py:52 src/web/templates/base.html.j2:58
|
||||
msgid "Connections"
|
||||
msgstr "Verbindungen"
|
||||
|
||||
#: src/web/conn_table.py:60
|
||||
#: src/web/conn_table.py:59
|
||||
msgid "Device-IP:Port"
|
||||
msgstr "Geräte-IP:Port"
|
||||
|
||||
#: src/web/conn_table.py:60
|
||||
#: src/web/conn_table.py:59
|
||||
msgid "Device-IP"
|
||||
msgstr "Geräte-IP"
|
||||
|
||||
#: src/web/conn_table.py:61 src/web/mqtt_table.py:34
|
||||
#: src/web/conn_table.py:60 src/web/mqtt_table.py:34
|
||||
msgid "Serial-No"
|
||||
msgstr "Seriennummer"
|
||||
|
||||
#: src/web/conn_table.py:62
|
||||
#: src/web/conn_table.py:61
|
||||
msgid "Cloud-IP:Port"
|
||||
msgstr "Cloud-IP:Port"
|
||||
|
||||
#: src/web/conn_table.py:62
|
||||
#: src/web/conn_table.py:61
|
||||
msgid "Cloud-IP"
|
||||
msgstr "Cloud-IP"
|
||||
|
||||
#: src/web/log_files.py:48
|
||||
msgid "n/a"
|
||||
msgstr "keine Angabe"
|
||||
|
||||
#: src/web/mqtt_table.py:27
|
||||
msgid "MQTT devices"
|
||||
msgstr "MQTT Geräte"
|
||||
@@ -120,12 +116,12 @@ msgid "TSUN Proxy - Log Files"
|
||||
msgstr "TSUN Proxy - Log Dateien"
|
||||
|
||||
#: src/web/templates/page_logging.html.j2:10
|
||||
msgid "Do you really want to delete the log file: <br>%(file)s ?"
|
||||
msgstr "Soll die Datei: <br>%(file)s<br>wirklich gelöscht werden?"
|
||||
msgid "Do you really want to delete the log file"
|
||||
msgstr "Soll die Datei wirklich gelöscht werden"
|
||||
|
||||
#: src/web/templates/page_logging.html.j2:12
|
||||
msgid "Delete File"
|
||||
msgstr "Datei löschen"
|
||||
msgid "Delete File</button"
|
||||
msgstr "File löschen"
|
||||
|
||||
#: src/web/templates/page_logging.html.j2:13
|
||||
msgid "Abort"
|
||||
|
||||
@@ -192,7 +192,7 @@ $(repro_all_subdirs) :
|
||||
mkdir -p $@
|
||||
|
||||
$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version FORCE
|
||||
$(JINJA) --strict -D AppVersion=$(VERSION)-$*$(RC) -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@
|
||||
$(JINJA) --strict -D AppVersion=$(VERSION)-$* -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@
|
||||
|
||||
$(repro_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json
|
||||
$(JINJA) --strict $< $(filter %.json,$^) -o $@
|
||||
|
||||
@@ -18,7 +18,7 @@ ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.5"
|
||||
FROM $BUILD_FROM AS base
|
||||
|
||||
# Installiere Python, pip und virtuelle Umgebungstools
|
||||
RUN apk add --no-cache python3=3.12.10-r1 py3-pip=24.3.1-r0 && \
|
||||
RUN apk add --no-cache python3=3.12.10-r0 py3-pip=24.3.1-r0 && \
|
||||
python -m venv /opt/venv && \
|
||||
. /opt/venv/bin/activate
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
"name": "TSUN-Proxy (Release Candidate)",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||
"version": "rc",
|
||||
"image": "ghcr.io/s-allius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-rc",
|
||||
"advanced": true,
|
||||
|
||||
Reference in New Issue
Block a user