Exemplo n.º 1
0
class mbusTCP:
    def __init__(self, ID, TCPaddress, PORT):  #, TCPaddress

        # Modbus instance
        #debug=True (quitar cuando no sea necesario el modo debug)

        try:
            self.mb_device = ModbusClient(host=TCPaddress,
                                          port=PORT,
                                          timeout=10,
                                          debug=True,
                                          unit_id=ID)
            self.functionCode = 3  # Function Code
            self.dict = {}
        except:
            print(self.mb_device)
            #return mb_device.last_error()
            raise

    def read_data(self, reg_ini, num_regs):
        try:
            data = self.mb_device.read_holding_registers(
                int(reg_ini), num_regs)
            return data
        except:
            print(self.mb_device.last_error())
            #return mb_device.last_error()
            raise

    def openTCP(self):
        try:
            self.mb_device.open()
            #time.sleep(1)
        except:
            print("\nError abriendo conexión TCP...")

    def closeTCP(self):
        try:
            self.mb_device.close()
            time.sleep(1)

        except:
            print("\nError cerrando conexión TCP...")


###-----the two functions below are only for documentation purposes in how to read U32/U64 registers

    def device_read_U64(self, ini_reg):
        regs = self.mb_device.read_holding_registers(ini_reg - 1, 4)
        Translate = convert4()
        Translate.u16.hh = regs[3]
        Translate.u16.hl = regs[2]
        Translate.u16.lh = regs[1]
        Translate.u16.ll = regs[0]
        return Translate.uint64

    def device_read_U32(self, ini_reg):
        regs = self.mb_device.read_holding_registers(ini_reg - 1, 2)
        Translate = convert2()
        Translate.u16.h = regs[1]
        Translate.u16.l = regs[0]
        return Translate.uint32
async def write_to_influx(dbhost, dbport, mbmeters, period, dbname,
                          legacysupport):
    global client
    global datapoint
    global reg_block
    global promInv
    global promMeter

    def trunc_float(floatval):
        return float('%.2f' % floatval)

    try:
        solar_client = InfluxDBClient(host=dbhost, port=dbport, db=dbname)
        await solar_client.create_database(db=dbname)
    except ClientConnectionError as e:
        logger.error(f'Error during connection to InfluxDb {dbhost}: {e}')
        return
    logger.info('Database opened and initialized')

    # Connect to the solaredge inverter
    client = ModbusClient(args.inverter_ip,
                          port=args.inverter_port,
                          unit_id=args.unitid,
                          auto_open=True)

    # Read the common blocks on the Inverter
    while True:
        reg_block = {}
        reg_block = client.read_holding_registers(40004, 65)
        if reg_block:
            decoder = BinaryPayloadDecoder.fromRegisters(reg_block,
                                                         byteorder=Endian.Big,
                                                         wordorder=Endian.Big)
            InvManufacturer = decoder.decode_string(32).decode(
                'UTF-8')  #decoder.decode_32bit_float(),
            InvModel = decoder.decode_string(32).decode(
                'UTF-8')  #decoder.decode_32bit_int(),
            Invfoo = decoder.decode_string(16).decode('UTF-8')
            InvVersion = decoder.decode_string(16).decode(
                'UTF-8')  #decoder.decode_bits()
            InvSerialNumber = decoder.decode_string(32).decode('UTF-8')
            InvDeviceAddress = decoder.decode_16bit_uint()

            print('*' * 60)
            print('* Inverter Info')
            print('*' * 60)
            print(' Manufacturer: ' + InvManufacturer)
            print(' Model: ' + InvModel)
            print(' Version: ' + InvVersion)
            print(' Serial Number: ' + InvSerialNumber)
            print(' ModBus ID: ' + str(InvDeviceAddress))
            break
        else:
            # Error during data receive
            if client.last_error() == 2:
                logger.error(
                    f'Failed to connect to SolarEdge inverter {client.host()}!'
                )
            elif client.last_error() == 3 or client.last_error() == 4:
                logger.error('Send or receive error!')
            elif client.last_error() == 5:
                logger.error('Timeout during send or receive operation!')
            await asyncio.sleep(period)

    # Read the common blocks on the meter/s (if present)
    connflag = False
    if mbmeters >= 1:
        while True:
            dictMeterLabel = []
            for x in range(1, mbmeters + 1):
                reg_block = {}
                if x == 1:
                    reg_block = client.read_holding_registers(40123, 65)
                if x == 2:
                    reg_block = client.read_holding_registers(40297, 65)
                if x == 3:
                    reg_block = client.read_holding_registers(40471, 65)
                if reg_block:
                    decoder = BinaryPayloadDecoder.fromRegisters(
                        reg_block, byteorder=Endian.Big, wordorder=Endian.Big)
                    MManufacturer = decoder.decode_string(32).decode(
                        'UTF-8')  #decoder.decode_32bit_float(),
                    MModel = decoder.decode_string(32).decode(
                        'UTF-8')  #decoder.decode_32bit_int(),
                    MOption = decoder.decode_string(16).decode('UTF-8')
                    MVersion = decoder.decode_string(16).decode(
                        'UTF-8')  #decoder.decode_bits()
                    MSerialNumber = decoder.decode_string(32).decode('UTF-8')
                    MDeviceAddress = decoder.decode_16bit_uint()
                    fooLabel = MManufacturer.split(
                        '\x00')[0] + '(' + MSerialNumber.split('\x00')[0] + ')'
                    dictMeterLabel.append(fooLabel)
                    print('*' * 60)
                    print('* Meter ' + str(x) + ' Info')
                    print('*' * 60)
                    print(' Manufacturer: ' + MManufacturer)
                    print(' Model: ' + MModel)
                    print(' Mode: ' + MOption)
                    print(' Version: ' + MVersion)
                    print(' Serial Number: ' + MSerialNumber)
                    print(' ModBus ID: ' + str(MDeviceAddress))
                    if x == mbmeters:
                        print('*' * 60)
                    connflag = True
                else:
                    # Error during data receive
                    if client.last_error() == 2:
                        logger.error(
                            f'Failed to connect to SolarEdge inverter {client.host()}!'
                        )
                    elif client.last_error() == 3 or client.last_error() == 4:
                        logger.error('Send or receive error!')
                    elif client.last_error() == 5:
                        logger.error(
                            'Timeout during send or receive operation!')
                    await asyncio.sleep(period)
            if connflag:
                break

    # Start the loop for collecting the metrics...
    while True:
        try:
            reg_block = {}
            dictInv = {}
            reg_block = client.read_holding_registers(40069, 50)
            if reg_block:
                # print(reg_block)
                # reg_block[0] = Sun Spec DID
                # reg_block[1] = Length of Model Block
                # reg_block[2] = AC Total current value
                # reg_block[3] = AC Phase A current value
                # reg_block[4] = AC Phase B current value
                # reg_block[5] = AC Phase C current value
                # reg_block[6] = AC current scale factor
                # reg_block[7] = AC Phase A to B voltage value
                # reg_block[8] = AC Phase B to C voltage value
                # reg_block[9] = AC Phase C to A voltage value
                # reg_block[10] = AC Phase A to N voltage value
                # reg_block[11] = AC Phase B to N voltage value
                # reg_block[12] = AC Phase C to N voltage value
                # reg_block[13] = AC voltage scale factor
                # reg_block[14] = AC Power value
                # reg_block[15] = AC Power scale factor
                # reg_block[16] = AC Frequency value
                # reg_block[17] = AC Frequency scale factor
                # reg_block[18] = AC Apparent Power
                # reg_block[19] = AC Apparent Power scale factor
                # reg_block[20] = AC Reactive Power
                # reg_block[21] = AC Reactive Power scale factor
                # reg_block[22] = AC Power Factor
                # reg_block[23] = AC Power Factor scale factor
                # reg_block[24] = AC Lifetime Energy (HI bits)
                # reg_block[25] = AC Lifetime Energy (LO bits)
                # reg_block[26] = AC Lifetime Energy scale factor
                # reg_block[27] = DC Current value
                # reg_block[28] = DC Current scale factor
                # reg_block[29] = DC Voltage value
                # reg_block[30] = DC Voltage scale factor
                # reg_block[31] = DC Power value
                # reg_block[32] = DC Power scale factor
                # reg_block[34] = Inverter temp
                # reg_block[37] = Inverter temp scale factor
                # reg_block[38] = Inverter Operating State
                # reg_block[39] = Inverter Status Code
                datapoint = {
                    'measurement': 'SolarEdge',
                    'tags': {},
                    'fields': {}
                }
                logger.debug(f'inverter reg_block: {str(reg_block)}')
                datapoint['tags']['inverter'] = str(1)

                data = BinaryPayloadDecoder.fromRegisters(reg_block,
                                                          byteorder=Endian.Big,
                                                          wordorder=Endian.Big)

                # SunSpec DID
                # Register 40069
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'SunSpec_DID'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal
                else:
                    dictInv[fooName] = 0.0

                # SunSpec Length
                # Register 40070
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'SunSpec_Length'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal
                else:
                    dictInv[fooName] = 0.0

                # AC Current
                data.skip_bytes(8)
                # Register 40075
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-10)
                # Register 40071-40074
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_Current'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_CurrentA'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_CurrentB'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_CurrentC'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # AC Voltage
                data.skip_bytes(14)
                # Register 40082
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-14)
                # Register 40077
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_VoltageAB'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_VoltageBC'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_VoltageCA'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_VoltageAN'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_VoltageBN'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_VoltageCN'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # AC Power
                data.skip_bytes(4)
                # Register 40084
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40083
                fooVal = trunc_float(data.decode_16bit_int())
                fooName = 'AC_Power'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

            # AC Frequency
                data.skip_bytes(4)
                # Register 40086
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40085
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'AC_Frequency'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # AC Apparent Power
                data.skip_bytes(4)
                # Register 40088
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40087
                fooVal = trunc_float(data.decode_16bit_int())
                fooName = 'AC_VA'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # AC Reactive Power
                data.skip_bytes(4)
                # Register 40090
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40089
                fooVal = trunc_float(data.decode_16bit_int())
                fooName = 'AC_VAR'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # AC Power Factor
                data.skip_bytes(4)
                # Register 40092
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40091
                fooVal = trunc_float(data.decode_16bit_int())
                fooName = 'AC_PF'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # AC Lifetime Energy Production
                data.skip_bytes(6)
                # Register 40095
                scalefactor = 10**data.decode_16bit_uint()
                data.skip_bytes(-6)
                # Register 40093
                fooVal = trunc_float(data.decode_32bit_uint())
                fooName = 'AC_Energy_WH'
                if fooVal < 4294967295:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # DC Current
                data.skip_bytes(4)
                # Register 40097
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40096
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'DC_Current'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # DC Voltage
                data.skip_bytes(4)
                # Register 40099
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40098
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'DC_Voltage'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # DC Power
                data.skip_bytes(4)
                # Register 40101
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-4)
                # Register 40100
                fooVal = trunc_float(data.decode_16bit_int())
                fooName = 'DC_Power'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # Inverter Temp
                data.skip_bytes(10)
                # Register 40106
                scalefactor = 10**data.decode_16bit_int()
                data.skip_bytes(-8)
                # Register 40103
                fooVal = trunc_float(data.decode_16bit_int())
                fooName = 'Temp_Sink'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal * scalefactor
                else:
                    dictInv[fooName] = 0.0

                # Inverter Operating State
                data.skip_bytes(6)
                # Register 40107
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'Status'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal
                else:
                    dictInv[fooName] = 0.0

                # Inverter Operating Status Code
                # Register 40108
                fooVal = trunc_float(data.decode_16bit_uint())
                fooName = 'Status_Vendor'
                if fooVal < 65535:
                    dictInv[fooName] = fooVal
                else:
                    dictInv[fooName] = 0.0

                # Adding the ScaleFactor elements
                dictInv['AC_Current_SF'] = 0.0
                dictInv['AC_Voltage_SF'] = 0.0
                dictInv['AC_Power_SF'] = 0.0
                dictInv['AC_Frequency_SF'] = 0.0
                dictInv['AC_VA_SF'] = 0.0
                dictInv['AC_VAR_SF'] = 0.0
                dictInv['AC_PF_SF'] = 0.0
                dictInv['AC_Energy_WH_SF'] = 0.0
                dictInv['DC_Current_SF'] = 0.0
                dictInv['DC_Voltage_SF'] = 0.0
                dictInv['DC_Power_SF'] = 0.0
                dictInv['Temp_SF'] = 0.0

                logger.debug(f'Inverter')
                for j, k in dictInv.items():
                    logger.debug(f'  {j}: {k}')

                publish_metrics(dictInv, 'inverter', '')
                logger.debug('Done publishing inverter metrics...')

                datapoint['time'] = str(datetime.datetime.utcnow().replace(
                    tzinfo=datetime.timezone.utc).isoformat())

                logger.debug(f'Writing to Influx: {str(datapoint)}')
                await solar_client.write(datapoint)

            else:
                # Error during data receive
                if client.last_error() == 2:
                    logger.error(
                        f'Failed to connect to SolarEdge inverter {client.host()}!'
                    )
                elif client.last_error() == 3 or client.last_error() == 4:
                    logger.error('Send or receive error!')
                elif client.last_error() == 5:
                    logger.error('Timeout during send or receive operation!')
                await asyncio.sleep(period)

            for x in range(1, mbmeters + 1):
                # Now loop through this for each meter that is attached.
                logger.debug(f'Meter={str(x)}')
                reg_block = {}
                dictM = {}

                # Start point is different for each meter
                if x == 1:
                    reg_block = client.read_holding_registers(40188, 105)
                if x == 2:
                    reg_block = client.read_holding_registers(40362, 105)
                if x == 3:
                    reg_block = client.read_holding_registers(40537, 105)
                if reg_block:
                    # print(reg_block)
                    # reg_block[0] = AC Total current value
                    # reg_block[1] = AC Phase A current value
                    # reg_block[2] = AC Phase B current value
                    # reg_block[3] = AC Phase C current value
                    # reg_block[4] = AC current scale factor
                    # reg_block[5] = AC Phase Line (average) to N voltage value
                    # reg_block[6] = AC Phase A to N voltage value
                    # reg_block[7] = AC Phase B to N voltage value
                    # reg_block[8] = AC Phase C to N voltage value
                    # reg_block[9] = AC Phase Line to Line voltage value
                    # reg_block[10] = AC Phase A to B voltage value
                    # reg_block[11] = AC Phase B to C voltage value
                    # reg_block[12] = AC Phase C to A voltage value
                    # reg_block[13] = AC voltage scale factor
                    # reg_block[14] = AC Frequency value
                    # reg_block[15] = AC Frequency scale factor
                    # reg_block[16] = Total Real Power
                    # reg_block[17] = Phase A Real Power
                    # reg_block[18] = Phase B Real Power
                    # reg_block[19] = Phase C Real Power
                    # reg_block[20] = Real Power scale factor
                    # reg_block[21] = Total Apparent Power
                    # reg_block[22] = Phase A Apparent Power
                    # reg_block[23] = Phase B Apparent Power
                    # reg_block[24] = Phase C Apparent Power
                    # reg_block[25] = Apparent Power scale factor
                    # reg_block[26] = Total Reactive Power
                    # reg_block[27] = Phase A Reactive Power
                    # reg_block[28] = Phase B Reactive Power
                    # reg_block[29] = Phase C Reactive Power
                    # reg_block[30] = Reactive Power scale factor
                    # reg_block[31] = Average Power Factor
                    # reg_block[32] = Phase A Power Factor
                    # reg_block[33] = Phase B Power Factor
                    # reg_block[34] = Phase C Power Factor
                    # reg_block[35] = Power Factor scale factor
                    # reg_block[36] = Total Exported Real Energy
                    # reg_block[38] = Phase A Exported Real Energy
                    # reg_block[40] = Phase B Exported Real Energy
                    # reg_block[42] = Phase C Exported Real Energy
                    # reg_block[44] = Total Imported Real Energy
                    # reg_block[46] = Phase A Imported Real Energy
                    # reg_block[48] = Phase B Imported Real Energy
                    # reg_block[50] = Phase C Imported Real Energy
                    # reg_block[52] = Real Energy scale factor
                    # reg_block[53] = Total Exported Real Energy
                    # reg_block[55] = Phase A Exported Real Energy
                    # reg_block[57] = Phase B Exported Real Energy
                    # reg_block[59] = Phase C Exported Real Energy
                    # reg_block[61] = Total Imported Real Energy
                    # reg_block[63] = Phase A Imported Real Energy
                    # reg_block[65] = Phase B Imported Real Energy
                    # reg_block[67] = Phase C Imported Real Energy
                    # reg_block[69] = Real Energy scale factor
                    logger.debug(f'meter reg_block: {str(reg_block)}')

                    # Set the Label to use for the Meter Metrics for Prometheus
                    metriclabel = dictMeterLabel[x - 1]
                    # Clear data from inverter, otherwise we publish that again!
                    datapoint = {
                        'measurement': 'SolarEdge',
                        'tags': {
                            'meter': dictMeterLabel[x - 1]
                        },
                        'fields': {}
                    }

                    data = BinaryPayloadDecoder.fromRegisters(
                        reg_block, byteorder=Endian.Big, wordorder=Endian.Big)

                    # SunSpec DID
                    # Register 40188
                    fooVal = trunc_float(data.decode_16bit_uint())
                    fooName = 'M_SunSpec_DID'
                    if fooVal < 65535:
                        dictM[fooName] = fooVal
                    else:
                        dictM[fooName] = 0.0

                    # SunSpec Length
                    # Register 40070
                    fooVal = trunc_float(data.decode_16bit_uint())
                    fooName = 'M_SunSpec_Length'
                    if fooVal < 65535:
                        dictM[fooName] = fooVal
                    else:
                        dictM[fooName] = 0.0

                    # AC Current
                    data.skip_bytes(8)
                    # Register 40194
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-10)
                    # Register 40190-40193
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_Current'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_CurrentA'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_CurrentB'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_CurrentC'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                # AC Voltage
                    data.skip_bytes(18)
                    # Register 40203
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-18)
                    # Register 40195-40202
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageLN'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageAN'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageBN'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageCN'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageLL'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageAB'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageBC'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VoltageCA'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # AC Frequency
                    data.skip_bytes(4)
                    # Register 40205
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-4)
                    # Register 40204
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_Frequency'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # AC Real Power
                    data.skip_bytes(10)
                    # Register 40210
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-10)
                    # Register 40206-40209
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_Power'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_Power_A'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_Power_B'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_Power_C'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # AC Apparent Power
                    data.skip_bytes(10)
                    # Register 40215
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-10)
                    # Register 40211-40214
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VA'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VA_A'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VA_B'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VA_C'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # AC Reactive Power
                    data.skip_bytes(10)
                    # Register 40220
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-10)
                    # Register 40216-40219
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VAR'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VAR_A'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VAR_B'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_VAR_C'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # AC Power Factor
                    data.skip_bytes(10)
                    # Register 40225
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-10)
                    # Register 40221-40224
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_PF'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_PF_A'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_PF_B'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_16bit_int())
                    fooName = 'M_AC_PF_C'
                    if fooVal < 32768:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # Accumulated AC Real Energy
                    data.skip_bytes(34)
                    # Register 40242
                    scalefactor = 10**data.decode_16bit_int()
                    data.skip_bytes(-34)
                    # Register 40226-40240
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Exported'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Exported_A'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Exported_B'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Exported_C'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Imported'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Imported_A'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Imported_B'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0
                    fooVal = trunc_float(data.decode_32bit_uint())
                    fooName = 'M_Imported_C'
                    if fooVal < 4294967295:
                        dictM[fooName] = fooVal * scalefactor
                    else:
                        dictM[fooName] = 0.0

                    # Accumulated AC Apparent Energy
                    #logger.debug(f'Apparent Energy SF: {str(np.int16(reg_block[69]))}')
                    #scalefactor = np.float_power(10,np.int16(reg_block[69]))
                    #datapoint['fields']['M_Exported_VA'] = trunc_float(((reg_block[53] << 16) + reg_block[54]) * scalefactor)
                    #datapoint['fields']['M_Exported_VA_A'] = trunc_float(((reg_block[55] << 16) + reg_block[56]) * scalefactor)
                    #datapoint['fields']['M_Exported_VA_B'] = trunc_float(((reg_block[57] << 16) + reg_block[58]) * scalefactor)
                    #datapoint['fields']['M_Exported_VA_C'] = trunc_float(((reg_block[59] << 16) + reg_block[60]) * scalefactor)
                    #datapoint['fields']['M_Imported_VA'] = trunc_float(((reg_block[61] << 16) + reg_block[62]) * scalefactor)
                    #datapoint['fields']['M_Imported_VA_A'] = trunc_float(((reg_block[63] << 16) + reg_block[64]) * scalefactor)
                    #datapoint['fields']['M_Imported_VA_B'] = trunc_float(((reg_block[65] << 16) + reg_block[66]) * scalefactor)
                    #datapoint['fields']['M_Imported_VA_C'] = trunc_float(((reg_block[67] << 16) + reg_block[68]) * scalefactor)

                    # Add the ScaleFactor elements
                    dictM['M_AC_Current_SF'] = 0.0
                    dictM['M_AC_Voltage_SF'] = 0.0
                    dictM['M_AC_Frequency_SF'] = 0.0
                    dictM['M_AC_Power_SF'] = 0.0
                    dictM['M_AC_VA_SF'] = 0.0
                    dictM['M_AC_VAR_SF'] = 0.0
                    dictM['M_AC_PF_SF'] = 0.0
                    dictM['M_Energy_W_SF'] = 0.0

                    publish_metrics(dictM, 'meter', metriclabel, x,
                                    legacysupport)

                    datapoint['time'] = str(datetime.datetime.utcnow().replace(
                        tzinfo=datetime.timezone.utc).isoformat())

                    logger.debug(f'Meter: {metriclabel}')
                    for j, k in dictM.items():
                        logger.debug(f'  {j}: {k}')

                    logger.debug(f'Writing to Influx: {str(datapoint)}')
                    await solar_client.write(datapoint)

                else:
                    # Error during data receive
                    if client.last_error() == 2:
                        logger.error(
                            f'Failed to connect to SolarEdge inverter {client.host()}!'
                        )
                    elif client.last_error() == 3 or client.last_error() == 4:
                        logger.error('Send or receive error!')
                    elif client.last_error() == 5:
                        logger.error(
                            'Timeout during send or receive operation!')
                    await asyncio.sleep(period)

        except InfluxDBWriteError as e:
            logger.error(f'Failed to write to InfluxDb: {e}')
        except IOError as e:
            logger.error(f'I/O exception during operation: {e}')
        except Exception as e:
            logger.error(f'Unhandled exception: {e}')

        await asyncio.sleep(period)
Exemplo n.º 3
0
HOST_ADDR = "192.168.8.20"
HOST_PORT = 502


class ModbusClient(ModbusClient):

    def reg_read(self, address, reg=0x0000, length=0x01):
        if address is not None:
            self.unit_id(address)
        return self.read_holding_registers(reg, length * 2)

    @staticmethod
    def reg_dump(temp=None):
        if temp is not None:
            print("Temperature:     {:.1f}".format(float(temp[0]/10)))
            print("Humidity:   {:10.1%}".format(float(temp[1] / 1000)))


if __name__ == '__main__':

    c = ModbusClient(host=HOST_ADDR, port=HOST_PORT, auto_open=True, auto_close=True, timeout=1)

    while True:
        if c.last_error() > 0:
            c.close()
            c = ModbusClient(host=HOST_ADDR, port=HOST_PORT, auto_open=True, auto_close=True, timeout=1)

        c.reg_dump(c.reg_read(address=0x01))

Exemplo n.º 4
0
class ThermiaGenesis:  # pylint:disable=too-many-instance-attributes
    """Main class to perform modbus requests to heat pump."""

    def __init__(self, host, port=502, kind='inverter', delay=0.1, max_registers=16):
        """Initialize."""

        self.data = {}
        self._client = ModbusClient(host, port=port, unit_id=1, auto_open=True)
        self.firmware = None
        if(kind == MODEL_MEGA): self.model = "Mega"
        else: self.model = "Diplomat Inverter"
        self._host = host
        self._port = port
        self._kind = kind
        self._delay = delay
        self.MAX_REGISTERS = max_registers

        _LOGGER.debug("Using host: %s:%d", host, port)

    async def async_set(self, register, value):  # pylint:disable=too-many-branches
        """Write data to heat pump."""
        ret_value = await self._set_data(register, value)
        self._client.close()

    async def async_update(self, register_types=REG_TYPES, only_registers = None):  # pylint:disable=too-many-branches
        """Update data from heat pump."""
        use_registers = []
        if(only_registers != None):
            #Make sure to sort registers by type and address
            use_registers = sorted(only_registers, key=(lambda x: f"{REGISTERS[x][KEY_REG_TYPE]}-{REGISTERS[x][KEY_ADDRESS]:03}"))
        else:
            use_registers = dict(filter(lambda x: x[1][self._kind], REGISTERS.items())).keys()

        raw_data = await self._get_data(use_registers)
        self._client.close()

        if not raw_data:
            self.data = {}
            return {}

        #_LOGGER.debug("RAW data: %s", raw_data)
        data = {}
        try:
            for i, (name, val) in enumerate(raw_data.items()):
                data[name] = val

            self.firmware = f"{self.data[ATTR_INPUT_SOFTWARE_VERSION_MAJOR]}.{self.data[ATTR_INPUT_SOFTWARE_VERSION_MINOR]}.{self.data[ATTR_INPUT_SOFTWARE_VERSION_MICRO]}"

            _LOGGER.debug("------------- REGISTERS ----------------------")
            for i, (name, val) in enumerate(self.data.items()):
                _LOGGER.debug(f"{REGISTERS[name][KEY_ADDRESS]}\t{val}\t{name}")


        except AttributeError as err:
            _LOGGER.debug("Incomplete data from modbus.")
            _LOGGER.debug(err)
        except KeyError as err: 
            _LOGGER.debug("Incomplete data from modbus.")
            _LOGGER.debug(err)
        except TypeError as err:
            _LOGGER.debug("Incomplete data from modbus.")
            _LOGGER.debug(err)
        self.data = data
        return data

    @property
    def available(self):
        """Return True is data is available."""
        return bool(self.data)


    async def _set_data(self, register, value):
        meta = REGISTERS[register]
        regtype = meta[KEY_REG_TYPE]
        address = meta[KEY_ADDRESS]
        scale = meta[KEY_SCALE]
        await asyncio.sleep(self._delay)
        try:
            if(regtype == REG_COIL):
                _LOGGER.debug(f"Set {regtype} register at {address} value {value} ({value})")
                self._client.write_single_coil(address, value)
            elif(regtype == REG_HOLDING):
                converted_value = int(value * scale)
                if(meta[KEY_DATATYPE] == TYPE_INT):
                    converted_value = num_to_bin(converted_value)
                _LOGGER.debug(f"Set {regtype} register at {address} value {converted_value} ({value}) {scale}")
                self._client.write_single_register(address, converted_value)
            else: 
                raise "This register can not be changed"
        except Exception as e:
            _LOGGER.error(f'exception: {e}')
            print(traceback.format_exc())
        return value


    async def _get_data(self, registers):
        """Retreive data from heat pump."""
        raw_data = {}
        #Split into requests that reads up to self.MAX_REGISTERS within REGISTER_RANGES (register blocks) for the requested registers
        first_chunk_address = 0
        current_type = None
        chunks = []
        chunk = None
        for name in registers:
            meta = REGISTERS[name]

            if(name == ATTR_HOLDING_FIXED_SYSTEM_SUPPLY_SET_POINT):
                #This will give an errror unless coil 42 is True, so skip if we don't know this or if it's false
                enableAttr = ATTR_COIL_ENABLE_FIXED_SYSTEM_SUPPLY_SET_POINT
                if(enableAttr not in raw_data and enableAttr not in self.data): 
                    _LOGGER.debug(f"Will not read {name} since we don't know if {ATTR_COIL_ENABLE_FIXED_SYSTEM_SUPPLY_SET_POINT} is set, include this register in the request to read this")
                    continue
                if(not raw_data[ATTR_COIL_ENABLE_FIXED_SYSTEM_SUPPLY_SET_POINT] and not self.data[ATTR_COIL_ENABLE_FIXED_SYSTEM_SUPPLY_SET_POINT]):
                    _LOGGER.debug(f"Will not read {name} since {ATTR_COIL_ENABLE_FIXED_SYSTEM_SUPPLY_SET_POINT} is False which disables this register")
                    continue

            reg_address = meta[KEY_ADDRESS]
            if(chunk == None #First iteration
                    or chunk[KEY_REG_TYPE] != meta[KEY_REG_TYPE] #New register type
                    or (reg_address - chunk['start']) >= self.MAX_REGISTERS #Exceeds max number of registers per request
                    or reg_address > chunk['range_end']): #Address belongs to another register block
                if(chunk != None):
                    chunks.append(chunk)
                start = meta[KEY_ADDRESS]
                chunk = { KEY_REG_TYPE: meta[KEY_REG_TYPE], 'start': start, 'slots': { name: 0 } }

                if(meta[KEY_DATATYPE] == TYPE_LONG): 
                    chunk['end'] = start + 1
                else: 
                    chunk['end'] = start

                in_range = list(filter(lambda x: x[0] <= start and x[1] >= start, REGISTER_RANGES[self._kind][meta[KEY_REG_TYPE]]))
                chunk['range_end'] = in_range[0][1]

            else:
                chunk['slots'][name] = reg_address - start
                if(meta[KEY_DATATYPE] == TYPE_LONG):
                    chunk['end'] = reg_address + 1
                else:
                    chunk['end'] = reg_address
        if(chunk != None): chunks.append(chunk)
        _LOGGER.info(f"Will make {len(chunks)} requests to read {len(registers)} registers")
        #print(f"Will make {len(chunks)} requests to read {len(registers)} registers")

        try:
            for chunk in chunks:
                await asyncio.sleep(self._delay)
                start_address = chunk['start']
                length = chunk['end'] - chunk['start'] + 1
                regtype = chunk[KEY_REG_TYPE]
                _LOGGER.debug(f"Reading {regtype} {start_address} length {length}")
                read_data = None
                if(regtype == REG_COIL):
                    read_data = self._client.read_coils(start_address, length)
                elif(regtype == REG_DISCRETE_INPUT):
                    read_data = self._client.read_discrete_inputs(start_address, length)
                elif(regtype == REG_INPUT):
                    read_data = self._client.read_input_registers(start_address, length)
                elif(regtype == REG_HOLDING):
                    read_data = self._client.read_holding_registers(start_address, length)
                if read_data:
                    for i, (name, address) in enumerate(chunk['slots'].items()):
                        info = REGISTERS[name]
                        datatype = info[KEY_DATATYPE]
                        scale = info[KEY_SCALE]
                        val = read_data[address]
                        if(datatype == TYPE_LONG):
                            regs = read_data[address:(address+2)]
                            val = word_list_to_long(regs)[0]
                        elif(datatype == TYPE_INT):
                            if(val == 32767): val = 0
                            if(val > 32767): val = val - 65536
                        elif(datatype == TYPE_STATUS):
                            status_str = "OFF"
                            if val == 1:
                                status_str = "Manual Operation"
                            elif val == 2:
                                status_str = "Defrost"
                            elif val == 3:
                                status_str = "Hot water"
                            elif val == 4:
                                status_str = "Heat"
                            elif val == 5:
                                status_str = "Cool"
                            elif val == 6:
                                status_str = "Pool"
                            elif val == 7:
                                status_str = "Anti legionella"
                            elif val == 98:
                                status_str = "Standby"
                            elif val == 99:
                                status_str = "No demand"
                            val = status_str

                        if(scale != 1): val = val / scale
                        raw_data[name] = val
                else:
                    if self._client.last_error() > 0:
                        _LOGGER.error(f'error {self._client.last_error()}')
                    raise Exception(f"Failed to read {regtype} {start_address} length {length}", self._client.last_error())
            #for regtype in register_types:
            #    last_chunk_address = 0
            #    values = []
            #    for chunk in REGISTER_RANGES[self._kind][regtype]:
            #        await asyncio.sleep(self._delay)
            #        start_address = chunk[0]
            #        length = chunk[1] - start_address
            #        #Insert 0 if there is a gap
            #        values.extend([0] * (start_address - last_chunk_address))
            #        _LOGGER.debug(f"Reading {regtype} {start_address} length {length}")
            #        read_data = None
            #        if(regtype == REG_COIL):
            #            read_data = self._client.read_coils(start_address, length)
            #        elif(regtype == REG_DISCRETE_INPUT):
            #            read_data = self._client.read_discrete_inputs(start_address, length)
            #        elif(regtype == REG_INPUT):
            #            read_data = self._client.read_input_registers(start_address, length)
            #        elif(regtype == REG_HOLDING):
            #            read_data = self._client.read_holding_registers(start_address, length)
            #        if read_data:
            #            values.extend(read_data)
            #        else:
            #            if self._client.last_error() > 0:
            #                print(f'error {self._client.last_error()}')
            #                _LOGGER.error(f'error {self._client.last_error()}')
            #            raise Exception(f"Failed to read {regtype} {start_address} length {length}", self._client.last_error())
            #        last_chunk_address = chunk[1]
            #    raw_data[regtype] = values
        except Exception as e:
            _LOGGER.error(f'exception: {e}')
            print(traceback.format_exc())

        return raw_data