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')