|  | @@ -0,0 +1,721 @@
 | 
	
		
			
				|  |  | +#!/usr/bin/env python3
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +from influxdb import InfluxDBClient
 | 
	
		
			
				|  |  | +from os import path
 | 
	
		
			
				|  |  | +import configparser
 | 
	
		
			
				|  |  | +import sys
 | 
	
		
			
				|  |  | +import os
 | 
	
		
			
				|  |  | +import minimalmodbus
 | 
	
		
			
				|  |  | +import time
 | 
	
		
			
				|  |  | +import datetime
 | 
	
		
			
				|  |  | +import yaml
 | 
	
		
			
				|  |  | +import logging
 | 
	
		
			
				|  |  | +import json
 | 
	
		
			
				|  |  | +import paho.mqtt.client as mqtt
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +# Change working dir to the same dir as this script
 | 
	
		
			
				|  |  | +os.chdir(sys.path[0])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +config = configparser.ConfigParser()
 | 
	
		
			
				|  |  | +config.read('modbuslog.ini')
 | 
	
		
			
				|  |  | +#print(config.sections())
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +# additional conffile names
 | 
	
		
			
				|  |  | +conffile_meter_types = 'meter_types.yml'
 | 
	
		
			
				|  |  | +conffile_readings_names = 'readings_names.yml'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +# config vars used more than once or updateable via commandline argument are stored as global vars
 | 
	
		
			
				|  |  | +conf_modbus_read_retries = config['rs485'].getint('read_retries', 4)
 | 
	
		
			
				|  |  | +conf_modbus_raise_error_on_reading_failure = config['rs485'].getboolean('raise_error_on_reading_failure', False)
 | 
	
		
			
				|  |  | +conf_modbus_sleep_between_readings = config['rs485'].getfloat('sleep_between_readings', 0.1)
 | 
	
		
			
				|  |  | +conf_modbus_sleep_between_instruments = config['rs485'].getfloat('sleep_between_instruments', 0.7)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +conf_publish_on_mqtt = config['main'].getboolean('publish_on_mqtt', False)
 | 
	
		
			
				|  |  | +conf_store_in_influxdb = config['main'].getboolean('store_in_influxdb', False)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +conf_mqtt_enabled = config['mqtt'].getboolean('enable', False)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +conf_mqtt_topic_prefix = config['mqtt'].get('topic_prefix') #must NOT end with / !!
 | 
	
		
			
				|  |  | +if conf_mqtt_topic_prefix[-1:] == '/':
 | 
	
		
			
				|  |  | +    conf_mqtt_topic_prefix = conf_mqtt_topic_prefix[0:-1]
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +conf_mqtt_topic_error = config['mqtt'].get('topic_error') #must NOT end with / !!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +conf_storage_path = config['filelog'].get('storage_path')
 | 
	
		
			
				|  |  | +if conf_storage_path[-1:] != '/':
 | 
	
		
			
				|  |  | +    conf_storage_path += '/'
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +meters_interval_momentary = config['meters'].getint('interval_momentary', 1) # s - base interval for reading instruments 
 | 
	
		
			
				|  |  | +meters_interval_report_momentary = config['meters'].getint('interval_report_momentary', 60)    # interval for reporting momentary readings, 0 to report immediately, overruled by powerdelta settings
 | 
	
		
			
				|  |  | +meters_interval_energy = config['meters'].getint('interval_energy', 60)    # s - interval for reporting kWh-readings
 | 
	
		
			
				|  |  | +                            # for now this is not a seperate function but based on "meters_interval_momentary" 
 | 
	
		
			
				|  |  | +                            # easuring elapsed time since last reading, so actual interval can vary, especially when
 | 
	
		
			
				|  |  | +                            # high "meters_interval_momentary" is set. To avoid that set 
 | 
	
		
			
				|  |  | +                            # "meters_interval_momentary" (= command parameter --interval) to desired value 
 | 
	
		
			
				|  |  | +                            # and configure "meters_use_only_one_interval" to True
 | 
	
		
			
				|  |  | +meters_use_only_one_interval = config['meters'].getboolean('use_only_one_interval', False)   # use only interval 1 "meters_interval_momentary"
 | 
	
		
			
				|  |  | +meters_report_on_powerdelta_low  = config['meters'].getfloat('report_on_powerdelta_low', 0.95)  # % in decimal notation - immediately report if power value changes by more then this %
 | 
	
		
			
				|  |  | +meters_report_on_powerdelta_high = config['meters'].getfloat('report_on_powerdelta_high', 1.05)      # % in decimal notation - immediately report if power value changes by more then this %
 | 
	
		
			
				|  |  | +meters_report_on_lowpower_treshold = config['meters'].getint('report_on_lowpower_treshold', 10) # treshold under which measured power is considered "low" and different powerdeltas are used
 | 
	
		
			
				|  |  | +meters_report_on_lowpower_powerdelta_low  = config['meters'].getfloat('report_on_lowpower_powerdelta_low', 0.70)   # % in decimal notation - immediately report if power value changes by more then this %
 | 
	
		
			
				|  |  | +meters_report_on_lowpower_powerdelta_high = config['meters'].getfloat('report_on_lowpower_powerdelta_high', 1.30)   # % in decimal notation - immediately report if power value changes by more then this %
 | 
	
		
			
				|  |  | +##report_instrument_read_retries = config['rs485'].getboolean('report_instrument_read_retries', False)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +conf_send_meters_readTime = config['meters'].getboolean('send_readtime', True)
 | 
	
		
			
				|  |  | +conf_default_decimals = config['readings'].getint('default_decimals', 3)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +conf_readingerror_publish_after = config['rs485'].getint('readingerror_publish_after', 60)         # time in s after that repeated instrument reading errors are published via MQTT
 | 
	
		
			
				|  |  | +conf_readingerror_publish_interval = config['rs485'].getint('readingerror_publish_interval', 300)     # interval in s to publish repeated instrument reading errors via MQTT
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +# -------------------------------------------------------------
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +# global variables - not for configuration
 | 
	
		
			
				|  |  | +args_output_verbose1 = False
 | 
	
		
			
				|  |  | +args_output_verbose2 = False
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +influxdb_write_energy_today_total = True
 | 
	
		
			
				|  |  | +influxdb_write_energy_yesterday_total = True
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class DataCollector:
 | 
	
		
			
				|  |  | +    def __init__(self, influx_client_momentary, influx_client_energy, meter_yaml):
 | 
	
		
			
				|  |  | +        self.influx_client_momentary = influx_client_momentary
 | 
	
		
			
				|  |  | +        self.influx_client_energy = influx_client_energy
 | 
	
		
			
				|  |  | +        self.meter_yaml = meter_yaml
 | 
	
		
			
				|  |  | +        self.max_iterations = None  # run indefinitely by default
 | 
	
		
			
				|  |  | +        #self.meter_types = None
 | 
	
		
			
				|  |  | +        self.meter_types = dict()
 | 
	
		
			
				|  |  | +        self.meter_types_last_change = dict()
 | 
	
		
			
				|  |  | +        #self.meter_types_last_change = -1
 | 
	
		
			
				|  |  | +        self.meter_map = None
 | 
	
		
			
				|  |  | +        self.meter_map_last_change = -1
 | 
	
		
			
				|  |  | +        self.meter_configuration_lastchecktime = None
 | 
	
		
			
				|  |  | +        self.meter_typesconfiguration_lastchecktime = None
 | 
	
		
			
				|  |  | +        self.lastMomentaryReportTime = dict()
 | 
	
		
			
				|  |  | +        self.lastEnergyUpdate = dict()
 | 
	
		
			
				|  |  | +        self.lastReadingErrorTime = dict()
 | 
	
		
			
				|  |  | +        self.lastReadingErrorPublishtime = dict()
 | 
	
		
			
				|  |  | +        #self.totalEnergy = dict()
 | 
	
		
			
				|  |  | +        self.saved_energy_today_min = dict()
 | 
	
		
			
				|  |  | +        self.data_momentary_last = dict()
 | 
	
		
			
				|  |  | +        self.saved_energy_yesterday_total = dict()     # remember total energy for each meter 
 | 
	
		
			
				|  |  | +        self.saved_todays_date = dict()      # remember today´s date for each meter, needed to check for date rollover in order to calculate energy yesterday/today
 | 
	
		
			
				|  |  | +        log.info('Meters:')
 | 
	
		
			
				|  |  | +        #for meter in sorted(self.get_meters()):   # does not work in Python 3, so dont sort for now
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        # reading conffile_readings_names
 | 
	
		
			
				|  |  | +        self.readingsNames = yaml.load(open(conffile_readings_names), Loader=yaml.FullLoader)
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        for meter in self.get_meters():
 | 
	
		
			
				|  |  | +            log.info('\t {} <--> {}'.format( meter['id'], meter['name']))
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    def load_meter_type(self, metertype):
 | 
	
		
			
				|  |  | +        log.info("Loading meter type: " + metertype)
 | 
	
		
			
				|  |  | +        conffile_meter_type = "metertype_" + metertype + ".yml"
 | 
	
		
			
				|  |  | +        assert path.exists(conffile_meter_type), 'Meters configuration not found: %s' % conffile_meter_type
 | 
	
		
			
				|  |  | +        lastchange = self.meter_types_last_change.get(metertype, None)
 | 
	
		
			
				|  |  | +        lastchange_file = path.getmtime(conffile_meter_type)
 | 
	
		
			
				|  |  | +        if lastchange == None or (lastchange and lastchange_file != lastchange):
 | 
	
		
			
				|  |  | +            try:
 | 
	
		
			
				|  |  | +                log.info('Reloading meter type configuration for ' + metertype + 'as file changed')
 | 
	
		
			
				|  |  | +                self.meter_types[metertype] = yaml.load(open(conffile_meter_type), Loader=yaml.FullLoader)
 | 
	
		
			
				|  |  | +                self.meter_types_last_change[metertype] = lastchange_file
 | 
	
		
			
				|  |  | +                log.debug('Reloaded meters configuration')
 | 
	
		
			
				|  |  | +            except Exception as e:
 | 
	
		
			
				|  |  | +                log.warning('Failed to re-load meter type configuration, going on with the old one.')
 | 
	
		
			
				|  |  | +                log.warning(e)
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    def check_load_reload_meter_types(self):
 | 
	
		
			
				|  |  | +        log.debug("")
 | 
	
		
			
				|  |  | +        log.debug("checking loaded meter types...")
 | 
	
		
			
				|  |  | +        for metertype in self.meter_types: 
 | 
	
		
			
				|  |  | +            log.debug(metertype)
 | 
	
		
			
				|  |  | +            self.load_meter_type(metertype)
 | 
	
		
			
				|  |  | +        log.debug("")
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +    def get_meters(self):
 | 
	
		
			
				|  |  | +        reloadconf = False
 | 
	
		
			
				|  |  | +        ts = int(time.time())
 | 
	
		
			
				|  |  | +        if self.meter_configuration_lastchecktime == None or (ts - self.meter_configuration_lastchecktime) > 60:
 | 
	
		
			
				|  |  | +            self.meter_configuration_lastchecktime = ts
 | 
	
		
			
				|  |  | +            assert path.exists(self.meter_yaml), 'Meter map not found: %s' % self.meter_yaml
 | 
	
		
			
				|  |  | +            if path.getmtime(self.meter_yaml) != self.meter_map_last_change:
 | 
	
		
			
				|  |  | +                reloadconf = True
 | 
	
		
			
				|  |  | +        if reloadconf:
 | 
	
		
			
				|  |  | +            try:
 | 
	
		
			
				|  |  | +                log.info('Reloading meter map as file changed')
 | 
	
		
			
				|  |  | +                new_map = yaml.load(open(self.meter_yaml), Loader=yaml.FullLoader)
 | 
	
		
			
				|  |  | +                self.meter_map = new_map['meters']
 | 
	
		
			
				|  |  | +                self.meter_map_last_change = path.getmtime(self.meter_yaml)                
 | 
	
		
			
				|  |  | +                log.debug('Reloaded meter map')
 | 
	
		
			
				|  |  | +                for entry in self.meter_map:
 | 
	
		
			
				|  |  | +                    log.debug(entry['type'])
 | 
	
		
			
				|  |  | +                    self.load_meter_type(entry['type'])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            except Exception as e:
 | 
	
		
			
				|  |  | +                log.warning('Failed to re-load meter map, going on with the old one.')
 | 
	
		
			
				|  |  | +                log.warning(e)
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +        if self.meter_typesconfiguration_lastchecktime == None or (ts - self.meter_typesconfiguration_lastchecktime) > 60:
 | 
	
		
			
				|  |  | +            self.meter_typesconfiguration_lastchecktime = ts
 | 
	
		
			
				|  |  | +            self.check_load_reload_meter_types()
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +        return self.meter_map
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def collect_and_store(self):
 | 
	
		
			
				|  |  | +        #instrument.debug = True
 | 
	
		
			
				|  |  | +        meters = self.get_meters()
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        instrument = minimalmodbus.Instrument(config['rs485'].get('serialdevice','/dev/ttyUSB0'), config['rs485'].getfloat('serialtimeout', 1.0))
 | 
	
		
			
				|  |  | +        instrument.mode = minimalmodbus.MODE_RTU   # rtu or ascii mode
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        data_momentary = dict()
 | 
	
		
			
				|  |  | +        data_energy = dict()
 | 
	
		
			
				|  |  | +        meter_id_name = dict() # mapping id to name
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        for meter in meters:
 | 
	
		
			
				|  |  | +            meterReadingError_momentary = False
 | 
	
		
			
				|  |  | +            meterReadingError_energy = False
 | 
	
		
			
				|  |  | +            if conf_modbus_sleep_between_instruments  > 0:
 | 
	
		
			
				|  |  | +                time.sleep(conf_modbus_sleep_between_instruments)
 | 
	
		
			
				|  |  | +            meter_id_name[meter['id']] = meter['name']
 | 
	
		
			
				|  |  | +            instrument.serial.baudrate = meter['baudrate']
 | 
	
		
			
				|  |  | +            instrument.serial.bytesize = meter['bytesize']
 | 
	
		
			
				|  |  | +            if meter['parity'] == 'none':
 | 
	
		
			
				|  |  | +                instrument.serial.parity = minimalmodbus.serial.PARITY_NONE
 | 
	
		
			
				|  |  | +            elif meter['parity'] == 'odd':
 | 
	
		
			
				|  |  | +                instrument.serial.parity = minimalmodbus.serial.PARITY_ODD
 | 
	
		
			
				|  |  | +            elif meter['parity'] == 'even':
 | 
	
		
			
				|  |  | +                instrument.serial.parity = minimalmodbus.serial.PARITY_EVEN
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                log.error('No parity specified')
 | 
	
		
			
				|  |  | +                raise
 | 
	
		
			
				|  |  | +            instrument.serial.stopbits = meter['stopbits']
 | 
	
		
			
				|  |  | +            instrument.serial.timeout  = meter['timeout']    # seconds
 | 
	
		
			
				|  |  | +            instrument.address = meter['id']    # this is the slave address number
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            log.debug('\nReading meter %s \'%s\'' % (meter['id'], meter_id_name[meter['id']]))
 | 
	
		
			
				|  |  | +            start_time = time.time()
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            #if not self.meter_types.get(meter['type'], False):
 | 
	
		
			
				|  |  | +            #    self.load_meter_type(meter['type'])
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +            readings = self.meter_types[meter['type']]
 | 
	
		
			
				|  |  | +            if args_output_verbose2:
 | 
	
		
			
				|  |  | +                log.debug("")
 | 
	
		
			
				|  |  | +                log.debug("Meter Type " + meter['type'] + " - defined readings:")
 | 
	
		
			
				|  |  | +                log.debug(json.dumps(readings, indent = 4))
 | 
	
		
			
				|  |  | +                log.debug("")
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            data_momentary[meter['id']] = dict()
 | 
	
		
			
				|  |  | +            data_energy[meter['id']] = dict()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            reading_success_momentary = 0
 | 
	
		
			
				|  |  | +            for reading in readings['momentary']:
 | 
	
		
			
				|  |  | +                # to prevent random readout errors, e.g. CRC check fail, sleep for a short time between the readings
 | 
	
		
			
				|  |  | +                if conf_modbus_sleep_between_readings > 0:
 | 
	
		
			
				|  |  | +                    time.sleep(conf_modbus_sleep_between_readings) # Sleep between readings to avoid read errors
 | 
	
		
			
				|  |  | +                retries = conf_modbus_read_retries
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                # get decimals needed from meter_types config
 | 
	
		
			
				|  |  | +                decimals = readings['momentary'][reading].get('decimals', conf_default_decimals)
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                while retries > 0:
 | 
	
		
			
				|  |  | +                    try:
 | 
	
		
			
				|  |  | +                        retries -= 1
 | 
	
		
			
				|  |  | +                        data_momentary[meter['id']][reading] = round(instrument.read_float(readings['momentary'][reading]['address'], 4, 2), decimals)
 | 
	
		
			
				|  |  | +                        log.debug('OK read meter {}, {} retries => \'{}\' = \'{}\''.format(meter['id'], conf_modbus_read_retries - retries, reading, data_momentary[meter['id']][reading]))
 | 
	
		
			
				|  |  | +                        retries = 0
 | 
	
		
			
				|  |  | +                        reading_success_momentary += 1
 | 
	
		
			
				|  |  | +                        pass
 | 
	
		
			
				|  |  | +                    except ValueError as ve:
 | 
	
		
			
				|  |  | +                        log.warning('Value Error while reading register {} from meter {}. Retries left {}.'
 | 
	
		
			
				|  |  | +                               .format(readings['momentary'][reading]['address'], meter['id'], retries))
 | 
	
		
			
				|  |  | +                        log.error(ve)
 | 
	
		
			
				|  |  | +                        if retries == 0 and conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                            raise RuntimeError
 | 
	
		
			
				|  |  | +                    except TypeError as te:
 | 
	
		
			
				|  |  | +                        log.warning('Type Error while reading register {} from meter {}. Retries left {}.'
 | 
	
		
			
				|  |  | +                               .format(readings['momentary'][reading]['address'], meter['id'], retries))
 | 
	
		
			
				|  |  | +                        log.error(te)
 | 
	
		
			
				|  |  | +                        if retries == 0 and conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                            raise RuntimeError
 | 
	
		
			
				|  |  | +                    except IOError as ie:
 | 
	
		
			
				|  |  | +                        log.warning('IO Error while reading register {} from meter {}. Retries left {}.'
 | 
	
		
			
				|  |  | +                               .format(readings['momentary'][reading]['address'], meter['id'], retries))
 | 
	
		
			
				|  |  | +                        log.error(ie)
 | 
	
		
			
				|  |  | +                        if retries == 0 and conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                            raise RuntimeError
 | 
	
		
			
				|  |  | +                    except:
 | 
	
		
			
				|  |  | +                        log.error("Unexpected error:", sys.exc_info()[0])
 | 
	
		
			
				|  |  | +                        raise
 | 
	
		
			
				|  |  | +            if reading_success_momentary < len(readings['momentary']):
 | 
	
		
			
				|  |  | +                log.debug("THERE WERE READING ERRORS")
 | 
	
		
			
				|  |  | +                meterReadingError_momentary = True
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            # report momentary interval
 | 
	
		
			
				|  |  | +            reportMomentary = False
 | 
	
		
			
				|  |  | +            if meters_interval_report_momentary > 0:
 | 
	
		
			
				|  |  | +                ts = int(time.time())
 | 
	
		
			
				|  |  | +                lastMomentaryReportTime = self.lastMomentaryReportTime.get(meter['id'], False)
 | 
	
		
			
				|  |  | +                if lastMomentaryReportTime:
 | 
	
		
			
				|  |  | +                    tdiff = ts - lastMomentaryReportTime
 | 
	
		
			
				|  |  | +                    if (tdiff > meters_interval_report_momentary):
 | 
	
		
			
				|  |  | +                        log.debug('Reporting momentary readings for meter %s' % meter['id'])
 | 
	
		
			
				|  |  | +                        reportMomentary = True
 | 
	
		
			
				|  |  | +                        self.lastMomentaryReportTime[meter['id']] = ts
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    log.debug('No lastMomentaryReportTime has yet been saved for meter %s' % meter['id'])
 | 
	
		
			
				|  |  | +                    reportMomentary = True
 | 
	
		
			
				|  |  | +                    self.lastMomentaryReportTime[meter['id']] = ts
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                reportMomentary = True
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +             
 | 
	
		
			
				|  |  | +            # override meters_interval_report_momentary if power has changed for more than configured powerdelta and no interval reporting is due in this iteration
 | 
	
		
			
				|  |  | +            if config['meters'].getboolean('report_on_powerdelta_enable', False) and not reportMomentary:
 | 
	
		
			
				|  |  | +                lastValues = self.data_momentary_last.get(meter['id'], None)
 | 
	
		
			
				|  |  | +                                
 | 
	
		
			
				|  |  | +                for usedReading in data_momentary[meter['id']].keys():
 | 
	
		
			
				|  |  | +                    currentValue = data_momentary[meter['id']][usedReading]
 | 
	
		
			
				|  |  | +                    for powerreadingname in self.readingsNames['power']:
 | 
	
		
			
				|  |  | +                        if usedReading == powerreadingname:
 | 
	
		
			
				|  |  | +                            lastValue = None
 | 
	
		
			
				|  |  | +                            if lastValues != None:
 | 
	
		
			
				|  |  | +                                lastValue = lastValues.get(powerreadingname, None)
 | 
	
		
			
				|  |  | +                            if lastValue != None:
 | 
	
		
			
				|  |  | +                                if (currentValue >= meters_report_on_lowpower_treshold):
 | 
	
		
			
				|  |  | +                                    powerdelta_high = meters_report_on_powerdelta_high
 | 
	
		
			
				|  |  | +                                    powerdelta_low = meters_report_on_powerdelta_low
 | 
	
		
			
				|  |  | +                                else:
 | 
	
		
			
				|  |  | +                                    powerdelta_high = meters_report_on_lowpower_powerdelta_high
 | 
	
		
			
				|  |  | +                                    powerdelta_low = meters_report_on_lowpower_powerdelta_low
 | 
	
		
			
				|  |  | +                                    
 | 
	
		
			
				|  |  | +                                if (currentValue > (lastValue * powerdelta_high)):
 | 
	
		
			
				|  |  | +                                    log.debug(powerreadingname + " INCREASED by more than factor " + str(powerdelta_high) + "     currentValue=" + str(currentValue) + "  lastValue=" + str(lastValue))
 | 
	
		
			
				|  |  | +                                    reportMomentary = True
 | 
	
		
			
				|  |  | +                                if (currentValue < (lastValue * powerdelta_low)):
 | 
	
		
			
				|  |  | +                                    log.debug(powerreadingname + " DECREASED by more than factor " + str(powerdelta_low) + "     currentValue=" + str(currentValue) + "  lastValue=" + str(lastValue))
 | 
	
		
			
				|  |  | +                                    reportMomentary = True
 | 
	
		
			
				|  |  | +                            if lastValues == None:
 | 
	
		
			
				|  |  | +                                self.data_momentary_last[meter['id']] = dict()
 | 
	
		
			
				|  |  | +                            self.data_momentary_last[meter['id']][powerreadingname] = data_momentary[meter['id']][powerreadingname]
 | 
	
		
			
				|  |  | +                    
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            # influxdb
 | 
	
		
			
				|  |  | +            t_utc = datetime.datetime.utcnow()
 | 
	
		
			
				|  |  | +            t_str = t_utc.isoformat() + 'Z'
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            if conf_store_in_influxdb and not meterReadingError_momentary and reportMomentary:
 | 
	
		
			
				|  |  | +                jsondata_momentary = [
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        'measurement': 'energy',
 | 
	
		
			
				|  |  | +                        'tags': {
 | 
	
		
			
				|  |  | +                            'meter': meter_id_name[meter['id']],
 | 
	
		
			
				|  |  | +                        },
 | 
	
		
			
				|  |  | +                        'time': t_str,
 | 
	
		
			
				|  |  | +                        'fields': data_momentary[meter['id']]
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                ]
 | 
	
		
			
				|  |  | +                if args_output_verbose1:
 | 
	
		
			
				|  |  | +                    print(json.dumps(jsondata_momentary, indent = 4))
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    self.influx_client_momentary.write_points(jsondata_momentary)
 | 
	
		
			
				|  |  | +                except Exception as e:
 | 
	
		
			
				|  |  | +                    log.error('Data not written!')
 | 
	
		
			
				|  |  | +                    log.error(e)
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +            if conf_send_meters_readTime:
 | 
	
		
			
				|  |  | +                readtime = round(time.time() - start_time, 3)
 | 
	
		
			
				|  |  | +                log.debug("Read time: " + str(readtime))
 | 
	
		
			
				|  |  | +                data_momentary[meter['id']]['Read time'] = readtime
 | 
	
		
			
				|  |  | +                if conf_mqtt_enabled and conf_publish_on_mqtt:
 | 
	
		
			
				|  |  | +                        mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/ReadTime", str(readtime))
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            if conf_mqtt_enabled and conf_publish_on_mqtt and reportMomentary:
 | 
	
		
			
				|  |  | +                for reading in readings['momentary']:
 | 
	
		
			
				|  |  | +                    tmpreading = data_momentary[meter['id']].get(reading, None)
 | 
	
		
			
				|  |  | +                    if tmpreading != None:
 | 
	
		
			
				|  |  | +                        if tmpreading.is_integer():
 | 
	
		
			
				|  |  | +                            tmpreading = int(tmpreading)
 | 
	
		
			
				|  |  | +                        #mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + reading, str('{0:.3f}'.format(tmpreading)))
 | 
	
		
			
				|  |  | +                        log.debug("MQTT pub: '"+conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + reading + "' = '" + str(tmpreading) + "'")
 | 
	
		
			
				|  |  | +                        mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + reading, str(tmpreading))
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            if meters_use_only_one_interval:
 | 
	
		
			
				|  |  | +                readEnergyData = True
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                readEnergyData = False
 | 
	
		
			
				|  |  | +                ts = int(time.time())
 | 
	
		
			
				|  |  | +                lastUpdate = self.lastEnergyUpdate.get(meter['id'], False)
 | 
	
		
			
				|  |  | +                if lastUpdate:
 | 
	
		
			
				|  |  | +                    tdiff = ts - lastUpdate
 | 
	
		
			
				|  |  | +                    if (tdiff > meters_interval_energy):
 | 
	
		
			
				|  |  | +                        readEnergyData = True
 | 
	
		
			
				|  |  | +                        self.lastEnergyUpdate[meter['id']] = ts
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    log.debug('No lastEnergyUpdate has yet been saved for meter %s' % meter['id'])
 | 
	
		
			
				|  |  | +                    readEnergyData = True
 | 
	
		
			
				|  |  | +                    self.lastEnergyUpdate[meter['id']] = ts
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            # save and restore yesterday´s total energy to calculate today´s energy
 | 
	
		
			
				|  |  | +            # check if total energy from yesterday is stored in memory, if not try to get it from saved file
 | 
	
		
			
				|  |  | +            today = datetime.date.today()
 | 
	
		
			
				|  |  | +            today_str = today.strftime('%Y%m%d')
 | 
	
		
			
				|  |  | +            yesterday = today - datetime.timedelta(days = 1)
 | 
	
		
			
				|  |  | +            yesterday_str = yesterday.strftime('%Y%m%d')
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            # check for date rollover
 | 
	
		
			
				|  |  | +            dateRollover = False
 | 
	
		
			
				|  |  | +            savedtoday = self.saved_todays_date.get(meter['id'], False)
 | 
	
		
			
				|  |  | +            if not savedtoday or savedtoday != today:
 | 
	
		
			
				|  |  | +                log.debug("date rollover happened or no date has been saved yet for meter " + str(meter['id']))
 | 
	
		
			
				|  |  | +                if savedtoday and savedtoday == yesterday:
 | 
	
		
			
				|  |  | +                    # a date rollover just happened, so change todays date to current and proceed with what has to be done
 | 
	
		
			
				|  |  | +                    dateRollover = True
 | 
	
		
			
				|  |  | +                    readEnergyData = True
 | 
	
		
			
				|  |  | +                    #log.debug(savedtoday)
 | 
	
		
			
				|  |  | +                self.saved_todays_date[meter['id']] = today
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +            if readEnergyData:
 | 
	
		
			
				|  |  | +                reading_success_energy = 0
 | 
	
		
			
				|  |  | +                for reading in readings['energy']:
 | 
	
		
			
				|  |  | +                    # to prevent random readout errors, e.g. CRC check fail, sleep for a short time between the readings
 | 
	
		
			
				|  |  | +                    if conf_modbus_sleep_between_readings > 0:
 | 
	
		
			
				|  |  | +                        time.sleep(conf_modbus_sleep_between_readings) # Sleep between readings to avoid read errors
 | 
	
		
			
				|  |  | +                    retries = conf_modbus_read_retries
 | 
	
		
			
				|  |  | +                    
 | 
	
		
			
				|  |  | +                    # get decimals needed from meter_types config
 | 
	
		
			
				|  |  | +                    decimals = readings['energy'][reading].get('decimals', conf_default_decimals)
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                    while retries > 0:
 | 
	
		
			
				|  |  | +                        try:
 | 
	
		
			
				|  |  | +                            retries -= 1
 | 
	
		
			
				|  |  | +                            data_energy[meter['id']][reading] = round(instrument.read_float(readings['energy'][reading]['address'], 4, 2), decimals)
 | 
	
		
			
				|  |  | +                            log.debug('OK read meter {}, {} retries => \'{}\' = \'{}\''
 | 
	
		
			
				|  |  | +                                .format(meter['id'], conf_modbus_read_retries - retries, reading, data_energy[meter['id']][reading]))
 | 
	
		
			
				|  |  | +                            reading_success_energy += 1
 | 
	
		
			
				|  |  | +                            retries = 0
 | 
	
		
			
				|  |  | +                            pass
 | 
	
		
			
				|  |  | +                        except ValueError as ve:
 | 
	
		
			
				|  |  | +                            log.warning('Value Error while reading register {} from meter {}. Retries left {}.'
 | 
	
		
			
				|  |  | +                                .format(readings['energy'][reading]['address'], meter['id'], retries))
 | 
	
		
			
				|  |  | +                            log.error(ve)
 | 
	
		
			
				|  |  | +                            if retries == 0 and conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                                raise RuntimeError
 | 
	
		
			
				|  |  | +                        except TypeError as te:
 | 
	
		
			
				|  |  | +                            log.warning('Type Error while reading register {} from meter {}. Retries left {}.'
 | 
	
		
			
				|  |  | +                                .format(readings['energy'][reading]['address'], meter['id'], retries))
 | 
	
		
			
				|  |  | +                            log.error(te)
 | 
	
		
			
				|  |  | +                            if retries == 0 and conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                                raise RuntimeError
 | 
	
		
			
				|  |  | +                        except IOError as ie:
 | 
	
		
			
				|  |  | +                            log.warning('IO Error while reading register {} from meter {}. Retries left {}.'
 | 
	
		
			
				|  |  | +                                .format(readings['energy'][reading]['address'], meter['id'], retries))
 | 
	
		
			
				|  |  | +                            log.error(ie)
 | 
	
		
			
				|  |  | +                            if retries == 0 and conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                                raise RuntimeError
 | 
	
		
			
				|  |  | +                        except:
 | 
	
		
			
				|  |  | +                            log.error("Unexpected error:", sys.exc_info()[0])
 | 
	
		
			
				|  |  | +                            if conf_modbus_raise_error_on_reading_failure:
 | 
	
		
			
				|  |  | +                                raise
 | 
	
		
			
				|  |  | +                if reading_success_energy < len(readings['energy']):
 | 
	
		
			
				|  |  | +                    log.debug("THERE WERE READING ERRORS")
 | 
	
		
			
				|  |  | +                    meterReadingError_energy = True
 | 
	
		
			
				|  |  | +                    
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                file_path_meter = conf_storage_path + meter_id_name[meter['id']] + "/"
 | 
	
		
			
				|  |  | +                file_today_min = file_path_meter + today_str + "_min.txt"
 | 
	
		
			
				|  |  | +                file_yesterday_total = file_path_meter + yesterday_str + "_total.txt"
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                energy_today_total = 0
 | 
	
		
			
				|  |  | +                energy_yesterday_min = 0
 | 
	
		
			
				|  |  | +                energy_today_min = self.saved_energy_today_min.get(meter['id'], None)
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                if dateRollover:
 | 
	
		
			
				|  |  | +                    energy_today_min = None
 | 
	
		
			
				|  |  | +                if energy_today_min == None:
 | 
	
		
			
				|  |  | +                    exists = os.path.isfile(file_today_min)
 | 
	
		
			
				|  |  | +                    if exists:
 | 
	
		
			
				|  |  | +                        # load energy_today_min from file if exists
 | 
	
		
			
				|  |  | +                        f = open(file_today_min, "r")
 | 
	
		
			
				|  |  | +                        if f.mode == 'r':
 | 
	
		
			
				|  |  | +                            contents = f.read()
 | 
	
		
			
				|  |  | +                            f.close()
 | 
	
		
			
				|  |  | +                        energy_today_min = float(contents)
 | 
	
		
			
				|  |  | +                        self.saved_energy_today_min[meter['id']] = energy_today_min
 | 
	
		
			
				|  |  | +                        log.debug(meter_id_name[meter['id']] + " - Energy Today min read from file -> = " + str(energy_today_min) + " kWh")
 | 
	
		
			
				|  |  | +                    else:
 | 
	
		
			
				|  |  | +                        # save current Energy_total to min-file
 | 
	
		
			
				|  |  | +                        if not os.path.exists(file_path_meter):
 | 
	
		
			
				|  |  | +                            os.mkdir(file_path_meter)
 | 
	
		
			
				|  |  | +                        f = open(file_today_min, "w+")
 | 
	
		
			
				|  |  | +                        energy_today_min = data_energy[meter['id']][self.readingsNames['energy_total']]
 | 
	
		
			
				|  |  | +                        self.saved_energy_today_min[meter['id']] = energy_today_min
 | 
	
		
			
				|  |  | +                        f.write(str('{0:.3f}'.format(energy_today_min)))
 | 
	
		
			
				|  |  | +                        f.close()
 | 
	
		
			
				|  |  | +                log.debug(meter_id_name[meter['id']] + " - Energy Today Min: " + str(energy_today_min) + " kWh")
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    energy_today_total = data_energy[meter['id']][self.readingsNames['energy_total']] - energy_today_min
 | 
	
		
			
				|  |  | +                    log.debug(meter_id_name[meter['id']] + " - Energy Today total: " + str('{0:.3f}'.format(energy_today_total)) + " kWh")
 | 
	
		
			
				|  |  | +                except:
 | 
	
		
			
				|  |  | +                    pass
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                energy_yesterday_total = self.saved_energy_yesterday_total.get(meter['id'], None)
 | 
	
		
			
				|  |  | +                if dateRollover:
 | 
	
		
			
				|  |  | +                    energy_yesterday_total = None
 | 
	
		
			
				|  |  | +                if energy_yesterday_total == None:
 | 
	
		
			
				|  |  | +                    exists = os.path.isfile(file_yesterday_total)
 | 
	
		
			
				|  |  | +                    if exists:
 | 
	
		
			
				|  |  | +                        # load energy_yesterday_total from file if exists
 | 
	
		
			
				|  |  | +                        f = open(file_yesterday_total, "r")
 | 
	
		
			
				|  |  | +                        if f.mode == 'r':
 | 
	
		
			
				|  |  | +                            contents = f.read()
 | 
	
		
			
				|  |  | +                            f.close()
 | 
	
		
			
				|  |  | +                        energy_yesterday_total = float(contents)
 | 
	
		
			
				|  |  | +                        self.saved_energy_yesterday_total[meter['id']] = energy_yesterday_total
 | 
	
		
			
				|  |  | +                        log.debug(meter_id_name[meter['id']] + " - Energy Yesterday total read from file -> = " + str(energy_yesterday_total) + " kWh")
 | 
	
		
			
				|  |  | +                    else:
 | 
	
		
			
				|  |  | +                        file_yesterday_min = file_path_meter + yesterday_str + "_min.txt"
 | 
	
		
			
				|  |  | +                        exists = os.path.isfile(file_yesterday_min)
 | 
	
		
			
				|  |  | +                        if exists:
 | 
	
		
			
				|  |  | +                            # load yesterday_min from file
 | 
	
		
			
				|  |  | +                            #if args_output_verbose1:
 | 
	
		
			
				|  |  | +                            #    print("file file_yesterday_min exists")
 | 
	
		
			
				|  |  | +                            f = open(file_yesterday_min, "r")
 | 
	
		
			
				|  |  | +                            if f.mode == 'r':
 | 
	
		
			
				|  |  | +                                contents =f.read()
 | 
	
		
			
				|  |  | +                                f.close()
 | 
	
		
			
				|  |  | +                            energy_yesterday_min = float(contents)
 | 
	
		
			
				|  |  | +                            log.debug(meter_id_name[meter['id']] + " - Energy yesterday min: " + str(energy_yesterday_min) + " kWh")
 | 
	
		
			
				|  |  | +                            
 | 
	
		
			
				|  |  | +                            energy_yesterday_total = round(energy_today_min - energy_yesterday_min, 3)
 | 
	
		
			
				|  |  | +                            ###log.debug(meter_id_name[meter['id']] + " - Energy yesterday total: " + str(energy_yesterday_total))
 | 
	
		
			
				|  |  | +                            
 | 
	
		
			
				|  |  | +                            if not os.path.exists(file_path_meter):
 | 
	
		
			
				|  |  | +                                os.mkdir(file_path_meter)
 | 
	
		
			
				|  |  | +                            f = open(file_yesterday_total, "w+")
 | 
	
		
			
				|  |  | +                            f.write(str('{0:.3f}'.format(energy_yesterday_total)))
 | 
	
		
			
				|  |  | +                            f.close()
 | 
	
		
			
				|  |  | +                        #else:
 | 
	
		
			
				|  |  | +                        #    # file yesterday_min does not exist
 | 
	
		
			
				|  |  | +                log.debug(meter_id_name[meter['id']] + " - Energy Yesterday Total: " + str(energy_yesterday_total) + " kWh")
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                if influxdb_write_energy_today_total:
 | 
	
		
			
				|  |  | +                    data_energy[meter['id']][self.readingsNames['energy_today']] = energy_today_total
 | 
	
		
			
				|  |  | +                if influxdb_write_energy_yesterday_total:
 | 
	
		
			
				|  |  | +                    data_energy[meter['id']][self.readingsNames['energy_yesterday']] = energy_yesterday_total
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                t_utc = datetime.datetime.utcnow()
 | 
	
		
			
				|  |  | +                t_str = t_utc.isoformat() + 'Z'
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                if conf_store_in_influxdb and not meterReadingError_energy:
 | 
	
		
			
				|  |  | +                    jsondata_energy = [
 | 
	
		
			
				|  |  | +                        {
 | 
	
		
			
				|  |  | +                            'measurement': 'energy',
 | 
	
		
			
				|  |  | +                            'tags': {
 | 
	
		
			
				|  |  | +                                'meter': meter_id_name[meter['id']],
 | 
	
		
			
				|  |  | +                            },
 | 
	
		
			
				|  |  | +                            'time': t_str,
 | 
	
		
			
				|  |  | +                            'fields': data_energy[meter['id']]
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                    ]
 | 
	
		
			
				|  |  | +                    if args_output_verbose1:
 | 
	
		
			
				|  |  | +                        print(json.dumps(jsondata_energy, indent = 4))
 | 
	
		
			
				|  |  | +                    try:
 | 
	
		
			
				|  |  | +                        self.influx_client_energy.write_points(jsondata_energy)
 | 
	
		
			
				|  |  | +                    except Exception as e:
 | 
	
		
			
				|  |  | +                        log.error('Data not written!')
 | 
	
		
			
				|  |  | +                        log.error(e)
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                if conf_send_meters_readTime:
 | 
	
		
			
				|  |  | +                    readtime = round(time.time() - start_time, 3)
 | 
	
		
			
				|  |  | +                    log.debug("Read time: " + str(readtime))
 | 
	
		
			
				|  |  | +                    data_energy[meter['id']]['Read time'] = readtime
 | 
	
		
			
				|  |  | +                    if conf_mqtt_enabled and conf_publish_on_mqtt:
 | 
	
		
			
				|  |  | +                        mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/ReadTime", str(readtime))
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                if conf_mqtt_enabled and conf_publish_on_mqtt:
 | 
	
		
			
				|  |  | +                    for reading in readings['energy']:
 | 
	
		
			
				|  |  | +                        tmpreading = data_energy[meter['id']].get(reading, None)
 | 
	
		
			
				|  |  | +                        if tmpreading != None:
 | 
	
		
			
				|  |  | +                            if tmpreading.is_integer():
 | 
	
		
			
				|  |  | +                                tmpreading = int(tmpreading)
 | 
	
		
			
				|  |  | +                            #mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + reading, str('{0:.3f}'.format(tmpreading)))
 | 
	
		
			
				|  |  | +                            log.debug("MQTT pub: '"+conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + reading + "' = '" + str(tmpreading) + "'")
 | 
	
		
			
				|  |  | +                            mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + reading, str(tmpreading))
 | 
	
		
			
				|  |  | +                    mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + self.readingsNames['energy_today'], str('{0:.3f}'.format(energy_today_total)))
 | 
	
		
			
				|  |  | +                    mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/" + self.readingsNames['energy_yesterday'], str('{0:.3f}'.format(energy_yesterday_total)))
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            if meterReadingError_momentary or meterReadingError_energy:
 | 
	
		
			
				|  |  | +                ts = int(time.time())
 | 
	
		
			
				|  |  | +                lasterrortime = self.lastReadingErrorTime.get(meter['id'], 0)
 | 
	
		
			
				|  |  | +                if lasterrortime == 0:
 | 
	
		
			
				|  |  | +                    self.lastReadingErrorTime[meter['id']] = ts
 | 
	
		
			
				|  |  | +                elif (ts - lasterrortime) > conf_readingerror_publish_after:
 | 
	
		
			
				|  |  | +                    lasterrorpubtime = self.lastReadingErrorPublishtime.get(meter['id'], 0)
 | 
	
		
			
				|  |  | +                    if lasterrorpubtime == 0 or (lasterrorpubtime > 0 and (ts - lasterrorpubtime) > conf_readingerror_publish_interval):
 | 
	
		
			
				|  |  | +                        self.lastReadingErrorPublishtime[meter['id']] = ts
 | 
	
		
			
				|  |  | +                        if conf_mqtt_enabled and conf_publish_on_mqtt:
 | 
	
		
			
				|  |  | +                            lasterrortime_str = datetime.datetime.fromtimestamp(lasterrortime).strftime("%Y-%m-%d %H:%M:%S")
 | 
	
		
			
				|  |  | +                            mqttc.publish(conf_mqtt_topic_prefix + "/" + meter_id_name[meter['id']] + "/STATE", "ERROR: could not read MODBUS meter " + meter_id_name[meter['id']] + " with ID=" + str(meter['id']) + " since " + str(lasterrortime_str))
 | 
	
		
			
				|  |  | +                            mqttc.publish(conf_mqtt_topic_error, "ERROR: could not read MODBUS meter " + meter_id_name[meter['id']] + " with ID=" + str(meter['id']) + " since " + str(lasterrortime_str))
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                self.lastReadingErrorTime[meter['id']] = 0
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +# END class DataCollector
 | 
	
		
			
				|  |  | +################################
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +def mqtt_on_connect(client, userdata, flags, rc):
 | 
	
		
			
				|  |  | +    if args_output_verbose1:
 | 
	
		
			
				|  |  | +        print("MQTT connected with result code " + str(rc))
 | 
	
		
			
				|  |  | +    #client.subscribe("some/topic")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +def mqtt_on_disconnect(client, userdata, rc):
 | 
	
		
			
				|  |  | +    if rc != 0:
 | 
	
		
			
				|  |  | +        if print_errors:
 | 
	
		
			
				|  |  | +            print("Unexpected MQTT disconnection. Will auto-reconnect")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +def repeat(interval_sec, max_iter, func, *args, **kwargs):
 | 
	
		
			
				|  |  | +    from itertools import count
 | 
	
		
			
				|  |  | +    starttime = 0
 | 
	
		
			
				|  |  | +    for i in count():
 | 
	
		
			
				|  |  | +        if i > 0 and interval_sec > 0:  # do not wait for interval time on first run
 | 
	
		
			
				|  |  | +            if ((time.time() - starttime) < interval_sec):
 | 
	
		
			
				|  |  | +                sleeptime = interval_sec - (time.time() - starttime)
 | 
	
		
			
				|  |  | +                print("\nsleep " + str(sleeptime) + " s")
 | 
	
		
			
				|  |  | +                time.sleep(sleeptime)
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            starttime = time.time()
 | 
	
		
			
				|  |  | +            func(*args, **kwargs)
 | 
	
		
			
				|  |  | +        except Exception as ex:
 | 
	
		
			
				|  |  | +            log.error(ex)
 | 
	
		
			
				|  |  | +        if max_iter and i >= max_iter:
 | 
	
		
			
				|  |  | +            return
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +if __name__ == '__main__':
 | 
	
		
			
				|  |  | +    import argparse
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    parser = argparse.ArgumentParser()
 | 
	
		
			
				|  |  | +    parser.add_argument('--interval', default=meters_interval_momentary,
 | 
	
		
			
				|  |  | +                        help='Meter readout interval for momentary values i.e. power, current... - in seconds, default 1s')
 | 
	
		
			
				|  |  | +    parser.add_argument('--energyinterval', default=meters_interval_energy,
 | 
	
		
			
				|  |  | +                        help='Meter readout interval for energy values, i.e. total kWh - in seconds, default 60s')
 | 
	
		
			
				|  |  | +    parser.add_argument('--use-only-one-interval', default=False,
 | 
	
		
			
				|  |  | +                        help='Meter readout interval for energy values, i.e. total kWh - in seconds, default 60s', action='store_true')
 | 
	
		
			
				|  |  | +    parser.add_argument('--meters', default='meters.yml',
 | 
	
		
			
				|  |  | +                        help='YAML file containing Meter ID, name, type etc. Default "meters.yml"')
 | 
	
		
			
				|  |  | +    #parser.add_argument('--verbose', '-v', default=0, help='print read data from the instruments to console', action='store_true')
 | 
	
		
			
				|  |  | +    parser.add_argument('--verbose', '-v', type=int, default=0, choices=[1, 2], help='print read data from the instruments to console')
 | 
	
		
			
				|  |  | +    parser.add_argument('--log', default='CRITICAL',
 | 
	
		
			
				|  |  | +                        help='Log levels, DEBUG, INFO, WARNING, ERROR or CRITICAL')
 | 
	
		
			
				|  |  | +    parser.add_argument('--logfile', default='',
 | 
	
		
			
				|  |  | +                        help='Specify log file, if not specified the log is streamed to console')
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    args = parser.parse_args()
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    loglevel = args.log.upper()
 | 
	
		
			
				|  |  | +    logfile = args.logfile
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    # Setup logging
 | 
	
		
			
				|  |  | +    log = logging.getLogger('energy-logger')
 | 
	
		
			
				|  |  | +    log.setLevel(getattr(logging, loglevel))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if logfile:
 | 
	
		
			
				|  |  | +        loghandle = logging.FileHandler(logfile, 'w')
 | 
	
		
			
				|  |  | +        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
 | 
	
		
			
				|  |  | +        loghandle.setFormatter(formatter)
 | 
	
		
			
				|  |  | +    else:
 | 
	
		
			
				|  |  | +        loghandle = logging.StreamHandler()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    log.addHandler(loghandle)
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    log.info('Started app')
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    #if args.verbose:
 | 
	
		
			
				|  |  | +    if int(args.verbose) == 1 or int(args.verbose) == None:
 | 
	
		
			
				|  |  | +        args_output_verbose1 = True
 | 
	
		
			
				|  |  | +        args_output_verbose2 = False
 | 
	
		
			
				|  |  | +        log.info("Verbose Level 1 ON - printing read data to console.")
 | 
	
		
			
				|  |  | +    elif int(args.verbose) == 2:
 | 
	
		
			
				|  |  | +        args_output_verbose1 = True
 | 
	
		
			
				|  |  | +        args_output_verbose2 = True
 | 
	
		
			
				|  |  | +        log.info("Verbose Level 2 ON - printing read data and more to console.")
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +    interval = int(args.interval)
 | 
	
		
			
				|  |  | +    log.info("Interval 1 (for MOMENTARY readings): " + str(interval) + " s")
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    if args.use_only_one_interval:
 | 
	
		
			
				|  |  | +        meters_use_only_one_interval = True
 | 
	
		
			
				|  |  | +        log.info("Using only Interval 1")
 | 
	
		
			
				|  |  | +    else:
 | 
	
		
			
				|  |  | +        meters_interval_energy = int(args.energyinterval)
 | 
	
		
			
				|  |  | +        log.info("Interval 2 (for ENERGY readings):    " + str(meters_interval_energy) + " s")
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    # create MQTT client object
 | 
	
		
			
				|  |  | +    if conf_mqtt_enabled:
 | 
	
		
			
				|  |  | +        mqttc = mqtt.Client()
 | 
	
		
			
				|  |  | +        mqttc.on_connect = mqtt_on_connect
 | 
	
		
			
				|  |  | +        mqttc.on_disconnect = mqtt_on_disconnect
 | 
	
		
			
				|  |  | +        ##mqttc.on_message = on_message  # callback for incoming msg (unused)
 | 
	
		
			
				|  |  | +        if len(config['mqtt'].get('password')) > 0 or len(config['mqtt'].get('server')) > 0:
 | 
	
		
			
				|  |  | +            mqttc.username_pw_set(config['mqtt'].get('user'), config['mqtt'].get('password'))
 | 
	
		
			
				|  |  | +        mqttc.connect(config['mqtt'].get('server'), config['mqtt'].getint('port', 1883), 60)
 | 
	
		
			
				|  |  | +        mqttc.loop_start()
 | 
	
		
			
				|  |  | +        #mqttc.loop_forever()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    # Create the InfluxDB object
 | 
	
		
			
				|  |  | +    if config['influxdb'].getboolean('separate_momentary_database', False):
 | 
	
		
			
				|  |  | +        influxclient_energy = InfluxDBClient(config['influxdb'].get('host'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].getint('port', 8086),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('user'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('password'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('database'))
 | 
	
		
			
				|  |  | +        influxclient_momentary = InfluxDBClient(config['influxdb_momentary'].get('host'),
 | 
	
		
			
				|  |  | +                            config['influxdb_momentary'].getint('port', 8086),
 | 
	
		
			
				|  |  | +                            config['influxdb_momentary'].get('user'),
 | 
	
		
			
				|  |  | +                            config['influxdb_momentary'].get('password'),
 | 
	
		
			
				|  |  | +                            config['influxdb_momentary'].get('database'))
 | 
	
		
			
				|  |  | +    else:
 | 
	
		
			
				|  |  | +        influxclient_energy = InfluxDBClient(config['influxdb'].get('host'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].getint('port', 8086),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('user'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('password'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('database'))
 | 
	
		
			
				|  |  | +        influxclient_momentary = InfluxDBClient(config['influxdb'].get('host'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].getint('port', 8086),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('user'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('password'),
 | 
	
		
			
				|  |  | +                            config['influxdb'].get('database'))
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    collector = DataCollector(influx_client_momentary=influxclient_momentary,
 | 
	
		
			
				|  |  | +                              influx_client_energy=influxclient_energy,
 | 
	
		
			
				|  |  | +                              meter_yaml=args.meters)
 | 
	
		
			
				|  |  | +                              
 | 
	
		
			
				|  |  | +    repeat(interval,
 | 
	
		
			
				|  |  | +        max_iter=collector.max_iterations,
 | 
	
		
			
				|  |  | +        func=lambda: collector.collect_and_store())
 |