|  |  | @ -7,29 +7,40 @@ Hoymiles micro-inverters main application | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | import sys | 
			
		
	
		
			
				
					|  |  |  | import struct | 
			
		
	
		
			
				
					|  |  |  | from enum import IntEnum | 
			
		
	
		
			
				
					|  |  |  | import re | 
			
		
	
		
			
				
					|  |  |  | import time | 
			
		
	
		
			
				
					|  |  |  | import traceback | 
			
		
	
		
			
				
					|  |  |  | from datetime import datetime | 
			
		
	
		
			
				
					|  |  |  | from datetime import timedelta | 
			
		
	
		
			
				
					|  |  |  | from suntimes import SunTimes | 
			
		
	
		
			
				
					|  |  |  | import re | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | import argparse | 
			
		
	
		
			
				
					|  |  |  | import yaml | 
			
		
	
		
			
				
					|  |  |  | from yaml.loader import SafeLoader | 
			
		
	
		
			
				
					|  |  |  | import hoymiles | 
			
		
	
		
			
				
					|  |  |  | import logging | 
			
		
	
		
			
				
					|  |  |  | from logging.handlers import RotatingFileHandler | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | import time | 
			
		
	
		
			
				
					|  |  |  | from suntimes import SunTimes | 
			
		
	
		
			
				
					|  |  |  | from datetime import datetime, timedelta | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | import hoymiles  # import paket on this place, call once: "hoymiles/__init__.py" | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | ################################################################################ | 
			
		
	
		
			
				
					|  |  |  | """ Signal Handler """ | 
			
		
	
		
			
				
					|  |  |  | # SIGINT  = Interrupt from keyboard (CTRL + C) | 
			
		
	
		
			
				
					|  |  |  | # SIGTERM = Signal Handler from terminating processes | 
			
		
	
		
			
				
					|  |  |  | # SIGHUP  = Hangup detected on controlling terminal or death of controlling process | 
			
		
	
		
			
				
					|  |  |  | # SIGKILL = Signal Handler SIGKILL and SIGSTOP cannot be caught, blocked, or ignored!! | 
			
		
	
		
			
				
					|  |  |  | ################################################################################ | 
			
		
	
		
			
				
					|  |  |  | # from signal import signal, Signals, SIGINT, SIGTERM, SIGKILL, SIGHUP | 
			
		
	
		
			
				
					|  |  |  | from signal import * | 
			
		
	
		
			
				
					|  |  |  | from signal import signal, Signals, SIGINT, SIGTERM, SIGHUP | 
			
		
	
		
			
				
					|  |  |  | from os import environ | 
			
		
	
		
			
				
					|  |  |  | def signal_handler(sig_num, frame): | 
			
		
	
		
			
				
					|  |  |  |     """ Signal Handler  | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     param: signal number [signal-name] | 
			
		
	
		
			
				
					|  |  |  |     param: 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")}') | 
			
		
	
		
			
				
					|  |  |  |     logging.info(f'Stop by Signal <{signame}> ({sig_num})') | 
			
		
	
		
			
				
					|  |  |  |     if environ.get('TERM') is not None: | 
			
		
	
		
			
				
					|  |  |  |        print (f'\nStop by Signal <{signame}> ({sig_num}) ' | 
			
		
	
		
			
				
					|  |  |  |             f'at: {time.strftime("%d.%m.%Y %H:%M:%S")}\n') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if mqtt_client: | 
			
		
	
		
			
				
					|  |  |  |        mqtt_client.disco() | 
			
		
	
	
		
			
				
					|  |  | @ -42,34 +53,22 @@ def signal_handler(sig_num, frame): | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     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!! | 
			
		
	
		
			
				
					|  |  |  | """ activate signal handler """ | 
			
		
	
		
			
				
					|  |  |  | signal(SIGINT,  signal_handler) | 
			
		
	
		
			
				
					|  |  |  | signal(SIGTERM, signal_handler) | 
			
		
	
		
			
				
					|  |  |  | signal(SIGHUP,  signal_handler) | 
			
		
	
		
			
				
					|  |  |  | # signal(SIGKILL, signal_handler) # not used | 
			
		
	
		
			
				
					|  |  |  | ################################################################################ | 
			
		
	
		
			
				
					|  |  |  | ################################################################################ | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | class InfoCommands(IntEnum): | 
			
		
	
		
			
				
					|  |  |  |     InverterDevInform_Simple = 0  # 0x00 | 
			
		
	
		
			
				
					|  |  |  |     InverterDevInform_All = 1     # 0x01 | 
			
		
	
		
			
				
					|  |  |  |     GridOnProFilePara = 2         # 0x02 | 
			
		
	
		
			
				
					|  |  |  |     HardWareConfig = 3            # 0x03 | 
			
		
	
		
			
				
					|  |  |  |     SimpleCalibrationPara = 4     # 0x04 | 
			
		
	
		
			
				
					|  |  |  |     SystemConfigPara = 5          # 0x05 | 
			
		
	
		
			
				
					|  |  |  |     RealTimeRunData_Debug = 11    # 0x0b | 
			
		
	
		
			
				
					|  |  |  |     RealTimeRunData_Reality = 12  # 0x0c | 
			
		
	
		
			
				
					|  |  |  |     RealTimeRunData_A_Phase = 13  # 0x0d | 
			
		
	
		
			
				
					|  |  |  |     RealTimeRunData_B_Phase = 14  # 0x0e | 
			
		
	
		
			
				
					|  |  |  |     RealTimeRunData_C_Phase = 15  # 0x0f | 
			
		
	
		
			
				
					|  |  |  |     AlarmData = 17                # 0x11, Alarm data - all unsent alarms | 
			
		
	
		
			
				
					|  |  |  |     AlarmUpdate = 18              # 0x12, Alarm data - all pending alarms | 
			
		
	
		
			
				
					|  |  |  |     RecordData = 19               # 0x13 | 
			
		
	
		
			
				
					|  |  |  |     InternalData = 20             # 0x14 | 
			
		
	
		
			
				
					|  |  |  |     GetLossRate = 21              # 0x15 | 
			
		
	
		
			
				
					|  |  |  |     GetSelfCheckState = 30        # 0x1e | 
			
		
	
		
			
				
					|  |  |  |     InitDataState = 0xff | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | class SunsetHandler: | 
			
		
	
		
			
				
					|  |  |  |     """ Sunset class | 
			
		
	
		
			
				
					|  |  |  |     to recognize the times of sunrise, sunset and to sleep at night time | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     :param str inverter: inverter serial | 
			
		
	
		
			
				
					|  |  |  |     :param retries: tx retry count if no inverter contact | 
			
		
	
		
			
				
					|  |  |  |     :type retries: int | 
			
		
	
		
			
				
					|  |  |  |     """ | 
			
		
	
		
			
				
					|  |  |  |     def __init__(self, sunset_config): | 
			
		
	
		
			
				
					|  |  |  |         self.suntimes = None | 
			
		
	
		
			
				
					|  |  |  |         if sunset_config and sunset_config.get('disabled', True) == False: | 
			
		
	
	
		
			
				
					|  |  | @ -78,9 +77,11 @@ class SunsetHandler: | 
			
		
	
		
			
				
					|  |  |  |             altitude = sunset_config.get('altitude') | 
			
		
	
		
			
				
					|  |  |  |             self.suntimes = SunTimes(longitude=longitude, latitude=latitude, altitude=altitude) | 
			
		
	
		
			
				
					|  |  |  |             self.nextSunset = self.suntimes.setutc(datetime.utcnow()) | 
			
		
	
		
			
				
					|  |  |  |             logging.info (f'Todays sunset is at {self.nextSunset} UTC') | 
			
		
	
		
			
				
					|  |  |  |             logging.info (f'Sunset today at: {self.nextSunset} UTC') | 
			
		
	
		
			
				
					|  |  |  |             # send info to mqtt, if broker configured | 
			
		
	
		
			
				
					|  |  |  |             self.sun_status2mqtt() | 
			
		
	
		
			
				
					|  |  |  |         else: | 
			
		
	
		
			
				
					|  |  |  |             logging.info('Sunset disabled.') | 
			
		
	
		
			
				
					|  |  |  |             logging.info('Sunset disabled!') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     def checkWaitForSunrise(self): | 
			
		
	
		
			
				
					|  |  |  |         if not self.suntimes: | 
			
		
	
	
		
			
				
					|  |  | @ -100,7 +101,8 @@ class SunsetHandler: | 
			
		
	
		
			
				
					|  |  |  |                time.sleep(time_to_sleep) | 
			
		
	
		
			
				
					|  |  |  |                logging.info (f'Woke up...') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     def sun_status2mqtt(self, dtu_ser, dtu_name): | 
			
		
	
		
			
				
					|  |  |  |     def sun_status2mqtt(self): | 
			
		
	
		
			
				
					|  |  |  |         """ send sunset information every day to MQTT broker """ | 
			
		
	
		
			
				
					|  |  |  |         if not mqtt_client or not self.suntimes: | 
			
		
	
		
			
				
					|  |  |  |             return | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  | @ -108,64 +110,76 @@ class SunsetHandler: | 
			
		
	
		
			
				
					|  |  |  |             local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M") | 
			
		
	
		
			
				
					|  |  |  |             local_sunset  = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M") | 
			
		
	
		
			
				
					|  |  |  |             local_zone    = self.suntimes.setlocal(datetime.now()).tzinfo.key | 
			
		
	
		
			
				
					|  |  |  |             mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \ | 
			
		
	
		
			
				
					|  |  |  |                          {'dis_night_comm' : 'True', \ | 
			
		
	
		
			
				
					|  |  |  |                            'local_sunrise' : local_sunrise, \ | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             mqtt_client.info2mqtt(f'{dtu_name}/{dtu_serial}',  | 
			
		
	
		
			
				
					|  |  |  |                 {'dis_night_comm' : 'True',  | 
			
		
	
		
			
				
					|  |  |  |                   'local_sunrise' : local_sunrise,  | 
			
		
	
		
			
				
					|  |  |  |                    'local_sunset' : local_sunset, | 
			
		
	
		
			
				
					|  |  |  |                      'local_zone' : local_zone}) | 
			
		
	
		
			
				
					|  |  |  |         else: | 
			
		
	
		
			
				
					|  |  |  |             mqtt_client.sun_info2mqtt({'sun_topic': f'{dtu_name}/{dtu_ser}'}, \ | 
			
		
	
		
			
				
					|  |  |  |                                  {'dis_night_comm': 'False'}) | 
			
		
	
		
			
				
					|  |  |  |             mqtt_client.info2mqtt(f'{dtu_name}/{dtu_serial}', {'dis_night_comm': 'False'}) | 
			
		
	
		
			
				
					|  |  |  |    | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | def main_loop(ahoy_config): | 
			
		
	
		
			
				
					|  |  |  |     """Main loop""" | 
			
		
	
		
			
				
					|  |  |  |     inverters = [ | 
			
		
	
		
			
				
					|  |  |  |             inverter for inverter in ahoy_config.get('inverters', []) | 
			
		
	
		
			
				
					|  |  |  |             if not inverter.get('disabled', False)] | 
			
		
	
		
			
				
					|  |  |  |     """ Main loop """ | 
			
		
	
		
			
				
					|  |  |  |     # check 'interval' parameter in config-file | 
			
		
	
		
			
				
					|  |  |  |     loop_interval = ahoy_config.get('interval', 15) | 
			
		
	
		
			
				
					|  |  |  |     logging.info(f"AHOY-MAIN: loop interval : {loop_interval} sec.") | 
			
		
	
		
			
				
					|  |  |  |     if (loop_interval <= 0): | 
			
		
	
		
			
				
					|  |  |  |         logging.critical("Parameter 'loop_interval' must grater 0 - please check ahoy.yml.") | 
			
		
	
		
			
				
					|  |  |  |         # print console message too | 
			
		
	
		
			
				
					|  |  |  |         print("Parameter 'loop_interval' must be >0 - please check ahoy.yml - STOP(0)") | 
			
		
	
		
			
				
					|  |  |  |         sys.exit(0) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     sunset = SunsetHandler(ahoy_config.get('sunset')) | 
			
		
	
		
			
				
					|  |  |  |     dtu_ser = ahoy_config.get('dtu', {}).get('serial', None) | 
			
		
	
		
			
				
					|  |  |  |     dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') | 
			
		
	
		
			
				
					|  |  |  |     sunset.sun_status2mqtt(dtu_ser, dtu_name) | 
			
		
	
		
			
				
					|  |  |  |     loop_interval = ahoy_config.get('interval', 1) | 
			
		
	
		
			
				
					|  |  |  |     # check 'transmit_retries' parameter in config-file | 
			
		
	
		
			
				
					|  |  |  |     transmit_retries = ahoy_config.get('transmit_retries', 5) | 
			
		
	
		
			
				
					|  |  |  |     if (transmit_retries <= 0): | 
			
		
	
		
			
				
					|  |  |  |         logging.critical('Parameter "transmit_retries" must be >0 - please check ahoy.yml.') | 
			
		
	
		
			
				
					|  |  |  |         # print message to console too | 
			
		
	
		
			
				
					|  |  |  |         print('Parameter "transmit_retries" must be >0 - please check ahoy.yml - STOP(0)x') | 
			
		
	
		
			
				
					|  |  |  |         logging.critical("Parameter 'transmit_retries' must grater 0 - please check ahoy.yml.") | 
			
		
	
		
			
				
					|  |  |  |         # print console message too | 
			
		
	
		
			
				
					|  |  |  |         print("Parameter 'transmit_retries' must be >0 - please check ahoy.yml - STOP(0)") | 
			
		
	
		
			
				
					|  |  |  |         sys.exit(0) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     try: | 
			
		
	
		
			
				
					|  |  |  |         do_init = True | 
			
		
	
		
			
				
					|  |  |  |         while True: | 
			
		
	
		
			
				
					|  |  |  |             sunset.checkWaitForSunrise() | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             t_loop_start = time.time() | 
			
		
	
		
			
				
					|  |  |  |     # get parameter from config-file | 
			
		
	
		
			
				
					|  |  |  |     inverters = [inverter for inverter in ahoy_config.get('inverters', []) | 
			
		
	
		
			
				
					|  |  |  |                  if not inverter.get('disabled', False)] | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # check all inverter names and serial numbers in config-file | 
			
		
	
		
			
				
					|  |  |  |     for inverter in inverters: | 
			
		
	
		
			
				
					|  |  |  |         if not 'name' in inverter: | 
			
		
	
		
			
				
					|  |  |  |            inverter['name'] = 'hoymiles' | 
			
		
	
		
			
				
					|  |  |  |         if not 'serial' in inverter: | 
			
		
	
		
			
				
					|  |  |  |            logging.error("No inverter serial number found in ahoy.yml - exit") | 
			
		
	
		
			
				
					|  |  |  |            sys.exit(999) | 
			
		
	
		
			
				
					|  |  |  |                 if hoymiles.HOYMILES_DEBUG_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                     logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') | 
			
		
	
		
			
				
					|  |  |  |                 poll_inverter(inverter, dtu_ser, do_init, transmit_retries) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # init Sunset-Handler object | 
			
		
	
		
			
				
					|  |  |  |     sunset = SunsetHandler(ahoy_config.get('sunset')) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if not hoymiles.HOYMILES_VERBOSE_LOGGING and not hoymiles.HOYMILES_TRANSACTION_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |        logging.info(f"MAIN LOOP starts now without any output") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     try: | 
			
		
	
		
			
				
					|  |  |  |         do_init = True | 
			
		
	
		
			
				
					|  |  |  |         while True:   # MAIN endless LOOP | 
			
		
	
		
			
				
					|  |  |  |             # check sunrise and sunset times and sleep in night time | 
			
		
	
		
			
				
					|  |  |  |             sunset.checkWaitForSunrise() | 
			
		
	
		
			
				
					|  |  |  |             t_loop_start = time.time() | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             for inverter in inverters: | 
			
		
	
		
			
				
					|  |  |  |                 poll_inverter(inverter, do_init, transmit_retries) | 
			
		
	
		
			
				
					|  |  |  |             do_init = False | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             if loop_interval > 0: | 
			
		
	
		
			
				
					|  |  |  |             # calc time to pause main-loop | 
			
		
	
		
			
				
					|  |  |  |             time_to_sleep = loop_interval - (time.time() - t_loop_start) | 
			
		
	
		
			
				
					|  |  |  |             if time_to_sleep > 0: | 
			
		
	
		
			
				
					|  |  |  |                if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                   logging.info(f'MAIN-LOOP: sleep for {time_to_sleep} sec.') | 
			
		
	
		
			
				
					|  |  |  |                time.sleep(time_to_sleep) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     except Exception as e: | 
			
		
	
		
			
				
					|  |  |  |         logging.fatal('Exception catched: %s' % e) | 
			
		
	
		
			
				
					|  |  |  |         logging.fatal(traceback.print_exc()) | 
			
		
	
		
			
				
					|  |  |  |         raise | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | def poll_inverter(inverter, dtu_ser, do_init, retries): | 
			
		
	
		
			
				
					|  |  |  | def poll_inverter(inverter, do_init, retries): | 
			
		
	
		
			
				
					|  |  |  |     """ | 
			
		
	
		
			
				
					|  |  |  |     Send/Receive command_queue, initiate status poll on inverter | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  | @ -176,17 +190,31 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): | 
			
		
	
		
			
				
					|  |  |  |     inverter_ser     = inverter.get('serial') | 
			
		
	
		
			
				
					|  |  |  |     inverter_name    = inverter.get('name') | 
			
		
	
		
			
				
					|  |  |  |     inverter_strings = inverter.get('strings') | 
			
		
	
		
			
				
					|  |  |  |     inv_str          = str(inverter_ser) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # Queue at least status data request | 
			
		
	
		
			
				
					|  |  |  |     inv_str = str(inverter_ser) | 
			
		
	
		
			
				
					|  |  |  |     if do_init: | 
			
		
	
		
			
				
					|  |  |  |       command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.InverterDevInform_All)) | 
			
		
	
		
			
				
					|  |  |  |       #command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.SystemConfigPara)) | 
			
		
	
		
			
				
					|  |  |  |     command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.RealTimeRunData_Debug)) | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.InverterDevInform_Simple)) # 00 | 
			
		
	
		
			
				
					|  |  |  |       command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.InverterDevInform_All))      # 01 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.GridOnProFilePara))        # 02 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.HardWareConfig))           # 03 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.SimpleCalibrationPara))    # 04 | 
			
		
	
		
			
				
					|  |  |  |       ##command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.SystemConfigPara))         # 05 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.RealTimeRunData_Reality))  # 0c | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.AlarmData))          # 11 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.AlarmUpdate))        # 12 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.RecordData))         # 13 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.InternalData))       # 14 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.GetLossRate))        # 15 | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.GetSelfCheckState))  # 1E | 
			
		
	
		
			
				
					|  |  |  |       # command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.InitDataState))      # FF | 
			
		
	
		
			
				
					|  |  |  |     command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.RealTimeRunData_Debug))  # 0b | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # Put all queued commands for current inverter on air | 
			
		
	
		
			
				
					|  |  |  |     while len(command_queue[inv_str]) > 0: | 
			
		
	
		
			
				
					|  |  |  |         payload = command_queue[inv_str].pop(0)    ## Sub.Cmd | 
			
		
	
		
			
				
					|  |  |  |         if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |            logging.info(f'Poll inverter name={inverter_name} ser={inverter_ser} command={hoymiles.InfoCommands(command_queue[inv_str][0][0]).name}') | 
			
		
	
		
			
				
					|  |  |  |         payload = command_queue[inv_str].pop(0)    ## get first object from command queue | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |         # Send payload {ttl}-times until we get at least one reponse | 
			
		
	
		
			
				
					|  |  |  |         payload_ttl = retries | 
			
		
	
	
		
			
				
					|  |  | @ -196,12 +224,12 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): | 
			
		
	
		
			
				
					|  |  |  |             com = hoymiles.InverterTransaction( | 
			
		
	
		
			
				
					|  |  |  |                     radio=hmradio, | 
			
		
	
		
			
				
					|  |  |  |                     txpower=inverter.get('txpower', None), | 
			
		
	
		
			
				
					|  |  |  |                     dtu_ser=dtu_ser, | 
			
		
	
		
			
				
					|  |  |  |                     dtu_ser=dtu_serial, | 
			
		
	
		
			
				
					|  |  |  |                     inverter_ser=inverter_ser, | 
			
		
	
		
			
				
					|  |  |  |                     request=next(hoymiles.compose_esb_packet( | 
			
		
	
		
			
				
					|  |  |  |                         payload, | 
			
		
	
		
			
				
					|  |  |  |                         seq=b'\x80', | 
			
		
	
		
			
				
					|  |  |  |                         src=dtu_ser, | 
			
		
	
		
			
				
					|  |  |  |                         src=dtu_serial, | 
			
		
	
		
			
				
					|  |  |  |                         dst=inverter_ser | 
			
		
	
		
			
				
					|  |  |  |                         ))) | 
			
		
	
		
			
				
					|  |  |  |             while com.rxtx(): | 
			
		
	
	
		
			
				
					|  |  | @ -213,51 +241,75 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): | 
			
		
	
		
			
				
					|  |  |  |                         logging.error(f'Error while retrieving data: {e_all}') | 
			
		
	
		
			
				
					|  |  |  |                     pass | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |         # Handle the response data if any | 
			
		
	
		
			
				
					|  |  |  |         # Handle response data, if any | 
			
		
	
		
			
				
					|  |  |  |         if response: | 
			
		
	
		
			
				
					|  |  |  |             if hoymiles.HOYMILES_TRANSACTION_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                 logging.debug(f'Payload: ' + hoymiles.hexify_payload(response)) | 
			
		
	
		
			
				
					|  |  |  |                 logging.debug(f'Payload: {len(response)} bytes: {hoymiles.hexify_payload(response)}') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             # prepare decoder object | 
			
		
	
		
			
				
					|  |  |  |             # get a ResponseDecoder object to decode response-payload | 
			
		
	
		
			
				
					|  |  |  |             decoder = hoymiles.ResponseDecoder(response,   | 
			
		
	
		
			
				
					|  |  |  |                     request=com.request, | 
			
		
	
		
			
				
					|  |  |  |                     inverter_ser=inverter_ser, | 
			
		
	
		
			
				
					|  |  |  |                     inverter_name=inverter_name, | 
			
		
	
		
			
				
					|  |  |  |                     dtu_ser=dtu_ser, | 
			
		
	
		
			
				
					|  |  |  |                     strings=inverter_strings | 
			
		
	
		
			
				
					|  |  |  |                     ) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             # get decoder object | 
			
		
	
		
			
				
					|  |  |  |             result = decoder.decode() | 
			
		
	
		
			
				
					|  |  |  |             if hoymiles.HOYMILES_DEBUG_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                logging.info(f'Decoded: {result.__dict__()}') | 
			
		
	
		
			
				
					|  |  |  |             result = decoder.decode()             # call decoder object | 
			
		
	
		
			
				
					|  |  |  |             data = result.__dict__()              # convert result into python-dict | 
			
		
	
		
			
				
					|  |  |  |             if hoymiles.HOYMILES_TRANSACTION_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                logging.debug(f'Decoded: {data}') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             # check decoder object for output | 
			
		
	
		
			
				
					|  |  |  |             # check result object for output | 
			
		
	
		
			
				
					|  |  |  |             if isinstance(result, hoymiles.decoders.StatusResponse): | 
			
		
	
		
			
				
					|  |  |  |                 if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                    logging.info(f"StatusResponse: payload contains {len(data)} elements " | 
			
		
	
		
			
				
					|  |  |  |                                 f"(power={data['phases'][0]['power']} W - event_count={data['event_count']})") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 data = result.__dict__() | 
			
		
	
		
			
				
					|  |  |  |                 # when 'event_count' is changed, add AlarmData-command to queue | 
			
		
	
		
			
				
					|  |  |  |                 if data is not None and 'event_count' in data: | 
			
		
	
		
			
				
					|  |  |  |                     if event_message_index[inv_str] < data['event_count']: | 
			
		
	
		
			
				
					|  |  |  |                     # if event_message_index[inv_str] < data['event_count']: | 
			
		
	
		
			
				
					|  |  |  |                     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 hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                           logging.info(f"event_count changed to {data['event_count']} --> AlarmData requested") | 
			
		
	
		
			
				
					|  |  |  |                        # add AlarmData-command to queue  | 
			
		
	
		
			
				
					|  |  |  |                        command_queue[inv_str].append(hoymiles.compose_send_time_payload(hoymiles.InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 # sent outputs | 
			
		
	
		
			
				
					|  |  |  |                 if mqtt_client: | 
			
		
	
		
			
				
					|  |  |  |                    mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) | 
			
		
	
		
			
				
					|  |  |  |                    mqtt_client.store_status(data) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 if influx_client: | 
			
		
	
		
			
				
					|  |  |  |                    influx_client.store_status(result) | 
			
		
	
		
			
				
					|  |  |  |                    influx_client.store_status(data) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 if volkszaehler_client: | 
			
		
	
		
			
				
					|  |  |  |                    volkszaehler_client.store_status(result) | 
			
		
	
		
			
				
					|  |  |  |                    volkszaehler_client.store_status(data) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             # check decoder object for output | 
			
		
	
		
			
				
					|  |  |  |             # check decoder object for different data types | 
			
		
	
		
			
				
					|  |  |  |             if isinstance(result, hoymiles.decoders.HardwareInfoResponse): | 
			
		
	
		
			
				
					|  |  |  |                if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                   logging.info(f"Firmware version {data['FW_ver_maj']}.{data['FW_ver_min']}.{data['FW_ver_pat']}, " | 
			
		
	
		
			
				
					|  |  |  |                                f"build at {data['FW_build_dd']:>02}/{data['FW_build_mm']:>02}/{data['FW_build_yy']}T" | 
			
		
	
		
			
				
					|  |  |  |                                f"{data['FW_build_HH']:>02}:{data['FW_build_MM']:>02}, " | 
			
		
	
		
			
				
					|  |  |  |                                f"HW revision {data['FW_HW_ID']}") | 
			
		
	
		
			
				
					|  |  |  |                if mqtt_client: | 
			
		
	
		
			
				
					|  |  |  |                    mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) | 
			
		
	
		
			
				
					|  |  |  |                   mqtt_client.store_status(data) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             if isinstance(result, hoymiles.decoders.EventsResponse): | 
			
		
	
		
			
				
					|  |  |  |                if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                   logging.info(f"EventsResponse: {data['inv_stat_txt']} ({data['inv_stat_num']})") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             if isinstance(result, hoymiles.decoders.DebugDecodeAny): | 
			
		
	
		
			
				
					|  |  |  |                if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                   logging.info(f"DebugDecodeAny: payload ({data['len_payload']} bytes): {data['payload']}") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | def mqtt_on_message(mqtt_client, userdata, message): | 
			
		
	
		
			
				
					|  |  |  |     '''  | 
			
		
	
		
			
				
					|  |  |  |     MQTT(PAHO) callcack method to handle receiving payload  | 
			
		
	
		
			
				
					|  |  |  |     ( run in thread: "paho-mqtt-client-" - important for signals and Exceptions !) | 
			
		
	
		
			
				
					|  |  |  |     a)  when receiving topic ends with "SENSOR" for privat electricity meter | 
			
		
	
		
			
				
					|  |  |  |     b)  when receiving topic ends with "command" for runtime faster debugging | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | def mqtt_on_command(client, userdata, message): | 
			
		
	
		
			
				
					|  |  |  |     """ | 
			
		
	
		
			
				
					|  |  |  |     Handle commands to topic | 
			
		
	
		
			
				
					|  |  |  |         hoymiles/{inverter_ser}/command | 
			
		
	
		
			
				
					|  |  |  |     frame a payload and put onto command_queue | 
			
		
	
	
		
			
				
					|  |  | @ -278,35 +330,45 @@ def mqtt_on_command(client, userdata, message): | 
			
		
	
		
			
				
					|  |  |  |     :param paho.mqtt.client.Client client: mqtt-client instance | 
			
		
	
		
			
				
					|  |  |  |     :param dict userdata: Userdata | 
			
		
	
		
			
				
					|  |  |  |     :param dict message: mqtt-client message object | 
			
		
	
		
			
				
					|  |  |  |     """ | 
			
		
	
		
			
				
					|  |  |  |     try: | 
			
		
	
		
			
				
					|  |  |  |         inverter_ser = next( | 
			
		
	
		
			
				
					|  |  |  |                 item[0] for item in mqtt_command_topic_subs if item[1] == message.topic) | 
			
		
	
		
			
				
					|  |  |  |     except StopIteration: | 
			
		
	
		
			
				
					|  |  |  |         logging.warning('Unexpedtedly received mqtt message for {message.topic}') | 
			
		
	
		
			
				
					|  |  |  |     ''' | 
			
		
	
		
			
				
					|  |  |  |     # print(f"msg-topic: {message.topic} - QoS: {message.qos}") | 
			
		
	
		
			
				
					|  |  |  |     # print(f"payload:  ",str(message.payload.decode("utf-8")), "\n") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # handle specific payload topic | 
			
		
	
		
			
				
					|  |  |  |     if message.topic.endswith("SENSOR"): | 
			
		
	
		
			
				
					|  |  |  |        if volkszaehler_client: | 
			
		
	
		
			
				
					|  |  |  |           volkszaehler_client.store_status(yaml.safe_load(str(message.payload.decode("utf-8")))) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if inverter_ser: | 
			
		
	
		
			
				
					|  |  |  |     if message.topic.endswith("command"): | 
			
		
	
		
			
				
					|  |  |  |         p_message = message.payload.decode('utf-8').lower() | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |         # Expand tttttttt to current time for use in hexlified payload | 
			
		
	
		
			
				
					|  |  |  |         expand_time = ''.join(f'{b:02x}' for b in struct.pack('>L', int(time.time()))) | 
			
		
	
		
			
				
					|  |  |  |         p_message = p_message.replace('tttttttt', expand_time) | 
			
		
	
		
			
				
					|  |  |  |         logging.info (f"MQTT-command: {message.topic} - {p_message}") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |         if (len(p_message) < 2048 \ | 
			
		
	
		
			
				
					|  |  |  |             and len(p_message) % 2 == 0 \ | 
			
		
	
		
			
				
					|  |  |  |             and re.match(r'^[a-f0-9]+$', p_message)): | 
			
		
	
		
			
				
					|  |  |  |         if (len(p_message) < 2048 and len(p_message) % 2 == 0 and re.match(r'^[a-f0-9]+$', p_message)): | 
			
		
	
		
			
				
					|  |  |  |             payload = bytes.fromhex(p_message) | 
			
		
	
		
			
				
					|  |  |  |             if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |                logging.info (f"MQTT-command for {inv_str}: {payload}") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |             # commands must start with \x80 | 
			
		
	
		
			
				
					|  |  |  |             if payload[0] == 0x80: | 
			
		
	
		
			
				
					|  |  |  |                 command_queue[str(inverter_ser)].append( | 
			
		
	
		
			
				
					|  |  |  |                     hoymiles.frame_payload(payload[1:])) | 
			
		
	
		
			
				
					|  |  |  |                 # array "command_queue[inv_str]" will be shared to an other thread --> critical section | 
			
		
	
		
			
				
					|  |  |  |                 command_queue[inv_str].append(hoymiles.frame_payload(payload[1:])) | 
			
		
	
		
			
				
					|  |  |  |             else: | 
			
		
	
		
			
				
					|  |  |  |                 logging.info (f"MQTT-command: must start with \x80: {payload}") | 
			
		
	
		
			
				
					|  |  |  |         else: | 
			
		
	
		
			
				
					|  |  |  |             logging.info (f"MQTT-command to long (max length: 2048 bytes) - or contains non hex char") | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | def init_logging(ahoy_config): | 
			
		
	
		
			
				
					|  |  |  |     """ init and prepare logging """ | 
			
		
	
		
			
				
					|  |  |  |     log_config = ahoy_config.get('logging') | 
			
		
	
		
			
				
					|  |  |  |     fn = 'hoymiles.log' | 
			
		
	
		
			
				
					|  |  |  |     lvl = logging.ERROR | 
			
		
	
		
			
				
					|  |  |  |     max_log_filesize = 1000000 | 
			
		
	
		
			
				
					|  |  |  |     max_log_files = 1 | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if log_config: | 
			
		
	
		
			
				
					|  |  |  |         fn = log_config.get('filename', fn) | 
			
		
	
		
			
				
					|  |  |  |         level = log_config.get('level', 'ERROR') | 
			
		
	
	
		
			
				
					|  |  | @ -322,15 +384,23 @@ def init_logging(ahoy_config): | 
			
		
	
		
			
				
					|  |  |  |             lvl = logging.FATAL | 
			
		
	
		
			
				
					|  |  |  |         max_log_filesize  = log_config.get('max_log_filesize', max_log_filesize) | 
			
		
	
		
			
				
					|  |  |  |         max_log_files = log_config.get('max_log_files', max_log_files) | 
			
		
	
		
			
				
					|  |  |  |     if hoymiles.HOYMILES_TRANSACTION_LOGGING: | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # define log switches | 
			
		
	
		
			
				
					|  |  |  |     if global_config.log_transactions: | 
			
		
	
		
			
				
					|  |  |  |        hoymiles.HOYMILES_TRANSACTION_LOGGING = True | 
			
		
	
		
			
				
					|  |  |  |        lvl = logging.DEBUG | 
			
		
	
		
			
				
					|  |  |  |     if global_config.verbose: | 
			
		
	
		
			
				
					|  |  |  |        hoymiles.HOYMILES_VERBOSE_LOGGING     = True | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # start configured logging | 
			
		
	
		
			
				
					|  |  |  |     logging.basicConfig(handlers=[RotatingFileHandler(fn, maxBytes=max_log_filesize, backupCount=max_log_files)],  | 
			
		
	
		
			
				
					|  |  |  |         format='%(asctime)s %(levelname)s: %(message)s',  | 
			
		
	
		
			
				
					|  |  |  |         datefmt='%Y-%m-%d %H:%M:%S.%s', level=lvl) | 
			
		
	
		
			
				
					|  |  |  |     dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu') | 
			
		
	
		
			
				
					|  |  |  |     logging.info(f'start logging for {dtu_name} with level: {logging.getLevelName(logging.root.level)}') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     logging.info(f'AHOY-logging started for "{dtu_name}" with level: {logging.getLevelName(logging.root.level)}') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | if __name__ == '__main__': | 
			
		
	
		
			
				
					|  |  |  |     # read commandline parameter | 
			
		
	
		
			
				
					|  |  |  |     parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles") | 
			
		
	
		
			
				
					|  |  |  |     parser.add_argument("-c", "--config-file", nargs="?", required=True, | 
			
		
	
		
			
				
					|  |  |  |         help="configuration file") | 
			
		
	
	
		
			
				
					|  |  | @ -340,7 +410,7 @@ if __name__ == '__main__': | 
			
		
	
		
			
				
					|  |  |  |         help="Enable detailed debug output (loglevel must be DEBUG)") | 
			
		
	
		
			
				
					|  |  |  |     global_config = parser.parse_args() | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # Load ahoy.yml config file | 
			
		
	
		
			
				
					|  |  |  |     # Load config file given in commandline parameter | 
			
		
	
		
			
				
					|  |  |  |     try: | 
			
		
	
		
			
				
					|  |  |  |         if isinstance(global_config.config_file, str): | 
			
		
	
		
			
				
					|  |  |  |             with open(global_config.config_file, 'r') as fh_yaml: | 
			
		
	
	
		
			
				
					|  |  | @ -355,27 +425,44 @@ if __name__ == '__main__': | 
			
		
	
		
			
				
					|  |  |  |         logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}') | 
			
		
	
		
			
				
					|  |  |  |         sys.exit(1) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if global_config.log_transactions: | 
			
		
	
		
			
				
					|  |  |  |         hoymiles.HOYMILES_TRANSACTION_LOGGING=True | 
			
		
	
		
			
				
					|  |  |  |     if global_config.verbose: | 
			
		
	
		
			
				
					|  |  |  |         hoymiles.HOYMILES_DEBUG_LOGGING=True | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # read AHOY configuration file and prepare logging | 
			
		
	
		
			
				
					|  |  |  |     # read all parameter from configuration file as 'ahoy_config' | 
			
		
	
		
			
				
					|  |  |  |     ahoy_config = dict(cfg.get('ahoy', {})) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # extract 'DTU' parameter | 
			
		
	
		
			
				
					|  |  |  |     dtu_serial  = ahoy_config.get('dtu', {}).get('serial', None) | 
			
		
	
		
			
				
					|  |  |  |     dtu_name    = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # init and prepare logging | 
			
		
	
		
			
				
					|  |  |  |     init_logging(ahoy_config) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # Prepare for multiple transceivers, makes them configurable | 
			
		
	
		
			
				
					|  |  |  |     # Prepare for multiple transceivers (radio modules), 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', None) | 
			
		
	
		
			
				
					|  |  |  |     if mqtt_config and not mqtt_config.get('disabled', False): | 
			
		
	
		
			
				
					|  |  |  |     # create MQTT client object | 
			
		
	
		
			
				
					|  |  |  |       # if: mqtt-disabled is "true" - only | 
			
		
	
		
			
				
					|  |  |  |       # if: mqtt-disabled is "true" AND inverter-mqtt-send_raw_enabled is "true"  | 
			
		
	
		
			
				
					|  |  |  |       # if: mqtt topic is defined - only or with other functions | 
			
		
	
		
			
				
					|  |  |  |     mqtt_c_obj  = mqtt_client = None                 # create client-obj-placeholder | 
			
		
	
		
			
				
					|  |  |  |     mqtt_config = ahoy_config.get('mqtt', None)      # get mqtt-config, if available | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if mqtt_config and (not mqtt_config.get('disabled', False) or mqtt_topic): | 
			
		
	
		
			
				
					|  |  |  |        from .outputs import MqttOutputPlugin | 
			
		
	
		
			
				
					|  |  |  |        mqtt_client = MqttOutputPlugin(mqtt_config) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # create INFLUX - client object | 
			
		
	
		
			
				
					|  |  |  |        # MQTT_TOPIC array should contain QOS levels as well as topic names. | 
			
		
	
		
			
				
					|  |  |  |        # MQTT_TOPIC = [("Server1/kpi1",0),("Server2/kpi2",0),("Server3/kpi3",0)] | 
			
		
	
		
			
				
					|  |  |  |        mqtt_topic  = mqtt_config.get('topic', None)     # get topic, if available | 
			
		
	
		
			
				
					|  |  |  |        mqtt_topic_array = []                            # create empty array | 
			
		
	
		
			
				
					|  |  |  |        if mqtt_topic: | 
			
		
	
		
			
				
					|  |  |  |           mqtt_topic_array.append((mqtt_topic, mqtt_config.get('QoS',0))) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |        # create MQTT(PAHO) client object with own callback funtion | 
			
		
	
		
			
				
					|  |  |  |        mqtt_c_obj = MqttOutputPlugin(mqtt_config, mqtt_on_message) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |        if mqtt_c_obj and not mqtt_config.get('disabled', False): | 
			
		
	
		
			
				
					|  |  |  |           mqtt_client = mqtt_c_obj | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # create INFLUX client object | 
			
		
	
		
			
				
					|  |  |  |     influx_client = None | 
			
		
	
		
			
				
					|  |  |  |     influx_config = ahoy_config.get('influxdb', None) | 
			
		
	
		
			
				
					|  |  |  |     if influx_config and not influx_config.get('disabled', False): | 
			
		
	
	
		
			
				
					|  |  | @ -387,31 +474,35 @@ if __name__ == '__main__': | 
			
		
	
		
			
				
					|  |  |  |                 bucket=influx_config.get('bucket', None), | 
			
		
	
		
			
				
					|  |  |  |                 measurement=influx_config.get('measurement', 'hoymiles')) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # create VOLKSZAEHLER - client object | 
			
		
	
		
			
				
					|  |  |  |     # 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) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # init important runtime variables | 
			
		
	
		
			
				
					|  |  |  |     event_message_index = {} | 
			
		
	
		
			
				
					|  |  |  |     command_queue = {} | 
			
		
	
		
			
				
					|  |  |  |     mqtt_command_topic_subs = [] | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     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), | 
			
		
	
		
			
				
					|  |  |  |                     g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command' | 
			
		
	
		
			
				
					|  |  |  |                     ) | 
			
		
	
		
			
				
					|  |  |  |             mqtt_client.client.subscribe(topic_item[1]) | 
			
		
	
		
			
				
					|  |  |  |             mqtt_command_topic_subs.append(topic_item) | 
			
		
	
		
			
				
					|  |  |  |     for g_inverter in ahoy_config.get('inverters', []): # loop inverters in ahoy_config | 
			
		
	
		
			
				
					|  |  |  |         inv_str = str(g_inverter.get('serial'))         # inverter serial number as index | 
			
		
	
		
			
				
					|  |  |  |         command_queue[inv_str] = []                     # create empty command-queue | 
			
		
	
		
			
				
					|  |  |  |         event_message_index[inv_str] = 0                # init event-queue with value=0 | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |         # if send_raw_enabled, add topic to subscribe command-queue | 
			
		
	
		
			
				
					|  |  |  |         if mqtt_c_obj and g_inverter.get('mqtt', {}).get('send_raw_enabled', False): | 
			
		
	
		
			
				
					|  |  |  |            mqtt_topic_array.append( | 
			
		
	
		
			
				
					|  |  |  |              (g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{inv_str}') + '/command', | 
			
		
	
		
			
				
					|  |  |  |               mqtt_config.get('QoS',0) | 
			
		
	
		
			
				
					|  |  |  |              )) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # start subscribe mqtt broker, if requested 'topic' is available | 
			
		
	
		
			
				
					|  |  |  |     if mqtt_c_obj and len(mqtt_topic_array) > 0: | 
			
		
	
		
			
				
					|  |  |  |        if hoymiles.HOYMILES_VERBOSE_LOGGING: | 
			
		
	
		
			
				
					|  |  |  |           logging.info(f'MQTT: subscribe for topic: {mqtt_topic_array}') | 
			
		
	
		
			
				
					|  |  |  |        mqtt_c_obj.client.subscribe(mqtt_topic_array) | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     # start main-loop | 
			
		
	
		
			
				
					|  |  |  |     main_loop(ahoy_config) | 
			
		
	
	
		
			
				
					|  |  | 
 |