From d80a9c83a8d52917319e287c13e82500f1e9d2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Mon, 16 Jan 2023 19:38:20 +0100 Subject: [PATCH 1/5] RPi:extend decoder for HardwareInfoResponse output To print Firmware data in main or in output.py, we need to add a new method __dict__() to class HardwareInfoResponse --- tools/rpi/hoymiles/__init__.py | 3 ++ tools/rpi/hoymiles/decoders/__init__.py | 46 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 169ad732..4b46a95e 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -158,6 +158,9 @@ class ResponseDecoder(ResponseDecoderFactory): model = self.inverter_model command = self.request_command + c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") + logging.debug(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') + model_decoders = __import__('hoymiles.decoders') if hasattr(model_decoders, f'{model}Decode{command.upper()}'): device = getattr(model_decoders, f'{model}Decode{command.upper()}') diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index c379f2e4..040f67ef 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -343,10 +343,14 @@ class HardwareInfoResponse(UnknownResponse): }; self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') """ - fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10]) + + def __dict__(self): + """ Base values, availabe in each __dict__ call """ responce_info = self.response - logging.debug(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') + logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') + + fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10]) fw_version_maj = int((fw_version / 10000)) fw_version_min = int((fw_version % 10000) / 100) @@ -355,10 +359,22 @@ class HardwareInfoResponse(UnknownResponse): fw_build_dd = int(fw_build_mmdd % 100) fw_build_HH = int(fw_build_hhmm / 100) fw_build_MM = int(fw_build_hhmm % 100) - logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\ + logging.info(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\ f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\ f'HW revision {hw_id}') + data = super().__dict__() + data['FW_ver_maj'] = fw_version_maj + data['FW_ver_min'] = fw_version_min + data['FW_ver_pat'] = fw_version_pat + data['FW_build_yy'] = fw_build_yyyy + data['FW_build_mm'] = fw_build_mm + data['FW_build_dd'] = fw_build_dd + data['FW_build_HH'] = fw_build_HH + data['FW_build_MM'] = fw_build_MM + data['FW_HW_ID'] = hw_id + return data + class DebugDecodeAny(UnknownResponse): """Default decoder""" @@ -405,10 +421,10 @@ class DebugDecodeAny(UnknownResponse): # 1121-Series Intervers, 1 MPPT, 1 Phase class Hm300Decode01(HardwareInfoResponse): - """ Firmware version / date """ + """ 1121-series Firmware version / date """ class Hm300Decode02(EventsResponse): - """ Inverter generic events log """ + """ 1121-series Inverter generic events log """ class Hm300Decode0B(StatusResponse): """ 1121-series mirco-inverters status data """ @@ -469,18 +485,18 @@ class Hm300Decode0C(Hm300Decode0B): """ 1121-series mirco-inverters status data """ class Hm300Decode11(EventsResponse): - """ Inverter generic events log """ + """ 1121-series Inverter generic events log """ class Hm300Decode12(EventsResponse): - """ Inverter major events log """ + """ 1121-series Inverter major events log """ # 1141-Series Inverters, 2 MPPT, 1 Phase class Hm600Decode01(HardwareInfoResponse): - """ Firmware version / date """ + """ 1141-Series Firmware version / date """ class Hm600Decode02(EventsResponse): - """ Inverter generic events log """ + """ 1141-Series Inverter generic events log """ class Hm600Decode0B(StatusResponse): """ 1141-series mirco-inverters status data """ @@ -576,18 +592,18 @@ class Hm600Decode0C(Hm600Decode0B): """ 1141-series mirco-inverters status data """ class Hm600Decode11(EventsResponse): - """ Inverter generic events log """ + """ 1141-Series Inverter generic events log """ class Hm600Decode12(EventsResponse): - """ Inverter major events log """ + """ 1141-Series Inverter major events log """ # 1161-Series Inverters, 2 MPPT, 1 Phase class Hm1200Decode01(HardwareInfoResponse): - """ Firmware version / date """ + """ 1161-Series Firmware version / date """ class Hm1200Decode02(EventsResponse): - """ Inverter generic events log """ + """ 1161-Series Inverter generic events log """ class Hm1200Decode0B(StatusResponse): """ 1161-series mirco-inverters status data """ @@ -737,7 +753,7 @@ class Hm1200Decode0C(Hm1200Decode0B): """ 1161-series mirco-inverters status data """ class Hm1200Decode11(EventsResponse): - """ Inverter generic events log """ + """ 1161-Series Inverter generic events log """ class Hm1200Decode12(EventsResponse): - """ Inverter major events log """ + """ 1161-Series Inverter major events log """ From 67ed21ae2ac2ae998924c95a8dbc09c2004b0c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Thu, 19 Jan 2023 11:26:21 +0100 Subject: [PATCH 2/5] RPi:print HardwareInfoResponse on MQTT channel print HardwareInfoResponse on MQTT channel check: HardwareInfoResponse does not print on VZ --- tools/rpi/hoymiles/__main__.py | 100 +++++++++++---------------------- tools/rpi/hoymiles/outputs.py | 90 +++++++++++++++++------------ 2 files changed, 86 insertions(+), 104 deletions(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index d9e35df1..c5451fb0 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -81,8 +81,8 @@ def main_loop(ahoy_config): sunset = SunsetHandler(ahoy_config.get('sunset')) dtu_ser = ahoy_config.get('dtu', {}).get('serial') - loop_interval = ahoy_config.get('interval', 1) + try: do_init = True while True: @@ -92,7 +92,7 @@ def main_loop(ahoy_config): for inverter in inverters: if hoymiles.HOYMILES_DEBUG_LOGGING: - logging.debug(f'Poll inverter {inverter["serial"]}') + logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') poll_inverter(inverter, dtu_ser, do_init, 3) do_init = False @@ -161,6 +161,8 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): c_datetime = datetime.now() if hoymiles.HOYMILES_DEBUG_LOGGING: logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) + + # prepare decoder object decoder = hoymiles.ResponseDecoder(response, request=com.request, inverter_ser=inverter_ser, @@ -168,71 +170,35 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): dtu_ser=dtu_ser, strings=inverter_strings ) + + # get decoder object result = decoder.decode() - if isinstance(result, hoymiles.decoders.StatusResponse): - data = result.__dict__() + if hoymiles.HOYMILES_DEBUG_LOGGING: + logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') - if hoymiles.HOYMILES_DEBUG_LOGGING: - logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') + # check decoder object for output + if isinstance(result, hoymiles.decoders.StatusResponse): + data = result.__dict__() if 'event_count' in data: if event_message_index[inv_str] < data['event_count']: event_message_index[inv_str] = data['event_count'] command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) if mqtt_client: - # mqtt_send_status(mqtt_client, inverter_ser, data, topic=inverter.get('mqtt', {}).get('topic', None)) - mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) + mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) if influx_client: - influx_client.store_status(result) + influx_client.store_status(result) if volkszaehler_client: - volkszaehler_client.store_status(result) - -def mqtt_send_status(broker, inverter_ser, data, topic=None): - """ - Publish StatusResponse object + volkszaehler_client.store_status(result) - :param paho.mqtt.client.Client broker: mqtt-client instance - :param str inverter_ser: inverter serial - :param hoymiles.StatusResponse data: decoded inverter StatusResponse - :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) - :type topic: str - """ + # check decoder object for output + if isinstance(result, hoymiles.decoders.HardwareInfoResponse): + if mqtt_client: + mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) - if not topic: - topic = f'hoymiles/{inverter_ser}' - - # Global Head - if data['time'] is not None: - broker.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) - - # AC Data - phase_id = 0 - for phase in data['phases']: - broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) - broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) - broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) - broker.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power']) - phase_id = phase_id + 1 - - # DC Data - string_id = 0 - for string in data['strings']: - broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) - broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) - broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) - broker.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily']) - broker.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000) - string_id = string_id + 1 - # Global - if data['powerfactor'] is not None: - broker.publish(f'{topic}/pf', data['powerfactor']) - broker.publish(f'{topic}/frequency', data['frequency']) - broker.publish(f'{topic}/temperature', data['temperature']) - if data['energy_total'] is not None: - broker.publish(f'{topic}/total', data['energy_total']/1000) def mqtt_on_command(client, userdata, message): """ @@ -321,29 +287,27 @@ if __name__ == '__main__': logging.error('Failed to load config file {global_config.config_file}: {e_yaml}') sys.exit(1) + # read AHOY configuration file and prepare logging ahoy_config = dict(cfg.get('ahoy', {})) init_logging(ahoy_config) - # Prepare for multiple transceivers, makes them configurable (currently - # only one supported) - for radio_config in ahoy_config.get('nrf', [{}]): - hmradio = hoymiles.HoymilesNRF(**radio_config) - - event_message_index = {} - command_queue = {} - mqtt_command_topic_subs = [] - if global_config.log_transactions: hoymiles.HOYMILES_TRANSACTION_LOGGING=True if global_config.verbose: hoymiles.HOYMILES_DEBUG_LOGGING=True + # Prepare for multiple transceivers, makes them configurable + for radio_config in ahoy_config.get('nrf', [{}]): + hmradio = hoymiles.HoymilesNRF(**radio_config) + + # create MQTT - client object mqtt_client = None mqtt_config = ahoy_config.get('mqtt', {}) if mqtt_config and not mqtt_config.get('disabled', False): from .outputs import MqttOutputPlugin mqtt_client = MqttOutputPlugin(mqtt_config) + # create INFLUX - client object influx_client = None influx_config = ahoy_config.get('influxdb', {}) if influx_config and not influx_config.get('disabled', False): @@ -355,23 +319,24 @@ if __name__ == '__main__': bucket=influx_config.get('bucket', None), measurement=influx_config.get('measurement', 'hoymiles')) + # create VOLKSZAEHLER - client object volkszaehler_client = None volkszaehler_config = ahoy_config.get('volkszaehler', {}) if volkszaehler_config and not volkszaehler_config.get('disabled', False): from .outputs import VolkszaehlerOutputPlugin - volkszaehler_client = VolkszaehlerOutputPlugin( - volkszaehler_config) + volkszaehler_client = VolkszaehlerOutputPlugin(volkszaehler_config) + + event_message_index = {} + command_queue = {} + mqtt_command_topic_subs = [] - g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])] for g_inverter in ahoy_config.get('inverters', []): g_inverter_ser = g_inverter.get('serial') inv_str = str(g_inverter_ser) command_queue[inv_str] = [] event_message_index[inv_str] = 0 - # # Enables and subscribe inverter to mqtt /command-Topic - # if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False): topic_item = ( str(g_inverter_ser), @@ -380,5 +345,6 @@ if __name__ == '__main__': mqtt_client.subscribe(topic_item[1]) mqtt_command_topic_subs.append(topic_item) - logging.info(f'Starting main_loop with inverter(s) {g_inverters}') + # start main-loop main_loop(ahoy_config) + diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index e70c3b8d..64763c37 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -8,7 +8,7 @@ Hoymiles output plugin library import socket import logging from datetime import datetime, timezone -from hoymiles.decoders import StatusResponse +from hoymiles.decoders import StatusResponse, HardwareInfoResponse try: from influxdb_client import InfluxDBClient @@ -185,45 +185,56 @@ class MqttOutputPlugin(OutputPluginFactory): :raises ValueError: when response is not instance of StatusResponse """ - if not isinstance(response, StatusResponse): - raise ValueError('Data needs to be instance of StatusResponse') - data = response.__dict__() topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' - # Global Head - if data['time'] is not None: - self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) - - # AC Data - phase_id = 0 - for phase in data['phases']: - self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) - 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}/Q_AC', phase['reactive_power']) - phase_id = phase_id + 1 - - # DC Data - string_id = 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) - if 'irradiation' in string: - self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) - string_id = string_id + 1 - - # Global - if data['powerfactor'] is not None: - self.client.publish(f'{topic}/pf', data['powerfactor']) - self.client.publish(f'{topic}/frequency', data['frequency']) - - self.client.publish(f'{topic}/Temp', data['temperature']) - if data['energy_total'] is not None: - self.client.publish(f'{topic}/total', data['energy_total']/1000) + if isinstance(response, StatusResponse): + + # Global Head + if data['time'] is not None: + self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) + + # AC Data + phase_id = 0 + for phase in data['phases']: + self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) + 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}/Q_AC', phase['reactive_power']) + phase_id = phase_id + 1 + + # DC Data + string_id = 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) + if 'irradiation' in string: + self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) + string_id = string_id + 1 + + # Global + if data['powerfactor'] is not None: + self.client.publish(f'{topic}/pf', data['powerfactor']) + self.client.publish(f'{topic}/frequency', data['frequency']) + + self.client.publish(f'{topic}/Temp', data['temperature']) + if data['energy_total'] is not None: + self.client.publish(f'{topic}/total', data['energy_total']/1000) + + elif isinstance(response, HardwareInfoResponse): + self.client.publish(f'{topic}/Firmware/Version',\ + f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}') + + 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"]}') + + self.client.publish(f'{topic}/Firmware/HWPartId', f'{data["FW_HW_ID"]}') + + else: + raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse') try: import requests @@ -237,6 +248,7 @@ class VzInverterOutput: self.serial = config.get('serial') self.baseurl = config.get('url', 'http://localhost/middleware/') self.channels = dict() + for channel in config.get('channels', []): uid = channel.get('uid') ctype = channel.get('type') @@ -286,6 +298,7 @@ class VzInverterOutput: if data['energy_total'] is not None: self.try_publish(ts, f'total', data['energy_total']) + def try_publish(self, ts, ctype, value): if not ctype in self.channels: return @@ -307,6 +320,7 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): self.session = requests.Session() self.inverters = dict() + for inverterconfig in config.get('inverters', []): serial = inverterconfig.get('serial') output = VzInverterOutput(inverterconfig, self.session) @@ -320,6 +334,8 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): :raises ValueError: when response is not instance of StatusResponse """ + + # check decoder object for output if not isinstance(response, StatusResponse): raise ValueError('Data needs to be instance of StatusResponse') From 96d64faf627582376fa3e144de79789095f8ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Wed, 25 Jan 2023 11:49:13 +0100 Subject: [PATCH 3/5] RPi: send value irradiation to VZ and MQTT to send all meassured and calculated values to Volkszaehler and to mqtt change some logging levels for better differentiation change frequency as part of AC phase --- tools/rpi/ahoy.yml.example | 28 +++++- tools/rpi/hoymiles/__init__.py | 2 +- tools/rpi/hoymiles/__main__.py | 6 +- tools/rpi/hoymiles/decoders/__init__.py | 36 ++++--- tools/rpi/hoymiles/outputs.py | 126 +++++++++++++++--------- 5 files changed, 132 insertions(+), 66 deletions(-) diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 7cb0c3b7..c157e826 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -44,9 +44,7 @@ ahoy: - serial: 114172220003 url: 'http://localhost/middleware/' channels: - - type: 'temperature' - uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' - - type: 'frequency' + - type: 'ac_frequency0' uid: '' - type: 'ac_power0' uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' @@ -54,18 +52,42 @@ ahoy: uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' - type: 'ac_current0' uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' + - type: 'ac_reactive_power0' + uid: '' - type: 'dc_power0' uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' - type: 'dc_voltage0' uid: '' - type: 'dc_current0' uid: '' + - type: 'dc_energy_total0' + uid: '' + - type: 'dc_energy_daily0' + uid: 'c2a93ea0-9a4e-11ed-8000-7d82e3ac8959' + - type: 'dc_irradiation0' + uid: 'c2d887a0-9a4e-11ed-a7ac-0dab944fd82d' - type: 'dc_power1' uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' - type: 'dc_voltage1' uid: '' - type: 'dc_current1' uid: '' + - type: 'dc_energy_total1' + uid: '' + - type: 'dc_energy_daily1' + uid: 'c3c04df0-9a4e-11ed-82c6-a15a9aba54a3' + - type: 'dc_irradiation1' + uid: 'c3f3efd0-9a4e-11ed-9a77-3fd3187e6237' + - type: 'temperature' + uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' + - type: 'powerfactor' + uid: '' + - type: 'yield_total' + uid: '' + - type: 'yield_today' + uid: 'c4a76dd0-9a4e-11ed-b79f-2de013d39150' + - type: 'efficiency' + uid: 'c4d8e9c0-9a4e-11ed-9d9e-9737749e4b45' dtu: serial: 99978563001 diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 4b46a95e..74ec9fd8 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -159,7 +159,7 @@ class ResponseDecoder(ResponseDecoderFactory): command = self.request_command c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") - logging.debug(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') + logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') model_decoders = __import__('hoymiles.decoders') if hasattr(model_decoders, f'{model}Decode{command.upper()}'): diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index c5451fb0..1dfc8321 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -17,7 +17,7 @@ from suntimes import SunTimes import argparse import yaml from yaml.loader import SafeLoader -import paho.mqtt.client +# import paho.mqtt.client import hoymiles import logging @@ -174,7 +174,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): # get decoder object result = decoder.decode() if hoymiles.HOYMILES_DEBUG_LOGGING: - logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') + logging.info(f'{c_datetime} Decoded: {result.__dict__()}') # check decoder object for output if isinstance(result, hoymiles.decoders.StatusResponse): @@ -284,7 +284,7 @@ if __name__ == '__main__': logging.error("Could not load config file. Try --help") sys.exit(2) except yaml.YAMLError as e_yaml: - logging.error('Failed to load config file {global_config.config_file}: {e_yaml}') + logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}') sys.exit(1) # read AHOY configuration file and prepare logging diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 040f67ef..b46dbe48 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -93,7 +93,8 @@ class Response: class StatusResponse(Response): """Inverter StatusResponse object""" - e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power', 'irradiation'] + phase_keys = ['voltage','current','power','reactive_power','frequency'] + string_keys = ['voltage','current','power','energy_total','energy_daily', 'irradiation'] temperature = None frequency = None powerfactor = None @@ -125,7 +126,7 @@ class StatusResponse(Response): p_exists = False phase_id = len(phases) phase = {} - for key in self.e_keys: + for key in self.phase_keys: prop = f'ac_{key}_{phase_id}' if hasattr(self, prop): p_exists = True @@ -149,7 +150,7 @@ class StatusResponse(Response): s_exists = False string_id = len(strings) string = {} - for key in self.e_keys: + for key in self.string_keys: prop = f'dc_{key}_{string_id}' if hasattr(self, prop): s_exists = True @@ -170,14 +171,27 @@ class StatusResponse(Response): data['phases'] = self.phases data['strings'] = self.strings data['temperature'] = self.temperature - data['frequency'] = self.frequency data['powerfactor'] = self.powerfactor - data['event_count'] = self.event_count - data['time'] = self.time_rx - data['energy_total'] = 0.0 + data['yield_total'] = 0.0 + data['yield_today'] = 0.0 + for string in data['strings']: + data['yield_total'] += string['energy_total'] + data['yield_today'] += string['energy_daily'] + + ac_sum_power = 0.0 + for phase in data['phases']: + ac_sum_power += phase['power'] + dc_sum_power = 0.0 for string in data['strings']: - data['energy_total'] += string['energy_total'] + dc_sum_power += string['power'] + if dc_sum_power != 0: + data['efficiency'] = round(ac_sum_power * 100 / dc_sum_power, 2) + else: + data['efficiency'] = 0.0 + + data['event_count'] = self.event_count + data['time'] = self.time_rx return data @@ -469,7 +483,7 @@ class Hm300Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 18)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 16)[0]/100 @property @@ -568,7 +582,7 @@ class Hm600Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 30)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 28)[0]/100 @property @@ -729,7 +743,7 @@ class Hm1200Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 50)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 48)[0]/100 @property diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 64763c37..18af494a 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -10,11 +10,6 @@ import logging from datetime import datetime, timezone from hoymiles.decoders import StatusResponse, HardwareInfoResponse -try: - from influxdb_client import InfluxDBClient -except ModuleNotFoundError: - pass - class OutputPluginFactory: def __init__(self, **params): """ @@ -59,10 +54,19 @@ class InfluxOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + from influxdb_client import InfluxDBClient + except ModuleNotFoundError: + ErrorText1 = f'Module "influxdb_client" for INFLUXDB necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install influxdb_client' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit() + self._bucket = params.get('bucket', 'hoymiles/autogen') self._org = params.get('org', '') - self._measurement = params.get('measurement', - f'inverter,host={socket.gethostname()}') + self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}') client = InfluxDBClient(url, token, bucket=self._bucket) self.api = client.write_api() @@ -105,6 +109,7 @@ class InfluxOutputPlugin(OutputPluginFactory): data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=Q_AC value={phase["reactive_power"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=frequency value={phase["frequency"]:.3f} {ctime}') phase_id = phase_id + 1 # DC Data @@ -115,28 +120,23 @@ class InfluxOutputPlugin(OutputPluginFactory): data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=YieldDay value={string["energy_daily"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=YieldTotal value={string["energy_total"]/1000:.4f} {ctime}') - if 'irradiation' in string: - data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}') string_id = string_id + 1 # Global if data['event_count'] is not None: data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}') if data['powerfactor'] is not None: - data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}') - data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') - + data_stack.append(f'{measurement},type=PF_AC value={data["powerfactor"]:f} {ctime}') data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}') - if data['energy_total'] is not None: - data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}') + if data['yield_total'] is not None: + data_stack.append(f'{measurement},type=YieldTotal value={data["yield_total"]/1000:.3f} {ctime}') + if data['yield_today'] is not None: + data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}') + data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}') self.api.write(self._bucket, self._org, data_stack) -try: - import paho.mqtt.client -except ModuleNotFoundError: - pass - class MqttOutputPlugin(OutputPluginFactory): """ Mqtt output plugin """ client = None @@ -164,6 +164,16 @@ class MqttOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + import paho.mqtt.client + except ModuleNotFoundError: + ErrorText1 = f'Module "paho.mqtt.client" for MQTT-output necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install paho-mqtt' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit() + mqtt_client = paho.mqtt.client.Client() if config.get('useTLS',False): mqtt_client.tls_set() @@ -196,33 +206,41 @@ class MqttOutputPlugin(OutputPluginFactory): # AC Data phase_id = 0 + phase_sum_power = 0 for phase in data['phases']: - self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) 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']) phase_id = phase_id + 1 + phase_sum_power += phase['power'] # DC Data 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) - if 'irradiation' in string: - self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) + self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) 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']) if data['powerfactor'] is not None: - self.client.publish(f'{topic}/pf', data['powerfactor']) - self.client.publish(f'{topic}/frequency', data['frequency']) - + self.client.publish(f'{topic}/PF_AC', data['powerfactor']) self.client.publish(f'{topic}/Temp', data['temperature']) - if data['energy_total'] is not None: - self.client.publish(f'{topic}/total', data['energy_total']/1000) + if data['yield_total'] is not None: + self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000) + 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']) + elif isinstance(response, HardwareInfoResponse): self.client.publish(f'{topic}/Firmware/Version',\ @@ -236,12 +254,6 @@ class MqttOutputPlugin(OutputPluginFactory): else: raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse') -try: - import requests - import time -except ModuleNotFoundError: - pass - class VzInverterOutput: def __init__(self, config, session): self.session = session @@ -274,7 +286,8 @@ class VzInverterOutput: self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) self.try_publish(ts, f'ac_current{phase_id}', phase['current']) self.try_publish(ts, f'ac_power{phase_id}', phase['power']) - self.try_publish(ts, f'ac_Q{phase_id}', phase['reactive_power']) + self.try_publish(ts, f'ac_reactive_power{phase_id}', phase['reactive_power']) + self.try_publish(ts, f'ac_frequency{phase_id}', phase['frequency']) phase_id = phase_id + 1 # DC Data @@ -283,33 +296,39 @@ class VzInverterOutput: self.try_publish(ts, f'dc_voltage{string_id}', string['voltage']) self.try_publish(ts, f'dc_current{string_id}', string['current']) self.try_publish(ts, f'dc_power{string_id}', string['power']) - self.try_publish(ts, f'dc_YieldDay{string_id}', string['energy_daily']) - self.try_publish(ts, f'dc_YieldTotal{string_id}', string['energy_total']) - if 'irradiation' in string: - self.try_publish(ts, f'dc_Irradiation{string_id}', string['irradiation']) + self.try_publish(ts, f'dc_energy_daily{string_id}', string['energy_daily']) + self.try_publish(ts, f'dc_energy_total{string_id}', string['energy_total']) + self.try_publish(ts, f'dc_irradiation{string_id}', string['irradiation']) string_id = string_id + 1 # Global + if data['event_count'] is not None: + self.try_publish(ts, f'event_count', data['event_count']) if data['powerfactor'] is not None: self.try_publish(ts, f'powerfactor', data['powerfactor']) - self.try_publish(ts, f'frequency', data['frequency']) - - self.try_publish(ts, f'Temp', data['temperature']) - if data['energy_total'] is not None: - self.try_publish(ts, f'total', data['energy_total']) - + self.try_publish(ts, f'temperature', data['temperature']) + if data['yield_total'] is not None: + self.try_publish(ts, f'yield_total', data['yield_total']) + if data['yield_today'] is not None: + self.try_publish(ts, f'yield_today', data['yield_today']) + self.try_publish(ts, f'efficiency', data['efficiency']) def try_publish(self, ts, ctype, value): if not ctype in self.channels: + logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml') return uid = self.channels[ctype] url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}' try: r = self.session.get(url) - if r.status_code != 200: - raise ValueError('Could not send request (%s)' % url) - except requests.exceptions.ConnectionError as e: - raise ValueError('Could not send request (%s)' % e) + if r.status_code == 404: + logging.critical('VZ-DB not reachable, please check "middleware"') + if r.status_code == 400: + logging.critical('UUID not configured in VZ-DB') + elif r.status_code != 200: + raise ValueError(f'Transmit result {url}') + except ConnectionError as e: + raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}') class VolkszaehlerOutputPlugin(OutputPluginFactory): def __init__(self, config, **params): @@ -318,6 +337,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + import requests + import time + except ModuleNotFoundError: + ErrorText1 = f'Module "requests" and "time" for VolkszaehlerOutputPlugin necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install requests' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit(1) + self.session = requests.Session() self.inverters = dict() 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 4/5] 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 5/5] 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()