Exemplo n.º 1
0
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()
Exemplo n.º 2
0
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']
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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