From c8af9c2e9a51625d823c24c5f7f621f830ab1bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Fri, 27 Jan 2023 16:24:03 +0100 Subject: [PATCH 1/2] RPi:MQTT support QoS, Retain and Last-Will To support Quality of Service, Retain and Last-Will Switch in ahoy.yml.example --- tools/rpi/ahoy.yml.example | 5 ++++ tools/rpi/hoymiles/__main__.py | 23 ++++++++++++++- tools/rpi/hoymiles/outputs.py | 52 ++++++++++++++++++++-------------- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index c157e826..fb033f80 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -28,6 +28,11 @@ ahoy: password: 'password' useTLS: False insecureTLS: False #set True for e.g. self signed certificates. + QoS: 0 + Retain: True + last_will: + topic: Appelweg_PV/114181807700 # defaults to 'hoymiles/{serial}' + payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!" # Influx2 output influxdb: diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 1dfc8321..97773ed1 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -17,10 +17,31 @@ from suntimes import SunTimes import argparse import yaml from yaml.loader import SafeLoader -# import paho.mqtt.client import hoymiles import logging +################################################################################ +""" Signal Handler """ +################################################################################ +# from signal import signal, Signals, SIGINT, SIGTERM, SIGKILL, SIGHUP +from signal import * +def signal_handler(sig_num, frame): + signame = Signals(sig_num).name + logging.info(f'Stop by Signal {signame} ({sig_num})') + print (f'Stop by Signal <{signame}> ({sig_num}) at: {time.strftime("%d.%m.%Y %H:%M:%S")}') + + if mqtt_client: + mqtt_client.disco() + + sys.exit(0) + +signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C) +signal(SIGTERM, signal_handler) # Signal Handler from terminating processes +signal(SIGHUP, signal_handler) # Hangup detected on controlling terminal or death of controlling process +# signal(SIGKILL, signal_handler) # Signal Handler SIGKILL and SIGSTOP cannot be caught, blocked, or ignored!! +################################################################################ +################################################################################ + class InfoCommands(IntEnum): InverterDevInform_Simple = 0 # 0x00 InverterDevInform_All = 1 # 0x01 diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 18af494a..723d82ca 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -179,10 +179,18 @@ class MqttOutputPlugin(OutputPluginFactory): mqtt_client.tls_set() mqtt_client.tls_insecure_set(config.get('insecureTLS',False)) mqtt_client.username_pw_set(config.get('user', None), config.get('password', None)) + mqtt_client.will_set(str(config.get('last_will', {}).get('topic', 'hoymiles')), str(config.get('last_will', None).get('payload', None))) + mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883)) mqtt_client.loop_start() self.client = mqtt_client + self.qos = config.get('QoS', 0) # Quality of Service + self.ret = config.get('Retain', True) # Retain Message + + def disco(self, **params): + self.client.loop_stop() # Stop loop + self.client.disconnect() # disconnect def store_status(self, response, **params): """ @@ -202,17 +210,17 @@ class MqttOutputPlugin(OutputPluginFactory): # Global Head if data['time'] is not None: - self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) + self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"), self.qos, self.ret) # AC Data phase_id = 0 phase_sum_power = 0 for phase in data['phases']: - self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) - self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) - self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) - self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power']) - self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency']) + self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'], self.qos, self.ret) phase_id = phase_id + 1 phase_sum_power += phase['power'] @@ -220,36 +228,38 @@ class MqttOutputPlugin(OutputPluginFactory): string_id = 0 string_sum_power = 0 for string in data['strings']: - self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) - self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) - self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) - self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily']) - self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000) - self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) + self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000, self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'], self.qos, self.ret) string_id = string_id + 1 string_sum_power += string['power'] # Global if data['event_count'] is not None: - self.client.publish(f'{topic}/total_events', data['event_count']) + self.client.publish(f'{topic}/total_events', data['event_count'], self.qos, self.ret) if data['powerfactor'] is not None: - self.client.publish(f'{topic}/PF_AC', data['powerfactor']) - self.client.publish(f'{topic}/Temp', data['temperature']) + self.client.publish(f'{topic}/PF_AC', data['powerfactor'], self.qos, self.ret) + self.client.publish(f'{topic}/Temp', data['temperature'], self.qos, self.ret) if data['yield_total'] is not None: - self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000) + self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000, self.qos, self.ret) if data['yield_today'] is not None: - self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000) - self.client.publish(f'{topic}/Efficiency', data['efficiency']) + self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000, self.qos, self.ret) + self.client.publish(f'{topic}/Efficiency', data['efficiency'], self.qos, self.ret) elif isinstance(response, HardwareInfoResponse): self.client.publish(f'{topic}/Firmware/Version',\ - f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}') + f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}', self.qos, self.ret) self.client.publish(f'{topic}/Firmware/Build_at',\ - f'{data["FW_build_dd"]}/{data["FW_build_mm"]}/{data["FW_build_yy"]}T{data["FW_build_HH"]}:{data["FW_build_MM"]}') + f'{data["FW_build_dd"]}/{data["FW_build_mm"]}/{data["FW_build_yy"]}T{data["FW_build_HH"]}:{data["FW_build_MM"]}',\ + self.qos, self.ret) - self.client.publish(f'{topic}/Firmware/HWPartId', f'{data["FW_HW_ID"]}') + self.client.publish(f'{topic}/Firmware/HWPartId',\ + f'{data["FW_HW_ID"]}', self.qos, self.ret) else: raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse') From 20abf8d3ba0932f6456f90a10af6d28f6d9ed7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Fri, 27 Jan 2023 16:48:17 +0100 Subject: [PATCH 2/2] RPi:MQTT Last-Will - handling empty config To handle empty config for last-will --- tools/rpi/hoymiles/outputs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 723d82ca..8fb55f3e 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -179,7 +179,12 @@ class MqttOutputPlugin(OutputPluginFactory): mqtt_client.tls_set() mqtt_client.tls_insecure_set(config.get('insecureTLS',False)) mqtt_client.username_pw_set(config.get('user', None), config.get('password', None)) - mqtt_client.will_set(str(config.get('last_will', {}).get('topic', 'hoymiles')), str(config.get('last_will', None).get('payload', None))) + + last_will = config.get('last_will', None) + if last_will: + lw_topic = last_will.get('topic', 'last will hoymiles') + lw_payload = last_will.get('payload', 'last will') + mqtt_client.will_set(str(lw_topic), str(lw_payload)) mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883)) mqtt_client.loop_start()