class Device(): def __init__(self, host, port, timeout, byteorder=BE): # big_endian : Byte order of the device memory structure # True >> big endian # False >> little endian if byteorder == BE: self.big_endian=True else: self.big_endian=False self.dev = ModbusClient() self.dev.host(host) self.dev.port(port) self.dev.timeout(timeout) self.dev.open() #self.dev.debug = True #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ READ METHODS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# #Method to read binary variable def read_bits(self, VarNameList, AddressList, functioncode=2): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # functioncode : functioncode for modbus reading operation # 1 >> for Discrete Output (Coils) # 2 >> for Discrete Input # Return : dictionary of variable name and its value self.values = [] if functioncode == 1: for address in AddressList: self.values.extend(self.dev.read_coils(address[0], len(address))) elif functioncode == 2: for address in AddressList: self.values.extend(self.dev.read_discrete_inputs(address[0], len(address))) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read INT16 or UINT16 variable def read_INT16(self, VarNameList, AddressList, MultiplierList, signed=False, roundto=3, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # MultiplierList : list of multiplier # roundto : number of digits after decimal point # any positive integer number >> to limit the number of digits after decimal point # None >> to disable # signed : True >> for signed values # False >> for unsigned values # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: self.values.extend(self.dev.read_holding_registers(address[0],len(address))) elif functioncode == 4: for address in AddressList: self.values.extend(self.dev.read_input_registers(address[0],len(address))) if signed: self.values = UINT16toINT16(self.values) for i in range(0, len(self.values)): self.values[i] = round(self.values[i]*MultiplierList[i],roundto) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read INT32 or UINT32 variable def read_INT32(self, VarNameList, AddressList, MultiplierList, signed=False, roundto=3, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # MultiplierList : list of multiplier # roundto : number of digits after decimal point # any positive integer number >> to limit the number of digits after decimal point # None >> to disable # signed : True >> for signed values # False >> for unsigned values # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: self.values.extend(self.dev.read_holding_registers(address[0],len(address))) elif functioncode == 4: for address in AddressList: self.values.extend(self.dev.read_input_registers(address[0],len(address))) self.values = UINT16toINT32(self.values, self.big_endian, signed) for i in range(0, len(self.values)): self.values[i] = round(self.values[i]*MultiplierList[i], roundto) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read INT64 or UINT64 variable def read_INT64(self, VarNameList, AddressList, MultiplierList, signed=False, roundto=3, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # MultiplierList : list of multiplier # roundto : number of digits after decimal point # any positive integer number >> to limit the number of digits after decimal point # None >> to disable # signed : True >> for signed values # False >> for unsigned values # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: self.values.extend(self.dev.read_holding_registers(address[0],len(address))) elif functioncode == 4: for address in AddressList: self.values.extend(self.dev.read_input_registers(address[0],len(address))) self.values = UINT16toINT64(self.values, self.big_endian, signed) for i in range(0, len(self.values)): self.values[i] = round(self.values[i]*MultiplierList[i], roundto) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read FLOAT16 variable def read_FLOAT16(self, VarNameList, AddressList, MultiplierList, roundto=3, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # MultiplierList : list of multiplier # roundto : number of digits after decimal point # any positive integer number >> to limit the number of digits after decimal point # None >> to disable # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: self.values.extend(self.dev.read_holding_registers(address[0],len(address))) elif functioncode == 4: for address in AddressList: self.values.extend(self.dev.read_input_registers(address[0],len(address))) self.values = UINT16toFLOAT16(self.values) for i in range(0, len(self.values)): self.values[i] = round(self.values[i]*MultiplierList[i], roundto) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read FLOAT32 variable def read_FLOAT32(self, VarNameList, AddressList, MultiplierList, roundto=3, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # MultiplierList : list of multiplier # roundto : number of digits after decimal point # any positive integer number >> to limit the number of digits after decimal point # None >> to disable # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: self.values.extend(self.dev.read_holding_registers(address[0],len(address))) elif functioncode == 4: for address in AddressList: self.values.extend(self.dev.read_input_registers(address[0],len(address))) self.values = UINT16toFLOAT32(self.values, self.big_endian) for i in range(0, len(self.values)): self.values[i] = round(self.values[i]*MultiplierList[i], roundto) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read FLOAT64 variable def read_FLOAT64(self, VarNameList, AddressList, MultiplierList, roundto=3, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # MultiplierList : list of multiplier # roundto : number of digits after decimal point # any positive integer number >> to limit the number of digits after decimal point # None >> to disable # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: self.values.extend(self.dev.read_holding_registers(address[0],len(address))) elif functioncode == 4: for address in AddressList: self.values.extend(self.dev.read_input_registers(address[0],len(address))) self.values = UINT16toFLOAT64(self.values, self.big_endian) for i in range(0, len(self.values)): self.values[i] = round(self.values[i]*MultiplierList[i], roundto) self.Result = dict(zip(VarNameList, self.values)) return self.Result #Method to read STRING variable def read_STRING(self, VarNameList, AddressList, functioncode=3): # Arguments: # VarNameList : list of variable name # AddressList : list of variable register address in decimal (relative address) # functioncode : functioncode for modbus reading operation # 3 >> for Holding Register # 4 >> for Input Register # Return : dictionary of variable name and its value self.values = [] if functioncode == 3: for address in AddressList: _uint16Val = self.dev.read_holding_registers(address[0],len(address)) self.values.append(UINT16toSTRING(_uint16Val, self.big_endian)) elif functioncode == 4: for address in AddressList: _uint16Val = self.dev.read_input_registers(address[0],len(address)) self.values.append(UINT16toSTRING(_uint16Val, self.big_endian)) self.Result = dict(zip(VarNameList, self.values)) return self.Result #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WRITE METHODS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Method to write binary value on discrete output register (Coil) def write_bit(self, registerAddress, value): # Arguments: # registerAddress : register address in decimal (relative address) # value : 0 or 1 self.dev.write_single_coil(registerAddress, value) # Method to write numeric value on holding register def write_num(self, registerAddress, value, valueType): # Arguments: # registerAddress : register START address in decimal (relative address) # value : numerical value # valueType : UINT16, UINT32, UINT64, INT16, INT32, INT64, FLOAT16, # FLOAT32, FLOAT64, STRING startAddress = registerAddress val = None if valueType == UINT16: val = [value] elif valueType == INT16: val = INT16toUINT16([value]) elif valueType == UINT32: val = INT32toUINT16(value, self.big_endian, signed=False) elif valueType == INT32: val = INT32toUINT16(value, self.big_endian, signed=True) elif valueType == UINT64: val = INT64toUINT16(value, self.big_endian, signed=False) elif valueType == INT64: val = INT64toUINT16(value, self.big_endian, signed=True) elif valueType == FLOAT16: val = FLOAT16toUINT16([value]) elif valueType == FLOAT32: val = FLOAT32toUINT16(value, self.big_endian) elif valueType == FLOAT64: val = FLOAT64toUINT16(value, self.big_endian) elif valueType == STRING: val = STRINGtoUINT16(value, self.big_endian) # write multiple registers self.dev.write_multiple_registers(startAddress, val) def close(self): self.dev.close()
class Plugin(LoggerPlugin): """ Zeichnet die Messdaten einer Heliotherm-Wärmepumpe (mit RCG2) auf. Modbus-TCP muss aktiviert sein und die korrekte IP-Adresse eingetragen sein, damit dieses Gerät funktioniert. """ def __init__(self, *args, **kwargs): # Plugin setup super(Plugin, self).__init__(*args, **kwargs) self.loadConfig() self.setDeviceName(self._name) self.smallGUI = True self.activeGroups = self.loadPersistentVariable('activeGroups', [True]) self.__doc_activeGroups__ = """ Enthält eine Liste mit Funktionen, die die Wärmepumpe besitzt. Gültige Werte sind: True: Alle Standartmessdaten werden aufgezeichnet 'EVU': Alle EVU-Daten werden aufgezeichnet 'MKR1': Mischkreis 1 Daten werden aufgezeichnet 'MKR2': Mischkreis 2 Daten werden aufgezeichnet 'ext': Externe Daten werden aufgezeichnet '2teStufe': Zweite Stufe Daten werden aufgezeichnet """ self._availableGroups = ['EVU', 'MKR1', 'MKR2', 'ext', '2teStufe'] # Data-logger thread self._firstStart = True self._lastStoerung = False self._lastExtAnf = True self._lastMode = 0 self._heizkreis1Solltemperatur = 0 self._heizkreis2Solltemperatur = 0 # self._error = False self._modes = { 0: 'AUS', 1: 'Automatik', 2: 'Kühlen', 3: 'Sommer', 4: 'Dauerbetrieb', 5: 'Absenkung', 6: 'Urlaub', 7: 'Party', 8: 'Ausheizen', 9: 'EVU Sperre', 10: 'Hauptschalter aus' } self._base_address = "" self.setPerpetualTimer(self._helio_alle) self._base_address = self.host self._c = ModbusClient(host=self._base_address, port=self.port, auto_open=True, auto_close=True) self._c.timeout(10) self.start() def loadGUI(self): self.widget = QtWidgets.QWidget() packagedir = self.getDir(__file__) uic.loadUi(packagedir + "/Heliotherm/heliotherm.ui", self.widget) # self.setCallbacks() self.widget.pushButton.clicked.connect(self._openConnectionCallback) self.widget.samplerateSpinBox.valueChanged.connect( self._changeSamplerate) self.widget.comboBox.setCurrentText(self.host) # self._openConnectionCallback() return self.widget def _openConnectionCallback(self): if self.run: self.cancel() self.widget.pushButton.setText("Verbinden") self._base_address = "" else: address = self.widget.comboBox.currentText() self._base_address = address self._c = ModbusClient(host=self._base_address, port=self.port, auto_open=True, auto_close=True) self._c.timeout(10) self.start() self.widget.pushButton.setText("Beenden") def _helio_set(self, reg_addr, reg_value): for element in self._mappingWrite: if element[0] == reg_addr and element[5] == True: ans = self._c.write_single_register(reg_addr, reg_value) if ans == True: return True else: return False else: raise PermissionError raise KeyError def _Schreiben(self, reg_name, reg_value): """ Schreibe ein Register manuell. """ for element in self._mappingWrite: if element[1] == reg_name and element[5] == True: ans = self._c.write_single_register(element[0], reg_value) if ans == True: return "Wert wurde geändert" else: return "Wert konnte nicht geändert werden." else: return "Element darf nicht beschrieben werden." return "Element nicht gefunden" def Anschalten(self, modus=1): """ Schalte den Modus der Wärmepumpe um: 0= AUS, 1= Automatik, 2= Kühlen, 3= Sommer, 4= Dauerbetrieb, 5= Absenkung, 6=Urlaub, 7= Party, """ # 0= AUS # 1= Automatik # 2= Kühlen # 3= Sommer # 4= Dauerbetrieb # 5= Absenkung # 6=Urlaub # 7= Party if int(modus) in self._modes.keys() and int(modus) <= 7: return self._c.write_single_register(100, int(modus)) else: return 'Wähle einen Modus zwischen 0 und 7\n' + str(self._modes) def MKR1Schreiben(self, modus=1): """ Schalte den Modus des MKR1 um: 0= AUS, 1= Automatik, 2= Kühlen, 3= Sommer, 4= Dauerbetrieb, 5= Absenkung, 6=Urlaub, 7= Party, """ # 0= AUS # 1= Automatik # 2= Kühlen # 3= Sommer # 4= Dauerbetrieb # 5= Absenkung # 6=Urlaub # 7= Party if int(modus) in self._modes.keys() and int(modus) <= 7: return self._c.write_single_register(107, int(modus)) else: return 'Wähle einen Modus zwischen 0 und 7\n' + str(self._modes) def MKR2Schreiben(self, modus=1): """ Schalte den Modus des MKR2 um: 0= AUS, 1= Automatik, 2= Kühlen, 3= Sommer, 4= Dauerbetrieb, 5= Absenkung, 6=Urlaub, 7= Party, """ # 0= AUS # 1= Automatik # 2= Kühlen # 3= Sommer # 4= Dauerbetrieb # 5= Absenkung # 6=Urlaub # 7= Party if int(modus) in self._modes.keys() and int(modus) <= 7: return self._c.write_single_register(112, int(modus)) else: return 'Wähle einen Modus zwischen 0 und 7\n' + str(self._modes) def MKR1Automatik(self): """ Schalte MKR1 auf Automatik """ return self.MKR1Schreiben(modus=1) def MKR2Automatik(self): """ Schalte MKR2 auf Automatik """ return self.MKR2Schreiben(modus=1) def MKR1Aus(self): """ Schalte MKR1 aus """ return self.MKR1Schreiben(modus=0) def MKR2Aus(self): """ Schalte MKR2 aus """ return self.MKR1Schreiben(modus=0) def MKR1Kuehlen(self): """ Schalte MKR1 auf Kühlen """ return self.MKR1Schreiben(modus=2) def MKR2Kuehlen(self): """ Schalte MKR2 auf Kühlen """ return self.MKR1Schreiben(modus=2) def MKR1Absenkung(self): """ Schalte MKR1 auf Absenkung """ return self.MKR1Schreiben(modus=5) def MKR2Absenkung(self): """ Schalte MKR2 auf Absenkung """ return self.MKR1Schreiben(modus=5) def Ausschalten(self): """ Schalte die Wärmepumpe aus """ return self._c.write_single_register(100, 0) @property def Heizkreis1Solltemperatur(self): return self._heizkreis1Solltemperatur @Heizkreis1Solltemperatur.setter def Heizkreis1Solltemperatur(self, value): reg_addr = 108 try: value = float(value) except: pass if value > 0: self._helio_set(reg_addr, value) self._heizkreis1Solltemperatur = value @property def Heizkreis2Solltemperatur(self): return self._heizkreis2Solltemperatur @Heizkreis2Solltemperatur.setter def Heizkreis2Solltemperatur(self, value): reg_addr = 113 try: value = float(value) except: pass if value > 0: self._helio_set(reg_addr, value) self._heizkreis2Solltemperatur = value def _helio_alle(self, all=False): readStart = 10 readEnd = 75 resultRead = self._c.read_input_registers(readStart, readEnd - readStart + 1) writeStart = 100 writeEnd = 159 resultWrite = self._c.read_holding_registers(writeStart, writeEnd - writeStart + 1) if type(resultWrite) != list: writeStart = 100 writeEnd = 134 resultWrite = self._c.read_holding_registers( writeStart, writeEnd - writeStart + 1) ans = {} if type(resultWrite) == list: for idx, value in enumerate(self._mappingWrite): if idx >= len(resultWrite): break if len(self._mappingWrite[idx]) == 6: if self._mappingWrite[idx][4] in self.activeGroups or all: sname = self._mappingWrite[idx][1] divisor = self._mappingWrite[idx][2] unit = self._mappingWrite[idx][3] y = resultWrite[idx] if y >= 2**16 / 2: y = 2**16 - y y = y / divisor ans[sname] = [y, unit] if self._mappingWrite[idx][0] == 100: if not self._firstStart and y != self._lastMode: mode = self._modes[y] self.event( 'Betriebsart wurde verändert: {}'.format( mode), 'Betriebsart', self._name, 0, 'ModusChanged') self._lastMode = y elif self._mappingWrite[idx][0] == 108: self._heizkreis1Solltemperatur = y elif self._mappingWrite[idx][0] == 113: self._heizkreis2Solltemperatur = y elif self._mappingWrite[idx][0] == 127: if not self._firstStart and y != self._lastExtAnf: if y == 1: self.event( 'Externe Anforderung angeschaltet', 'Externe Anforderung', self._name, 0, 'ExtAnfAn') else: self.event( 'Externe Anforderung ausgeschaltet', 'Externe Anforderung', self._name, 0, 'ExtAnfAus') self._lastExtAnf = y else: pass # handle parameters else: logging.warning( 'Could not read writeable-registers, {}'.format(resultWrite)) if self.widget: self.widget.comboBox.setCurrentText('Fehler') if type(resultRead) == list: for idx, value in enumerate(self._mappingRead): if self._mappingRead[idx][4] in self.activeGroups or all: if len(self._mappingRead[idx] == 5): sname = self._mappingRead[idx][1] divisor = self._mappingRead[idx][2] unit = self._mappingRead[idx][3] y = resultRead[idx] if y >= 2**16 / 2: y = 2**16 - y y = y / divisor ans[sname] = [y, unit] if self._mappingRead[idx][0] == 26: if not self._firstStart and y != self._lastStoerung: if y != 0: self.event( 'Störung aufgetreten: {}!'.format(y), 'Stoerung', self._name, 2, 'StoerungAn') else: self.event('Störung wurde behoben!', 'Stoerung', self._name, 0, 'StoerungAus') self._lastStoerung = y else: pass # handle parameters self._firstStart = False else: logging.warning( 'Could not read read-registers, {}'.format(resultRead)) if self.widget: self.widget.comboBox.setCurrentText('Fehler') self.stream(sdict={self._name: ans}) def _changeSamplerate(self): self.samplerate = self.widget.samplerateSpinBox.value() def loadConfig(self): packagedir = self.getDir(__file__) with open(packagedir + "/config.json", encoding="UTF-8") as jsonfile: config = json.load(jsonfile, encoding="UTF-8") self._mappingWrite = config['mappingWrite'] self._mappingRead = config['mappingRead'] self.host = config['hostname'] self.port = config['port'] self._name = config['name'] self.samplerate = config['samplerate']
class Plugin(LoggerPlugin): """ Zeichnet die Messdaten einer Heliotherm-Wärmepumpe (mit Remote Control Gateway X SERIES EL-005-0001 ab v1.0.3.2 - 10.12.2018) auf. Modbus-TCP muss im Webinterface der Wärmepumpe aktiviert sein und im Anschluss die IP-Adresse der Wärmepumpe im Parameter "IP_Adresse" eingetragen werden. """ def __init__(self, *args, **kwargs): # Plugin setup super(Plugin, self).__init__(*args, **kwargs) self.setDeviceName('Heliotherm') self._connected = False self.__doc_connected__ = "Zeigt an, ob eine Wärmepumpe verbunden ist" self.status = "" self.__doc_status__ = "Zeigt Verbindungs-Informationen an, falls vorhanden" self._firstStart = True self._lastStoerung = False self._lastExtAnf = True self._lastMode = 0 self._c = None # Initialize persistant attributes self.IP_Adresse = self.initPersistentVariable('IP_Adresse', '') self.__doc_IP_Adresse__ = "IP-Adresse des RCG2 der Heliotherm-Wärmepumpe" self.samplerate = self.initPersistentVariable('samplerate', 1) self.__doc_samplerate__ = "Aufzeichnungsrate in Herz" self.Port = self.initPersistentVariable('Port', 502) self.__doc_Port__ = "ModbusTCP-Port (Standart: 502)" self._groups = self.initPersistentVariable('_groups', {'Global': True}) # Load mapping.json file self._groupInfo = {} self._mappingWrite = [] self._mappingRead = [] self._loadMapping() # Update self._groups for group in self._groupInfo.keys(): if group not in self._groups.keys(): self._groups[group] = False self._createGroupAttributes(self._groups) # Create attributes for self._createAttributes(self._mappingWrite) self.connect() @property def connected(self): return self._connected @connected.setter def connected(self, value): if value is True: self.connect() elif value is False: self.disconnect() # self._connected = value def _loadMapping(self): packagedir = self.getDir(__file__) with open(packagedir+"/mapping.json", encoding="UTF-8") as jsonfile: mapping = json.load(jsonfile, encoding="UTF-8") self._groupInfo = mapping['groups'] self._mappingWrite = mapping['mappingWrite'] self._mappingRead = mapping['mappingRead'] def _createAttributes(self, mappingWrite): groups = self._activeGroups() for parameter in mappingWrite: if parameter['group'] in groups: # Replace invalid chars for attribute names name = self._formatAttributeName(parameter['sname']) # Create 'private' parameter with _ in front: self._name setattr(self.__class__, '_'+name, None) # This intermediate function handles the strange double self arguments def setter(self, name, parameter, selfself, value): self._setAttribute(name, value, parameter) # Make sure, that the parameters are "the values, which would be represented at this point with print(...)" setpart = partial(setter, self, name, parameter) getpart = partial(getattr, self, '_'+name) # Create an attribute with the actual name. The getter refers to it's self._name attribute. The setter function to self._setGroupAttribute(name, value, parameter) setattr(self.__class__, name, property(getpart, setpart)) # If parameter contains docs, create a third attribute at self.__doc_PARNAME__ if "doc" in parameter.keys(): setattr(self, '__doc_'+name+'__', parameter['doc']) def _activeGroups(self): groups = [] for group in self._groups.keys(): if self._groups[group] is True: groups.append(group) return groups def _createGroupAttributes(self, groups): for groupname in groups.keys(): # Replace invalid chars for attribute names name = 'Gruppe_'+self._formatAttributeName(groupname) # Create 'private' parameter with _ in front: self._name setattr(self.__class__, '_'+name, groups[groupname]) # This intermediate function handles the strange double self arguments def setter(self, name, parameter, selfself, value): self._setGroupAttribute(name, value, parameter) # Make sure, that the parameters are "the values, which would be represented at this point with print(...)" setpart = partial(setter, self, name, groupname) getpart = partial(getattr, self, '_'+name) # Create an attribute with the actual name. The getter refers to it's self._name attribute. The setter function to self._setAttribute(name, value, parameter) setattr(self.__class__, name, property(getpart, setpart)) setattr(self, '__doc_'+name+'__', self._groupInfo[groupname]) def _setAttribute(self, name, value, parameter): if self._c is None: return if parameter['write'] is False: self.warning('This parameter cannot be changed') self.status = 'This parameter cannot be changed' else: ok = self._writeModbusRegister(parameter, value) if ok: self.info('Wrote register in WP') self.status = 'Wrote register in WP' else: self.error('Failed to write register in WP') self.status = 'Failed to write register in WP' def _setAttributeRef(self, name, value, parameter): name = self._formatAttributeName(name) setattr(self, '_'+name, value) def _formatAttributeName(self, name): return name.replace(' ','_').replace('/','_').replace('.','_').replace('-','_') def _setGroupAttribute(self, name, value, groupname): name = self._formatAttributeName(name) setattr(self, '_'+name, value) self._groups[groupname] = value self.savePersistentVariable('_groups', self._groups) def connect(self, ip=None): """ Verbinde dich mit einer Heliotherm Wärmepumpe (mit RCG2). Gib dazu die IP-Adresse/den Hostnamen an. Wenn die IP-Adresse schonmal konfiguriert wurde, lasse das Feld einfach leer. """ if ip != None and ip != '' and type(ip) is str: self.IP_Adresse = ip self.savePersistentVariable('IP_Adresse', self.IP_Adresse) self.info('Connecting with {}'.format(self.IP_Adresse)) if not self._connected: try: self.setPerpetualTimer(self._readModbusRegisters, samplerate=self.samplerate) self._c = ModbusClient(host=self.IP_Adresse, port=self.Port, auto_open=True, auto_close=True) if self._c is None: self.error('Could not connect with {}'.format(self.IP_Adresse)) self.status = 'Could not connect with {}'.format(self.IP_Adresse) return False self._c.timeout(10) self.status = 'Trying to connect...' self.start() self._connected = True return True except Exception as e: self.error('Could not connect with {}:\n{}'.format(self.IP_Adresse, e)) self.status = 'Could not connect with {}:\n{}'.format(self.IP_Adresse, e) return False else: self.status = 'Already connected. Disconnect first' return False def disconnect(self): """ Trenne die Verbindung zu der Heliotherm Wärmepumpe """ if self._connected: self.cancel() self._connected = False self._c = None self.status = 'Successfully disconnected' return True else: self.status = 'Cannot disconnect. Not connected.' return False def _mappingMinMaxRegister(self, mapping): min = 9999999999999 max = 0 for parameter in mapping: if parameter['register']> max: max = parameter['register'] if parameter['register']< min: min = parameter['register'] return min, max def _readModbusRegisters(self): if self._c == None: return active_groups = self._activeGroups() writeStart=100 writeEnd=159 resultWrite = self._c.read_holding_registers(writeStart, writeEnd-writeStart+1) if type(resultWrite) != list: writeStart=100 writeEnd=134 resultWrite = self._c.read_holding_registers(writeStart, writeEnd-writeStart+1) ans = {} if type(resultWrite) == list: for parameter in self._mappingWrite: if parameter['group'] in active_groups: regIdx= parameter['register']-writeStart if regIdx >= len(resultWrite): continue writeableValue = resultWrite[regIdx] if writeableValue>=2 **16/2: writeableValue = 2 **16 - writeableValue writeableValue = writeableValue/parameter['scale'] if parameter['record']: ans[parameter['sname']]=[writeableValue, parameter['unit']] # if parameter['write'] is True: self._setAttributeRef(self._formatAttributeName(parameter['sname']), writeableValue, parameter) if parameter['sname'] == "Betriebsart": if not self._firstStart and writeableValue != self._lastMode: mode = self._modes[writeableValue] self.event('Betriebsart wurde verändert: {}'.format(mode),parameter['sname'],self._devicename, 0, 'ModusChanged') self._lastMode = writeableValue elif parameter['sname'] == "Externe Anforderung": if not self._firstStart and writeableValue != self._lastExtAnf: if writeableValue == 1: self.event('Externe Anforderung angeschaltet',parameter['sname'],self._devicename, 0, 'ExtAnfAn') else: self.event('Externe Anforderung ausgeschaltet',parameter['sname'],self._devicename, 0, 'ExtAnfAus') self._lastExtAnf = writeableValue else: self.warning('Could not read writeable-registers, {}'.format(resultWrite)) self.status = 'Could not read writeable-registers, {}'.format(resultWrite) readStart=10 readEnd=75 resultRead = self._c.read_input_registers(readStart, readEnd-readStart+1) if type(resultRead) == list: for parameter in self._mappingRead: if parameter['group'] in active_groups: regIdx= parameter['register']-readStart if regIdx >= len(resultRead): continue writeableValue = resultRead[regIdx] if writeableValue>=2 **16/2: writeableValue = 2 **16 - writeableValue writeableValue = writeableValue/parameter['scale'] if parameter['record']: ans[parameter['sname']]=[writeableValue, parameter['unit']] if parameter['sname'] == 'Stoerung': if not self._firstStart and writeableValue != self._lastStoerung: if writeableValue != 0: self.event('Störung aufgetreten: {}!'.format(writeableValue),parameter['sname'],self._devicename, 2, 'StoerungAn') else: self.event('Störung wurde behoben!',parameter['sname'],self._devicename, 0, 'StoerungAus') self._lastStoerung = writeableValue self._firstStart = False self.status = 'Connected' else: self.warning('Could not read read-registers, {}'.format(resultRead)) self.status = 'Could not read read-registers, {}'.format(resultRead) self.stream(sdict={self._devicename:ans}) def _writeModbusRegister(self, parameter, reg_value): # if str(reg_addr) in self._mappingWrite.keys(): ans = self._c.write_single_register(parameter['register'],reg_value*parameter['scale']) if ans == True: return True else: return False
from pyModbusTCP.client import ModbusClient from pyModbusTCP.constants import MODBUS_RTU from config.modbus import * from time import sleep ### MODBUS INITIALIZATION c = ModbusClient() c.mode(MODBUS_RTU) # enable MODBUS_RTU mode c.timeout(1) c.debug(True) c.auto_open(True) c.auto_close(True) reg_value={'dum':None} # DUMMY FOR RECORDING REGISTRY LISTS ### END MODBUS INITIALIZATION ### INTERNAL FUNCTIONS def loc_id(loc): device=loc.split('.') modmap,conn_id = MODBUS_MAP[device[2]],CONNECTIONS[device[0]][device[1]] return modmap,conn_id # [slave_id,reg_values],[IP_addr,port,slave_id_modifier] def reg_add(modmap,color): reg=[0x0,0x0,0x0] for cl in COLOR_LIST[color]: mapcol=modmap[1][cl] # [0xXXXX,0xXXXX,0xXXXX] for i in range(0,3): reg[i]+=mapcol[i] return reg def open_comm(): # MODBUS AUTO-RECONNECT
class CC_class: """Charge Controller Class""" def __init__(self, verbose_mode, name, oem_series, ip_addr, port, tcp_timeout, expected_max_power = None): """Constructor verbose_mode param description: used to enable all methods to annunciation during runtime (see also local_verbose_mode arg in each method) type: bool oem_series param description: sets which OEM and Series of Charge Controller self is type: string name param description: used as a simple name to describe the charge controller type: string ip_addr param description: IP Address of Charge Controller type: string port param description: Port of Server application running on Charge Controller (502 default for Midnite Classics) type: string tcp_timeout param description: Timeout, in milliseconds, for TCP connection type: float expected_max_power param description: Expected Maximum Input power for system type: int """ #Init self variables with constructor params self.verbose_mode = verbose_mode self.name = name self.oem_series = oem_series self.ip_addr = ip_addr self.port = port self.tcp_timeout = tcp_timeout self.expected_max_power = expected_max_power #Init Internals self.TCP_Mod_Client = ModbusClient() self.dataread_DateTime_stamp = datetime.datetime.now() #Image of controller read data (Note: for future dev, this should be wrapped up in seperate class) self.conn_state = None self.MAC_addr = None self.Date_Time = None self.Unit_Type = None self.Serial = None self.FW_Rev = None self.FW_Build = None self.HW_Rev = None self.PCB_Temperature = None self.FET_Temperature = None self.PCB_Temperature_DegF = None self.FET_Temperature_DegF = None self.Charge_Stage = None self.Charge_Stage_Message = None self.Last_VoC_Measured = None self.Highest_InputV_Logged = None self.InfoData_Flags = None self.InfoData_FlagsBin = None self.Fault_FlagsBin = None self.Alarm_FlagsBin = None self.KWh_Lifetime = None self.Ahr_Lifetime = None self.MPPT_Mode = None self.MPPT_Mode_Message = None self.Aux1_Function = None self.Aux2_Function = None self.Input_Voltage = None self.Input_Current = None self.Input_Watts = None self.Output_Voltage = None self.Output_Current = None self.Output_Watts = None self.KWh = None self.Ahr = None self.Shunt_Installed = None self.Shunt_Temperature = None self.Shunt_Temperature_DegF = None self.Shunt_Current = None self.Shunt_Net_Ahr = None self.Battery_Voltage = None self.Battery_SOC = None self.Battery_Temperature = None self.Battery_Temperature_DegF = None self.Battery_Remaining = None self.Battery_Capacity = None self.Conf_Nominal_Voltage = None self.Conf_Absorb_Setpoint = None self.Conf_Absorb_Time = None self.Conf_End_Amps = None self.Conf_Float_Setpoint = None self.Conf_EQ_Setpoint = None self.Conf_EQ_Time = None self.Conf_MaxTemp_Comp = None self.Conf_MinTemp_Comp = None self.Conf_TempComp_Value = None self.Calced_CC_Eff = None self.Calced_Load_Current = None self.Calced_Load_Power = None self.Calced_TimeTo_Batt50 = None self.Calced_Batt_50Capacity = None self.Calced_Batt_FreezeEstimate = None self.Calced_Batt_FreezeEstimate_DegF = None #Accumulated Values self.Absorb_Time_Count = None self.Float_Time_Count = None #Peaks and other captured events self.Peak_Input_Watts = "0" self.Peak_Output_Watts = "0" self.Peak_Output_Voltage = "0" self.Peak_Output_Current = "0" self.Peak_CC_Temperature = "0" self.Peak_CC_Temperature_DegF = "0" self.Max_Batt_Temperature = "-999" self.Min_Batt_Temperature = "999" self.Max_Batt_Temperature_DegF = "-999" self.Min_Batt_Temperature_DegF = "999" self.Max_Batt_Voltage = "0" self.Min_Batt_Voltage = "999" self.Event_Absorb_Reached = "No" self.Event_TimeIn_Absorb = "0" self.Event_Float_Reached = "No" self.Event_TimeIn_Float = "0" #------------------------------------------------------------------------------------------------------------------------------ def connect (self, UI=None, local_verbose_vode=None): #Annunciate if self.verbose_mode or local_verbose_mode: print("Attempting connection to "+self.name+" at "+self.ip_addr+":"+self.port+"......") #Transfer connection data to Image area self.conn_state = 10 #0 = not connected, 1 = connected, 10 = attempting connection if UI and Valid_Dependancy: CC_To_UI_Exchange(self, UI) #Set TCP_Mod_Client params self.TCP_Mod_Client.host(self.ip_addr) self.TCP_Mod_Client.port(self.port) self.TCP_Mod_Client.timeout(self.tcp_timeout) #Connect and return state #Also Update UI object on the fly, if class is being used in CCDM (allows stand alone us of this class) #If connection is not open, try to connect------ if not self.TCP_Mod_Client.is_open(): if self.TCP_Mod_Client.open(): if self.verbose_mode or local_verbose_mode: print(" -> Connected") self.conn_state = 1 #Update UI if UI and Valid_Dependancy: CC_To_UI_Exchange(self, UI) return self.conn_state else: if self.verbose_mode or local_verbose_mode: print(" -> TCP-Modbus client could not connect to server") #blank all internal data associated with read self.blank_cc_image() self.conn_state = 0 #Update UI if UI and Valid_Dependancy: CC_To_UI_Exchange(self, UI) return self.conn_state #If connection is already open, annunciate--------- elif self.TCP_Mod_Client.is_open(): if self.verbose_mode or local_verbose_mode: print(" -> Connected") self.conn_state = 1 #Update UI if UI and Valid_Dependancy: CC_To_UI_Exchange(self, UI) return self.conn_state #------------------------------------------------------------------------------------------------------------------------------ def disconnect (self, local_verbose_vode=None): if self.verbose_mode or local_verbose_mode: print("Disconnecting from "+self.name) self.TCP_Mod_Client.close() reset_conn_state() #------------------------------------------------------------------------------------------------------------------------------ def reset_conn_state(): self.conn_state = 0 return self.conn_state #------------------------------------------------------------------------------------------------------------------------------ def read_data (self, local_verbose_mode=None): if self.verbose_mode or local_verbose_mode: print("Gathering data from "+self.name) #Begin reading and storing data based on configured Controller OEM and Type #------------------------------ Midnite Solar, Clssic Type Charge Controller ----------------------------------- if self.oem_series == "Midnite - Classic": #NOTE: Reg address and actual address are offset by 1 (EXAMPLE: to access Reg4101 you must read at address value 4100) #Holding Reg4101-4103 = PCB Rev(MSB) / Unit Type (LSB), Software Build Year, Software Build Month (MSB) Software Build Day (LSB) incoming_data = self.TCP_Mod_Client.read_holding_registers(4100, 3) if incoming_data: self.HW_Rev = str(incoming_data[0] & 0xFF00 >> 8) self.Unit_Type = "Classic "+str(incoming_data[0] & 0x00FF) self.FW_Rev = str(incoming_data[2] >> 8).zfill(2)+"/"+str((incoming_data[2] & 0x00FF))+"/"+str(incoming_data[1]) #Assembled to M/D/Year (Build level will be added below) #Holding Reg4106-4108 = MAC Address incoming_data = self.TCP_Mod_Client.read_holding_registers(4105, 3) if incoming_data: MAC1 = (str(hex((incoming_data[2] & 0xFF00)>>8))) MAC1 = MAC1[2:].zfill(2) MAC2 = (str(hex((incoming_data[2] & 0x00FF)))) MAC2 = MAC2[2:].zfill(2) MAC3 = (str(hex((incoming_data[1] & 0xFF00)>>8))) MAC3 = MAC3[2:].zfill(2) MAC4 = (str(hex((incoming_data[1] & 0x00FF)))) MAC4 = MAC4[2:].zfill(2) MAC5 = (str(hex((incoming_data[0] & 0xFF00)>>8))) MAC5 = MAC5[2:].zfill(2) MAC6 = (str(hex((incoming_data[0] & 0x00FF)))) MAC6 = MAC6[2:].zfill(2) #Concat the above results self.MAC_addr = MAC1.upper()+":"+MAC2.upper()+":"+MAC3.upper()+":"+MAC4.upper()+":"+MAC5.upper()+":"+MAC6.upper() #.upper() method used to convert all letters to uppercase else: print(" -> Error Reading Reg4106-4108 block of "+self.name) #Holding Reg4115-4119 = Battery Voltage, CC Input Voltage, Battery Current, KWh, Output Power incoming_data = self.TCP_Mod_Client.read_holding_registers(4114, 5) if incoming_data: self.Output_Voltage = str(incoming_data[0] / 10) self.Battery_Voltage = self.Output_Voltage self.Input_Voltage = str(incoming_data[1] / 10) self.Output_Current = str(incoming_data[2] / 10) self.KWh = str(incoming_data[3] / 10) self.Output_Watts = str(incoming_data[4]) else: print(" -> Error Reading Reg4115-4119 block of "+self.name) #Holding Reg4120-4123 = Charge Stage, Input Current, VoC Last Measured, HighestV Input Log incoming_data = self.TCP_Mod_Client.read_holding_registers(4119, 4) if incoming_data: self.Charge_Stage = str((incoming_data[0] & 0xFF00) >> 8) #Develop actual charge stage message based upon value if self.Charge_Stage: self.Charge_Stage_Message = self.set_chargestage_msg(self.Charge_Stage) self.Input_Current = str(incoming_data[1] / 10) self.Last_VoC_Measured = str(incoming_data[2] / 10) self.Highest_InputV_Logged = str(incoming_data[3]) else: print(" -> Error Reading Reg4120-4123 block of "+self.name) #Holding Reg4125-4129 = Charge Amp Hours, Lifetime KWh (2 bytes), Lifetime Ahr (2 bytes) incoming_data = self.TCP_Mod_Client.read_holding_registers(4124, 5) if incoming_data: self.Ahr = str(incoming_data[0]) self.KWh_Lifetime = str(incoming_data[1] + (incoming_data[2] << 16)) self.Ahr_Lifetime = str(incoming_data[3] + (incoming_data[4] <<16)) else: print(" -> Error Reading Reg4125-4129 block of "+self.name) #Holding Reg4130-4134 = Info Data Flags (2 bytes), Battery Temperature, FET Temperature, PCB Temperature incoming_data = self.TCP_Mod_Client.read_holding_registers(4129, 5) if incoming_data: self.InfoData_FlagsBin = str('{0:032b}'.format( int(incoming_data[0] + (int(incoming_data[1]) << 16 )))) self.InfoData_Flags = str(hex(int(incoming_data[0] + (int(incoming_data[1]) << 16)))).translate(str.maketrans("abcdef", "ABCDEF")) #Gather Various Temperatures, sign and also store F values from C self.Battery_Temperature = str(float(int_to_signed(incoming_data[2]) / 10)) self.Battery_Temperature_DegF = str(DegC_to_DegF(float(self.Battery_Temperature))) self.FET_Temperature = str(float(int_to_signed(incoming_data[3]) / 10)) self.PCB_Temperature = str(float(int_to_signed(incoming_data[4]) / 10)) self.FET_Temperature_DegF = str(DegC_to_DegF(float(self.FET_Temperature))) self.PCB_Temperature_DegF = str(DegC_to_DegF(float(self.PCB_Temperature))) else: print(" -> Error Reading Reg4130-4134 block of "+self.name) #Holding Reg4138-4139 = Time spent in Float, Time spent in Absorb incoming_data = self.TCP_Mod_Client.read_holding_registers(4137, 2) if incoming_data: self.Float_Time_Count = str(incoming_data[0] / 3600) #Convert from seconds to hours self.Absorb_Time_Count = str(incoming_data[1] / 3600) #Convert from seconds to hours else: print(" -> Error Reading Reg4138-4139 block of "+self.name) #Holding Reg4149-4151 = Absorb Voltage SP, Float Voltage SP, EQ Voltage SP incoming_data = self.TCP_Mod_Client.read_holding_registers(4148, 3) if incoming_data: self.Conf_Absorb_Setpoint = str(incoming_data[0] / 10) self.Conf_Float_Setpoint = str(incoming_data[1] / 10) self.Conf_EQ_Setpoint = str(incoming_data[2] / 10) else: print(" -> Error Reading Reg4149-4151 block of "+self.name) #Holding Reg4154-4157 = Absorb Time, Max Temp Compensation , Min Temp Compensation, Temperature Compensation Value incoming_data = self.TCP_Mod_Client.read_holding_registers(4153, 4) if incoming_data: self.Conf_Absorb_Time = str(incoming_data[0] / 3600) #Convert from seconds to hours self.Conf_MaxTemp_Comp = str(incoming_data[1] / 10) self.Conf_MinTemp_Comp = str(incoming_data[2] / 10) self.Conf_TempComp_Value = str(incoming_data[3] / 10) else: print(" -> Error Reading Reg4154-4157 block of "+self.name) #Holding Reg4162-4165 = EQ Time, EQ Interval, MPPT Mode, Aux12 Function #(Check for Shunt (WhizBangJr)) incoming_data = self.TCP_Mod_Client.read_holding_registers(4161, 4) if incoming_data: self.Conf_EQ_Time = str(incoming_data[0] / 3600) #Convert from seconds to hours #->Placeholder for adding EQ Interval self.MPPT_Mode = str(incoming_data[2]) #Develop MPPT Mode message based upon value if self.MPPT_Mode != None: self.MPPT_Mode_Message = self.set_mpptmode_msg(self.MPPT_Mode) self.Aux1_Function = incoming_data[3] & 0x003F #Bits 0-5 self.Aux2_Function = (incoming_data[3] & 0x3F00) >> 8 #Bits 8-13 if self.Aux2_Function == 18: #Configured as WhizBangJR read self.Shunt_Installed = True else: self.Shunt_Installed = False else: print(" -> Error Reading Reg4162-4165 block of "+self.name) #Holding Reg4245-4246 = VbatNominal (12, 24, etc), Ending Amps incoming_data = self.TCP_Mod_Client.read_holding_registers(4244, 2) if incoming_data: self.Conf_Nominal_Voltage = str(incoming_data[0]) self.Conf_End_Amps = str(incoming_data[1] / 10) else: print(" -> Error Reading Reg4245-4246 block of "+self.name) #NOTE the following are only applicable if Shunt is found (WhizBangJr) if self.Shunt_Installed: #Holding Reg4369-4370 = Net Amp Hours incoming_data = self.TCP_Mod_Client.read_holding_registers(4368, 3) if incoming_data: #Assemble and Calculate Net Amp Hours self.Shunt_Net_Ahr = str(float(int_to_signed32((incoming_data[1] << 16) + incoming_data[0]))) else: print(" -> Error Reading Reg4368-4369 block of "+self.name) #Holding Reg4371-4373 = WhizBangJR (Shunt) Current, WhizBangJr (Shunt) CRC (MSB) and Temperature (LSB), SoC % incoming_data = self.TCP_Mod_Client.read_holding_registers(4370, 3) if incoming_data: #Convert shunt current to signed value self.Shunt_Current = str(float(int_to_signed(incoming_data[0]) / 10)) self.Shunt_Temperature = str(((incoming_data[1] & 0xFF00 >>8) - 50)) self.Shunt_Temperature_DegF = str(DegC_to_DegF(float(self.Shunt_Temperature))) self.Battery_SOC = str(incoming_data[2]) else: print(" -> Error Reading Reg4370-4373 block of "+self.name) #Holding Reg4377 = Battery remaining Ahrs incoming_data = self.TCP_Mod_Client.read_holding_registers(4376, 1) if incoming_data: self.Battery_Remaining = str(incoming_data[0]) else: print(" -> Error Reading Reg4377 of "+self.name) #Holding Reg4381 = Battery Capacity Ahrs incoming_data = self.TCP_Mod_Client.read_holding_registers(4380, 1) if incoming_data: self.Battery_Capacity = str(incoming_data[0]) else: print(" -> Error Reading Reg4381 of "+self.name) #Holding Reg16385-16390 = Release Version, Net Version, Release Build (2 regs), Net Build (2 regs) incoming_data = self.TCP_Mod_Client.read_holding_registers(16384, 6) if incoming_data: self.FW_Build = str(incoming_data[2] + (incoming_data[3] << 16)) else: print(" -> Error Reading Reg16385-16390 block of "+self.name) #Assembled Release Ver and Build, final format will be: Build:m/d/year if self.FW_Rev != None and self.FW_Build !=None: self.FW_Rev = self.FW_Build+":"+self.FW_Rev #Holding Reg28673-28674 = Serial (2 regs) incoming_data = self.TCP_Mod_Client.read_holding_registers(28672, 2) if incoming_data: self.Serial = "CL"+str(incoming_data[1] + (incoming_data[0] << 16)) else: print(" -> Error Reading Reg28673-28674 block of "+self.name) #---------------------------- Tristar, MPPT 60 Type Charge Controller ---------------------------------------- elif self.oem_series == "Tristar - MPPT 60": #Call MPPT Mode method to set mode (static in these controllers) self.MPPT_Mode = 0x000B self.MPPT_Mode_Message = self.set_mpptmode_msg(0) #Set Shunt_Installed false as this option does not exist on Tristar MPPT Charge Controllers self.Shunt_Installed = False #Holding 0x0000-0x0004 = V_PU hi, V_PU lo, I_PU hi, I PU lo, Software Version incoming_data = self.TCP_Mod_Client.read_holding_registers(0, 5) if incoming_data: #Scaling data for Voltage and Current calcs V_PU_hi = incoming_data[0] V_PU_lo = incoming_data[1] I_PU_hi = incoming_data[2] I_PU_lo = incoming_data[3] self.FW_Rev = str(incoming_data[4]) else: print(" -> Error Reading 0x0000-0x0004 block of "+self.name) #Holding 0x0018-0x001D = Batt. Voltage Filtered, Batt. Term Voltage Filtered, Batt. Sense Voltage Filtered, Array Voltage Filtered, Batt. Current Filtered, Array Current Filtered incoming_data = self.TCP_Mod_Client.read_holding_registers(24, 6) if incoming_data: self.Output_Voltage = str(self.tristar_scaling(V_PU_hi, V_PU_lo, incoming_data[1])) #Voltage at CC Output terminals self.Battery_Voltage = str(self.tristar_scaling(V_PU_hi, V_PU_lo, incoming_data[2])) #Voltage at battery sense inputs self.Input_Voltage = str(self.tristar_scaling(V_PU_hi, V_PU_lo, incoming_data[3])) self.Output_Current = str(self.tristar_scaling(I_PU_hi, I_PU_lo, incoming_data[4])) self.Input_Current = str(self.tristar_scaling(I_PU_hi, I_PU_lo, incoming_data[5])) #Calc Nominal voltage as this is not software stored in Tristar if float(self.Output_Voltage) > 9.0 and float(self.Output_Voltage) < 17.0: self.Conf_Nominal_Voltage = "12" elif float(self.Output_Voltage) > 18.0 and float(self.Output_Voltage) < 34.0: self.Conf_Nominal_Voltage = "24" elif float(self.Output_Voltage) > 36.0 and float(self.Output_Voltage) < 68.0: self.Conf_Nominal_Voltage = "48" else: self.Conf_Nominal_Voltage = "??" else: print(" -> Error Reading 0x0018-0x001D block of "+self.name) #Holding 0x0023-0x0025 = Heatsink Temperature, RTS Temperature, Battery Temperature incoming_data = self.TCP_Mod_Client.read_holding_registers(35, 3) if incoming_data: self.PCB_Temperature = str(incoming_data[0]) self.PCB_Temperature_DegF = str(DegC_to_DegF(float(self.PCB_Temperature))) self.Battery_Temperature = str(incoming_data[2]) self.Battery_Temperature_DegF = str(DegC_to_DegF(float(self.Battery_Temperature))) else: print(" -> Error Reading 0x0023-0x0025 block of "+self.name) #Holding 0x002C-0x002F = Fault Bits, Alarm Bits hi, Alarm Bits lo incoming_data = self.TCP_Mod_Client.read_holding_registers(45, 3) if incoming_data: self.Fault_FlagsBin = str('{0:016b}'.format(int(incoming_data[0]))) self.Alarm_FlagsBin = str('{0:032b}'.format(int(incoming_data[1] << 16) + int(incoming_data[2]))) else: print(" -> Error Reading 0x002C-0x002F block of "+self.name) #Holding 0x0032-0x003A = Charger Stage, Target Reg Voltage, AH Total resetable, NA, AH Total, NA, KWh Total Resetable, KWh Total, Output Power incoming_data = self.TCP_Mod_Client.read_holding_registers(50, 9) if incoming_data: self.Charge_Stage = str(incoming_data[0]) #Develop actual charge stage message based upon value if self.Charge_Stage: self.Charge_Stage_Message = self.set_chargestage_msg(self.Charge_Stage) self.Ahr_Lifetime = str(incoming_data[4]) self.KWh_Lifetime = str(incoming_data[7]) self.Output_Watts = str(incoming_data[8]) else: print(" -> Error Reading 0x0032-0x003A block of "+self.name) #Holding 0x0043-0x0044 = Daily Ahr Total, Daily KWh Total incoming_data = self.TCP_Mod_Client.read_holding_registers(67, 2) if incoming_data: self.Ahr = str(incoming_data[0]) self.KWh = str(incoming_data[1] / 1000) else: print(" -> Error Reading 0x0043-0x0044 block of "+self.name) #Holding 0x004D-0x004F = Daily time in Absorb, Daily time in EQ, Daily time in Float incoming_data = self.TCP_Mod_Client.read_holding_registers(77, 3) if incoming_data: self.Absorb_Time_Count = str(incoming_data[0] / 3600) #Convert from seconds to hours self.Float_Time_Count = str(incoming_data[2] / 3600) #Convert from seconds to hours else: print(" -> Error Reading 0x004D-0x004F block of "+self.name) #Holding 0x1026-0x1028 = MAC Address incoming_data = self.TCP_Mod_Client.read_holding_registers(4134, 3) if incoming_data: MAC1 = (str(hex((incoming_data[0] & 0xFF00)>>8))) MAC1 = MAC1[2:].zfill(2) MAC2 = (str(hex((incoming_data[0] & 0x00FF)))) MAC2 = MAC2[2:].zfill(2) MAC3 = (str(hex((incoming_data[1] & 0xFF00)>>8))) MAC3 = MAC3[2:].zfill(2) MAC4 = (str(hex((incoming_data[1] & 0x00FF)))) MAC4 = MAC4[2:].zfill(2) MAC5 = (str(hex((incoming_data[1] & 0xFF00)>>8))) MAC5 = MAC5[2:].zfill(2) MAC6 = (str(hex((incoming_data[1] & 0x00FF)))) MAC6 = MAC6[2:].zfill(2) #Concat the above results self.MAC_addr = MAC1.upper()+":"+MAC2.upper()+":"+MAC3.upper()+":"+MAC4.upper()+":"+MAC5.upper()+":"+MAC6.upper() else: print(" -> Error Reading 0x1026-0x1028 block of "+self.name) #Holding 0xE000-0xE0C02 = Absorb Setpoint, Float Setpoint, Absorb Time incoming_data = self.TCP_Mod_Client.read_holding_registers(57344, 3) if incoming_data: self.Conf_Absorb_Setpoint = str(self.tristar_scaling(V_PU_hi, V_PU_lo, incoming_data[0])) self.Conf_Float_Setpoint = str(self.tristar_scaling(V_PU_hi, V_PU_lo, incoming_data[1])) self.Conf_Absorb_Time = str(incoming_data[2] / 3600) #Convert from seconds to hours else: print(" -> Error Reading 0xE000-0xE002 block of "+self.name) #Holding 0xE007-0xE00A = EQ Setpoint, Days Between EQ, EQ Time Above Vreg, EQ Time at Vreg incoming_data = self.TCP_Mod_Client.read_holding_registers(57351, 4) if incoming_data: self.Conf_EQ_Setpoint = str(self.tristar_scaling(V_PU_hi, V_PU_lo, incoming_data[0])) self.Conf_EQ_Time = str(incoming_data[3] / 3600) #Convert from seconds to hours else: print(" -> Error Reading 0xE007-0xE00A block of "+self.name) #Holding 0xE011-0xE012 = Maximum Temperature Comp, Minimum Temperature Comp incoming_data = self.TCP_Mod_Client.read_holding_registers(57361, 2) if incoming_data: self.Conf_MaxTemp_Comp = str(incoming_data[0]) self.Conf_MinTemp_Comp = str(incoming_data[1]) else: print(" -> Error Reading 0xE011-0xE012 block of "+self.name) #Holding 0xE0C0-0xE0C3 = Model, Hardware Version incoming_data = self.TCP_Mod_Client.read_holding_registers(57536, 3) if incoming_data: self.Serial = str((incoming_data[0] & 0x00FF)) + str(incoming_data[0] >> 8) + str((incoming_data[1] & 0x00FF)) + str(incoming_data[1] >> 8) + str((incoming_data[2] & 0x00FF)) + str(incoming_data[2] >> 8) else: print(" -> Error Reading 0xE0C0-0xE0C3 block of "+self.name) #Holding 0xE0CC-0xE0CD = Model, Hardware Version incoming_data = self.TCP_Mod_Client.read_holding_registers(57547, 2) if incoming_data: self.Unit_Type = str(incoming_data[0]) self.HW_Rev = str(incoming_data[1]) else: print(" -> Error Reading 0xE0CC-0xE0CD block of "+self.name) #Invlaid OEM - Type configured ------------------------------------------------- else: if self.verbose_mode or local_verbose_mode: print(" -> Invalid OEM - Type assigned to "+self.name+", see CCDM_config File") #Update date and time stamp each read self.dataread_DateTime_stamp = datetime.datetime.now() #Perform some calculations to further extract information from data points above self.misc_calcs() return 0 #------------------------------------------------------------------------------------------------------------------------------ def misc_calcs(self): #Perform some calculations #Input Watts if self.Input_Voltage and self.Input_Current: self.Input_Watts = str(round(float(self.Input_Voltage) * float(self.Input_Current),2)) #CC Eff if self.Input_Watts and self.Output_Watts and (float(self.Input_Watts) > 0): self.Calced_CC_Eff = round((float(self.Output_Watts) / float(self.Input_Watts))*100,2) if self.Calced_CC_Eff > 100: self.Calced_CC_Eff = str(100) else: self.Calced_CC_Eff = str(self.Calced_CC_Eff) else: self.Calced_CC_Eff = str(0) #Current to Load (Inverter or other load) if self.Output_Current and self.Shunt_Current: self.Calced_Load_Current = str(round(float(self.Output_Current) - float(self.Shunt_Current),2)) #Power Consumed by Load (Inverter or other load) if self.Output_Voltage and self.Calced_Load_Current: self.Calced_Load_Power = str(round(float(self.Output_Voltage) * float(self.Calced_Load_Current),2)) #Battery Half Capacity and Estimated time to 50% discharge if self.Battery_Capacity and (int(self.Battery_Capacity) > 0): self.Calced_Batt_50Capacity = str((int(self.Battery_Capacity) / 2)) if self.Battery_Capacity and self.Battery_Remaining and self.Calced_Batt_50Capacity: if (float(self.Battery_Remaining) > float(self.Calced_Batt_50Capacity)) and (float(self.Shunt_Current) != 0): if float(self.Shunt_Current) < 0: self.Calced_TimeTo_Batt50 = str(round((float(self.Battery_Remaining) - float(self.Calced_Batt_50Capacity)) / abs(float(self.Shunt_Current)),2)) else: self.Calced_TimeTo_Batt50 = "Infinite" else: self.Calced_TimeTo_Batt50 = str(0) #Battery Estimated Freezing Point if self.Battery_SOC: self.Calced_Batt_FreezeEstimate = str(linear_scaling(int(self.Battery_SOC), 100, 0, -50, 0)) #Current Battery SOC, Upper SOC%, Lower SOC%, Freeze Point @ Upper SOC%, Freeeze Point @ Lower SOC% self.Calced_Batt_FreezeEstimate_DegF = str(DegC_to_DegF(float(self.Calced_Batt_FreezeEstimate))) #Capture peaks and other various process Events ---------------------------------- #Peak Inputs Watts, daily if self.Input_Watts and float(self.Input_Watts) > float(self.Peak_Input_Watts): self.Peak_Input_Watts = self.Input_Watts #Peak Output Watts, daily if self.Output_Watts and float(self.Output_Watts) > float(self.Peak_Output_Watts): self.Peak_Output_Watts = self.Output_Watts #Peak Output Voltage, daily if self.Output_Voltage and float(self.Output_Voltage) > float(self.Peak_Output_Voltage): self.Peak_Output_Voltage = self.Output_Voltage #Peak Output Current, daily if self.Output_Current and float(self.Output_Current) > float(self.Peak_Output_Current): self.Peak_Output_Current = self.Output_Current #Peak CC Temperature, daily if self.FET_Temperature and float(self.FET_Temperature) > float(self.Peak_CC_Temperature): self.Peak_CC_Temperature = self.FET_Temperature self.Peak_CC_Temperature_DegF = str(DegC_to_DegF(float(self.Peak_CC_Temperature))) #Maximum Battery Temperature, daily if self.Battery_Temperature and float(self.Battery_Temperature) > float(self.Max_Batt_Temperature): self.Max_Batt_Temperature = self.Battery_Temperature self.Max_Batt_Temperature_DegF = str(DegC_to_DegF(float(self.Max_Batt_Temperature))) #Minimum Battery Temperature, daily if self.Battery_Temperature and float(self.Battery_Temperature) < float(self.Min_Batt_Temperature): self.Min_Batt_Temperature = self.Battery_Temperature self.Min_Batt_Temperature_DegF = str(DegC_to_DegF(float(self.Min_Batt_Temperature))) #Maximum Battery Voltage, daily if self.Output_Voltage and float(self.Battery_Voltage) > float(self.Max_Batt_Voltage): self.Max_Batt_Voltage = self.Battery_Voltage #Minimum Battery Voltage, daily if self.Output_Voltage and float(self.Battery_Voltage) < float(self.Min_Batt_Voltage): self.Min_Batt_Voltage = self.Battery_Voltage #Absorb Reached Event if self.Charge_Stage_Message[0:6] == "Absorb": self.Event_Absorb_Reached = "Yes" #Time in Absorb if self.oem_series == "Midnite - Classic": if self.Absorb_Time_Count != None and (float(self.Absorb_Time_Count) > 0) and (float(self.Absorb_Time_Count) < float(self.Conf_Absorb_Time)) and self.Conf_Absorb_Time != None: self.Event_TimeIn_Absorb = str(round((float(self.Conf_Absorb_Time) - float(self.Absorb_Time_Count)), 2)) elif self.oem_series == "Tristar - MPPT 60": if self.Absorb_Time_Count != None: self.Event_TimeIn_Absorb = str(round(float(self.Absorb_Time_Count), 2)) #Float Reached Event if self.Charge_Stage_Message[0:5] == "Float": self.Event_Float_Reached = "Yes" #Time in Float if self.oem_series == "Midnite - Classic": if self.Float_Time_Count != None: self.Event_TimeIn_Float = str(round(float(self.Float_Time_Count), 2)) elif self.oem_series == "Tristar - MPPT 60": if self.Float_Time_Count != None: self.Event_TimeIn_Float = str(round(float(self.Float_Time_Count), 2)) #Just before midnite, reset all peaks and captured events if (self.dataread_DateTime_stamp.hour == 23) and (self.dataread_DateTime_stamp.minute == 59): self.Peak_Input_Watts = "0" self.Peak_Output_Watts = "0" self.Peak_Output_Voltage = "0" self.Peak_Output_Current = "0" self.Peak_CC_Temperature = "0" self.Max_Batt_Temperature = "0" self.Min_Batt_Temperature = "999" self.Max_Batt_Voltage = "0" self.Min_Batt_Voltage = "999" self.Event_Absorb_Reached = "No" self.Event_TimeIn_Absorb = "0" self.Event_Float_Reached = "No" self.Event_TimeIn_Float = "0" #------------------------------------------------------------------------------------------------------------------------------ def tristar_scaling(self, hi, lo, value_to_scale): #Scaling for Voltahe and Current as per Morningstar document scaler = hi + (lo / 65535) scaled_value = (value_to_scale * scaler) / 32768 return scaled_value #------------------------------------------------------------------------------------------------------------------------------ def name(self): return self.name #------------------------------------------------------------------------------------------------------------------------------ def blank_cc_image(self): #reset all data back to None (or zero where applicable) self.MAC_addr = None self.Date_Time = None self.FW_Rev = None self.Unit_Type = None self.HW_Rev = None self.FET_Temperature = None self.PCB_Temperature = None self.Charge_Stage = None self.Charge_Stage_Message = None self.Last_VoC_Measured = None self.Highest_InputV_Logged = None self.InfoData_Flags = None self.InfoData_FlagsBin = None self.KWh_Lifetime = None self.Ahr_Lifetime = None self.MPPT_Mode = None self.MPPT_Mode_Message = None self.Aux1_Function = None self.Aux2_Function = None self.Input_Voltage = None self.Input_Current = None self.Input_Watts = None self.Input_Peak_Watts = "0" self.Output_Voltage = None self.Output_Current = None self.Output_Watts = None self.KWh = None self.Ahr = None self.Shunt_Installed = None self.Shunt_Temperature = None self.Shunt_Current = None self.Battery_SOC = None self.Battery_Temperature = None self.Battery_Remaining = None self.Battery_Capacity = None self.Conf_Nominal_Voltage = None self.Conf_Absorb_Setpoint = None self.Conf_Absorb_Time = None self.Conf_End_Amps = None self.Conf_Float_Setpoint = None self.Conf_EQ_Setpoint = None self.Conf_EQ_Time = None self.Conf_MaxTemp_Comp = None self.Conf_MinTemp_Comp = None self.Conf_TempComp_Value = None self.Calced_CC_Eff = None self.Calced_Load_Current = None self.Calced_Load_Power = None self.Calced_TimeTo_Batt50 = "0" self.Calced_Batt_50Capactiy = "0" self.Calced_Batt_FreezeEstimate = "0" #------------------------------------------------------------------------------------------------------------------------------ def set_chargestage_msg(self, charge_stage_value): icharge_stage_value = int(charge_stage_value) charge_stage_message = None if self.oem_series == "Midnite - Classic": #Develop message based on value if icharge_stage_value == 0: charge_stage_message = "Resting ("+charge_stage_value+")" elif icharge_stage_value == 3: charge_stage_message = "Absorb ("+charge_stage_value+")" elif icharge_stage_value == 4: charge_stage_message = "Bulk MPPT ("+charge_stage_value+")" elif icharge_stage_value == 5: charge_stage_message = "Float ("+charge_stage_value+")" elif icharge_stage_value == 6: charge_stage_message = "Float MPPT ("+charge_stage_value+")" elif icharge_stage_value == 7: charge_stage_message = "Equalize ("+charge_stage_value+")" elif icharge_stage_value == 10: charge_stage_message = "Hyper Voc ("+charge_stage_value+")" elif icharge_stage_value == 18: charge_stage_message = "Equalize MPPT ("+charge_stage_value+")" else: charge_stage_message = "Unknown state (Value given: "+charge_stage_value+")" elif self.oem_series == "Tristar - MPPT 60": #Develop message based on value if icharge_stage_value == 0: charge_stage_message = "Start ("+charge_stage_value+")" elif icharge_stage_value == 1: charge_stage_message = "Night Check ("+charge_stage_value+")" elif icharge_stage_value == 2: charge_stage_message = "Disconnect ("+charge_stage_value+")" elif icharge_stage_value == 3: charge_stage_message = "Night ("+charge_stage_value+")" elif icharge_stage_value == 4: charge_stage_message = "Fault ("+charge_stage_value+")" elif icharge_stage_value == 5: charge_stage_message = "Bulk MPPT ("+charge_stage_value+")" elif icharge_stage_value == 6: charge_stage_message = "Absorb ("+charge_stage_value+")" elif icharge_stage_value == 7: charge_stage_message = "Float ("+charge_stage_value+")" elif icharge_stage_value == 8: charge_stage_message = "Equalize ("+charge_stage_value+")" elif icharge_stage_value == 9: charge_stage_message = "Slave ("+charge_stage_value+")" else: charge_stage_message = "Unknown state (Value given: "+charge_stage_value+")" return charge_stage_message #------------------------------------------------------------------------------------------------------------------------------ def set_mpptmode_msg(self, mppt_mode_value): imppt_mode_value = int(mppt_mode_value) mppt_mode_message = None if self.oem_series == "Midnite - Classic": #Develop message based on value if imppt_mode_value == 1: mppt_mode_message = "PV_Uset ("+mppt_mode_value+")" elif imppt_mode_value == 3: mppt_mode_message = "Dynamic ("+mppt_mode_value+")" elif imppt_mode_value == 5: mppt_mode_message = "Wind Track ("+mppt_mode_value+")" elif imppt_mode_value == 7: mppt_mode_message = "RESERVED ("+mppt_mode_value+")" elif imppt_mode_value == 9: mppt_mode_message = "Legacy P&O ("+mppt_mode_value+")" elif imppt_mode_value == 11: mppt_mode_message = "Solar ("+mppt_mode_value+")" elif imppt_mode_value == 13: mppt_mode_message = "Hydro ("+mppt_mode_value+")" elif imppt_mode_value == 15: mppt_mode_message = "RESERVED ("+mppt_mode_value+")" else: mppt_mode_message = "Unknown ("+mppt_mode_value+")" elif self.oem_series == "Tristar - MPPT 60": #There is no MPPT Mode settings for Tristar, it is fixed solar input type mppt_mode_message = "Solar" return mppt_mode_message
class AcquisitionProcess: """ Class that handles the Modbus communication between the application and the target CUs. The class implements methods allowing: - Modbus communication establishment; - Reading register value from a Modbus server; - Comparing this value to the previous one to check if the CU is still alive. """ def __init__(self, config, gui): """ AcquisitionProcess class constructor. :param config: (CuConfig object) Contains an object that handles the CU config file (LifeCounterConfig.ini) :param gui: (GuiManagement object) Contains an object that handles the GUI display. """ # Communication configuration self.config_cu_name = config.get("cu_name") self.config_server_host = config.get("server_host") self.config_test_period = config.get("test_period") self.config_register_address = config.get("register_address") self.config_process_check_time = config.get("process_check_time") # Modbus communication definition self.client = ModbusClient() self.client.host(self.config_server_host) self.client.port(SERVER_PORT) self.client.timeout(MODBUS_TIMEOUT) # Values initialization self.previous_counter_value = -1 self.previous_process_check = 0 self.previous_process_time = datetime.now() self.process_startup = True self.process_started = False self.connection_attempt = 0 # Define new name for alarms tracking file, changes everyday self.day_file_name = "{}-{}_logs.txt".format(time.strftime("%Y_%m_%d"), self.config_cu_name) # Objects initialization self.thread = None self.logger = None # GUI textbox declaration self.gui = gui def __del__(self): """ Method called at object deletion. Override in order to cancel thread and close communication socket in case they are running. :return: N/A """ if self.client.is_open(): self.client.close() if self.thread: self.thread.cancel() def __setupLogger(self, name, log_file, level=logging.INFO): """ Private method that creates a logger for the :param name: (string) Logger name :param log_file: (string) File pâth to write logs :param level: (logging.level) Level of information to write :return: (logging) Logger dedicated to current CU instantiated """ # Define logs format formatter = logging.Formatter( fmt='[%(levelname)s] - %(asctime)s - %(message)s', datefmt='%Y/%m/%d %I:%M:%S %p') if not os.path.exists('./Logs/'): os.makedirs('./Logs/') # Define file handler with path and format handler = logging.FileHandler("./Logs/" + log_file) handler.setFormatter(formatter) # Define logger with name, level and adding the file handler logger = logging.getLogger(name) logger.setLevel(level) logger.addHandler(handler) return logger def __handleFunction(self): """ Private method that handles the thread of the the current CU communication. This method also defines the log files, the logger format and ensures that the prerequisites are met before starting the application. :return: N/A """ try: # Notify if process still running self.__notifyRunning() # Open or reconnect TCP to server self.__modbusClientConnection() # Check that CU is still running self.__processCheck() except AttributeError as e: error_message = e + \ "\nThe process was stopped during communication establishment to" + \ self.config_cu_name self.__displayError(error_message) # Handle thread if self.process_started: self.thread = Timer(self.config_test_period, self.__handleFunction) self.thread.start() def start(self): """ Public method that starts the data acquisition through a Modbus/TCP communication. :return: N/A """ if not self.process_started: self.connection_attempt = 0 display_message = "================================\n" + \ self.config_cu_name + " Life counter process started\n" \ "================================" self.__displayLog(display_message) self.logger = self.__setupLogger(self.config_cu_name, self.day_file_name) self.thread = Timer(self.config_test_period, self.__handleFunction) self.thread.start() self.process_started = True else: showinfo(self.config_cu_name + " info", self.config_cu_name + " process already started.") def stop(self): """ Public method that stops the data acquisition through a Modbus/TCP communication. :return: N/A """ if self.process_started: if self.client.is_open(): self.client.close() self.__updateComStatus(CuStatus.UNKNOWN) display_message = "================================\n" + \ self.config_cu_name + " Life counter process stopped\n" \ "================================\n" self.__displayLog(display_message) self.thread.cancel() self.__notifyStop() self.process_started = False else: showinfo(self.config_cu_name + " info", self.config_cu_name + " process already stopped.") def __updateComStatus(self, status): """ Private method that allows to ask the GUI related object ot update its CU status picture. :param status: (Enum CuStatus) Status of the CU communication :return: N/A """ self.gui.displayCuComStatus(self.config_cu_name, status) def __displayLog(self, message): """ Private method used to write logs in the CU frame on the GUI. :param message: (string) Message to display :return: N/A """ self.gui.displayLog(self.config_cu_name, message) def __displayError(self, error): """ Private method to generate popup in case of communication error detected. :param error: (string) Message to display on the popup :return:N/A """ showerror("Error", error) def __notifyRunning(self): """ Private method to generate a log to notify that the process is currently running. :return: N/A """ process_current_time = datetime.now() process_check_time_delta = process_current_time - self.previous_process_time if (int(process_check_time_delta.total_seconds()) >= self.config_process_check_time) \ or self.process_startup: log_message = self.config_cu_name + " - Process running." self.logger.info(log_message) self.previous_process_time = datetime.now() self.process_startup = False def __notifyStop(self): """ Private method to generate a log to notify that the process was stopped :return: N/A """ log_message = self.config_cu_name + " - Process stopped." self.logger.info(log_message) self.logger = None def __modbusClientConnection(self): """ Private method that handles the Modbus connection to the CU. This method implements the following checks: - Maximum connection attempts. If the maximum number is reached, a popup is displayed and the connection is aborted. The CU communication status display changes to unhealthy. - Connection established. If it is not established, a popup is displayed and a log is written in the CU frame and in the log file. The CU communication status display changes to unhealthy. If the connection is correctly established, the CU communication status display changes to healthy. :return: N/A """ if not self.client.is_open(): if self.connection_attempt < MAX_CONNECTION_ATTEMPT: # Try to connect try: if not self.client.open(): self.thread.cancel() self.__updateComStatus(CuStatus.UNHEALTHY) # If not able, records the error error_message = "Unable to connect to {} : {}.".format( self.config_server_host, str(SERVER_PORT)) log_message = "Unable to connect to {} ({}:{}).".format( self.config_cu_name, self.config_server_host, str(SERVER_PORT)) display_message = log_message self.__displayError(error_message) self.__displayLog(display_message) self.logger.error(log_message) self.connection_attempt += 1 else: self.__updateComStatus(CuStatus.HEALTHY) except AttributeError: pass else: self.stop() self.__displayError("Connection to " + self.config_cu_name + " aborted.") else: self.__updateComStatus(CuStatus.HEALTHY) def __processCheck(self): """ Private method that checks that the counter value of the CU is still changing. This method prints the value read in the frame of the CU. If the counter value does not change between two calls, an error popup is displayed and the error is written in the log file. :return: N/A """ # Counter test if self.client.is_open(): # Read the counter value self.counter_value = self.client.read_holding_registers( self.config_register_address) display_message = "{} Life counter value: {}".format( self.config_cu_name, str(self.counter_value)) self.__displayLog(display_message) # Test if counter is incremented if self.counter_value == self.previous_counter_value or str( self.counter_value) == "None": self.thread.cancel() self.__updateComStatus(CuStatus.UNHEALTHY) error_message = self.config_cu_name + " has stopped running." log_message = "Life counter value didn't change (value = {}))".format( self.counter_value) display_message = log_message self.__displayError(error_message) self.__displayLog(display_message) self.logger.error(log_message) # Records the counter value for next comparison self.previous_counter_value = self.counter_value