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)
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))
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