import easymodbus.modbusClient from datetime import datetime as dt from functools import reduce from registers import regdict import numpy as np from pubsub import pub nRegsPortStatus = 32 addrControlAC1 = 64 addrStatusAC1 = 96 addrControlDC2 = 128 addrStatusDC2 = 160 addrControlDC3 = 192 addrStatusDC3 = 224 addrControlSystem = 0x0100 addrStatusSystem = 0x0120 addrStatusThermal = 0x0160 addrStatusDiagnostic = 0x0180 system_status = ["" for i in range(16)] system_status[0] = "PCS in self-test mode" system_status[1] = "PCS reconnect timer 0 counting down from last disconnect due to ABORT-0 fault" system_status[2] = "PCS reconnect timer 1 counting down from last disconnect due to ABORT-1 fault" system_status[3] = "PCS reconnect timer 2 counting down from last disconnect due to ABORT-2 fault" system_status[4] = "PCS DC2 port pre-charge operation is active" system_status[5] = "Invalid control method programmed in PCS" system_status[6] = "AC rotation indication ( 0 – AC1 port wired as A-B-C | 1 – AC1 port wired as C-B-A )" system_status[7] = "PV/MPPT low-voltage indication ( 0 – PV/MPPT is disabled due to low voltage at port | 1 – PV/MPPT " \ "is enabled with voltage above minimum PV/MPPT limit set for port ) " system_status[8] = "PV/MPPT time-of-day indication ( 0 – System time-of-day outside PV/MPPT operational range | 1 – " \ "System time-of-day satisfies start-time and stop-time limits set for PV/MPPT port, PV/MPPT port " \ "allowed to convert power ) " system_status[9] = "PCS power conversion is active" system_status[10] = "PCS hardware shutdown function is active (power conversion disabled)" system_status[11] = "PCS lockdown active due to GFDI fault, IMI fault, or fan fault" system_status[12] = "PCS fault of severity level ABORT-0 active" system_status[13] = "PCS fault of severity level ABORT-1 active" system_status[14] = "PCS fault of severity level ABORT-2 active" system_status[15] = "PCS GFDI fault or IMI fault detected" p1_port_status = {0: "AC1 port is real power soft-limiting", 1: "AC1 port is current soft-limiting", 2: "AC1 port is reactive power soft-limiting", 3: "AC1 port is power derated (limited) due to high temperature", 4: "Reserved", 5: "AC1 port is throttling back on port DC2 due to soft-limiting", 6: "Reserved", 7: "AC1 port is throttling back on port DC3 due to soft-limiting", 8: "AC1 port has the seamless transfer feature enabled when in FPWR control", 9: "The PCS SEL-547 interface transfer switch HW is indicating the PCS is islanded (islanding " "contactor is commanded to open). If enabled, the PCS is able to form a microgrid in FPWR " "control.", 10: "The PCS SEL-547 interface transfer switch HW is indicating the PCS is islanded (islanding " "contactor has successfully opened). If enabled, the PCS is able to form a microgrid in FPWR " "control.", 11: "Reserved", 12: "Reserved", 13: "Reserved", 14: "Reserved", 15: "Reserved"} p2_port_status = {0: "DC2 port is power soft-limiting", 1: "DC2 port is current soft-limiting", 2: "Reserved", 3: "DC2 port is power derated (limited) due to high temperature", 4: "Reserved", 5: "DC2 port is throttling back on port AC1 due to soft-limiting", 6: "Reserved", 7: "DC2 port is throttling back on port DC3 due to soft-limiting", 8: "Reserved", 9: "Reserved", 10: "Reserved", 11: "Reserved", 12: "Reserved", 13: "Reserved", 14: "Reserved", 15: "Reserved"} p3_port_status = {0: "DC3 port is power soft-limiting", 1: "DC3 port is current soft-limiting", 2: "Reserved", 3: "DC3 port is power derated (limited) due to high temperature", 4: "Reserved", 5: "DC3 port is throttling back on port AC1 due to soft-limiting", 6: "Reserved", 7: "DC3 port is throttling back on port DC2 due to soft-limiting", 8: "Reserved", 9: "Reserved", 10: "Reserved", 11: "Reserved", 12: "Reserved", 13: "Reserved", 14: "Reserved", 15: "Reserved"} mode_dict = { "IDLE": 0x0000, "NET": 0x0001, "GPWR": 0x0402, "FPWR": 0x0502, "PV/MPPT": 0x0002, "CURR": 0x0301, "PWR": 0x0401, "VOLT": 0x0501, } def toPower(x): return np.int16(x) * 10 def toVar(x): return np.int16(x) * 10 def toVa(x): return np.int16(x) * 10 def toPwrFactor(x): return np.int16(x) * 0.01 def toCurrent(x): return np.int16(x) * 0.1 def toVoltage(x): return np.int16(x) def toFreq(x): return np.uint16(x) * 0.001 def toTemperature(x): return np.uint16(x) * 0.1 def toRpm(x): return x def toMinutes(x): return x def toTime(x): return x // 60, x % 60 def toBaud(x): return x * 100 def toString(r): ms = r >> 8 ls = (r & 0xff) return chr(ms) + chr(ls) def toBitString(r): return bin(r)[2:].zfill(16) def selectMessages (bits, msgDict): msgs = [] for i, bit in enumerate(bits[::-1]): if bit == '1': msgs.append(msgDict[i]) return msgs def parse_address (addr_string): import re m = re.findall(r'0x([0-9A-F]+)', addr_string) if len(m) > 1: start, end = m elif len(m) > 0: start = m[0] end = start else: raise ValueError() nregisters = int(end, 16) - int(start, 16) + 1 return int(start, 16), nregisters valueConversion = {"U16": np.uint16, "S16": np.int16, "HEX4": toBitString, "HEX": toBitString, "POWER": toPower, "VAR": toVar, "VA": toVa, "PF": toPwrFactor, "CURRENT": toCurrent, "VOLTAGE": toVoltage, "FREQ": toFreq, "TEMP": toTemperature, "RPM": toRpm, "MINUTES": toMinutes, "BAUD": toBaud, "STRING": toString, } class StabilitiRegister(object): def __init__(self, name, proplist): self.name = name self.index = list(map(int, proplist[0])) self.address, self.size = parse_address(proplist[1]) self.access = proplist[2] self.dtype = proplist[3] self.vrange = proplist[4] self.vdefault = proplist[5] def isreadonly(self): return self.is_readable() and (not self.is_writable()) def is_readable(self): return 'R' in self.access def is_writable(self): return 'W' in self.access def convertValue(self, raw): return valueConversion[self.dtype](raw) def getdefaultvalue(self): if self.vdefault == 'NA': return None else: return self.vdefault class StabilitiController(object): '''''' def __init__(self, ipaddr, port=502, uid=240): self.client = easymodbus.modbusClient.ModbusClient(ipaddr, port) self.client.unitidentifier = uid self.reg_info = {k:StabilitiRegister(k, v) for k, v in regdict.items()} self.registers = np.zeros(4000, dtype=np.int) def checkConnect(self): if not self.client.is_connected(): self.client.connect() def checkClose(self): if self.client.is_connected(): self.client.close() def setPortModes(self, modes): self.client.write_single_register(self.reg_info["p1_control_method"].address, mode_dict[modes[0]]) self.client.write_single_register(self.reg_info["p2_control_method"].address, mode_dict[modes[1]]) self.client.write_single_register(self.reg_info["p3_control_method"].address, mode_dict[modes[2]]) def setPortSetpoints(self, val): for k,v in val.items(): self.client.write_single_register(self.reg_info[k].address, int(float(v))) def setUserStart(self): self.client.write_single_register(self.reg_info["user_start"].address, 1) def setUserStop(self): self.client.write_single_register(self.reg_info["user_stop"].address, 1) def readAllRegisters(self): print("read all registers") self.checkConnect() message_size = 64 for i in range(0, 654, message_size): holdingRegisters = self.client.read_holdingregisters(i,message_size) self.registers[i:i+message_size] = np.asarray(holdingRegisters) for i in range(2000, 2069, message_size): holdingRegisters = self.client.read_holdingregisters(i,message_size) self.registers[i:i+message_size] = np.asarray(holdingRegisters) pub.sendMessage("read_all_registers", reg=self.registers, info=self.reg_info) def reg2str(self, r): return toString(r) def getNetworkConfigs(self, config=None): self.checkConnect() configDict = {} from functools import reduce if config: pass else: holdingRegisters = self.client.read_holdingregisters(2030,8) configDict["ipaddr"] = (reduce (lambda a, b: str(a)+str(b), map(self.reg2str, holdingRegisters))) holdingRegisters = self.client.read_holdingregisters(2038,8) configDict["netmask"] = (reduce (lambda a, b: str(a)+str(b), map(self.reg2str, holdingRegisters))) return configDict def reg2utc(self, timel, timeu): return dt.fromtimestamp((timeu << 16) + timel) def reg2bits(self, reg): return bin(reg)[2:].zfill(16) def resetPcs(self): self.checkConnect() self.client.write_single_register(266, 0x8000) def checkOpMode(self): self.checkConnect() val = self.client.read_holdingregisters(267, 1) print (val) return val[0] == 1 def readFaultDetail(self, findex): if not (0<=findex<=63): return None self.checkConnect() self.client.write_single_register(0, findex) regs = self.client.read_holdingregisters(1, 9) flimit, fval, fcount, ftimel, ftimeu = regs[:5] fselector, fstatus = regs[-2:] fselector = bin(fselector)[2:].zfill(16)[-3:] print(bin(fstatus)[2:].zfill(16)) fseverity = bin(fstatus)[2:].zfill(16)[-3:] ''' b0-b2: The severity of the fault. 0x000 = Info: increments the fault counter only. 0x001 = Alert: increments the fault counter only. 0x010 = Alarm: fault is logged. 0x011 = Abort 0: fault is logged and unit is stopped. Reconnect timer 0 is used for restart. 0x100 = Abort 1: fault is logged and unit is stopped. Reconnect timer 1 is used for restart. 0x101 = Abort 2: fault is logged and unit is stopped. Reconnect timer 2 is used for restart. 0x110 = Lockdown: fault is logged, unit stops processing power and requires a reset. 0x111 = Reserved ''' fstatus = bin(fstatus)[2:].zfill(16)[-5:-3] ''' b3-b4: The status of the fault. 0x01 = No fault 0x10 = Active 0x11 = Occurred ''' print(flimit, fval, fcount, self.reg2utc(ftimel, ftimeu), fselector, fstatus, fseverity ) return { "Number": findex, "Limit": flimit, "Value": fval, "Occurence": fcount, "TimeStamp": self.reg2utc(ftimel, ftimeu), "Selector": fselector, "Severity": fseverity, "Status": fstatus, } def readFaultArray(self, start): import operator as op self.checkConnect() faults = self.client.read_holdingregisters(start,4) faultStrings = map(lambda x: bin(x)[2:].zfill(16)[::-1], faults) return reduce(op.add, faultStrings) def readFaultActivity(self): return self.readFaultArray(16) def readFaultOccurence(self): return self.readFaultArray(24) def printAllFaults(self, faults): fmt = " {} || {} | {} | {} | {} | {} | {} | {} | {} " sep = " ================================== " print (fmt.format(" ", *list(range(8)))) print (sep) for i in range(8): idx = i * 8 print (fmt.format(i, *faults[idx:idx+8])) def reconnectTimer(self): pass def getSystemStatus(self): '''Get PCS Status''' regs = self.registers sys_dict = {k:v for k,v in self.reg_info.items() if v.index[0] == 4 and v.index[1] == 10} system_all_status = { k: v.convertValue(regs[v.address]) for k, v in sys_dict.items() } status = system_all_status['system_status'] return selectMessages(status, system_status), system_all_status def getPortStatusAC1(self): '''Get Status of AC1 Power Port''' def relAddr(x): return x - addrStatusAC1 # regs = self.client.read_holdingregisters(addrStatusAC1, nRegsPortStatus) regs = self.registers[addrStatusAC1:addrStatusAC1+nRegsPortStatus] p1_all_status = {"p1_port_status": toBitString(regs[relAddr(96)]), "p1_real_pwr_ramped": toPower(regs[relAddr(100)]), "p1_reactive_pwr_ramped": toVar(regs[relAddr(101)]), "p1_frequency": toFreq(regs[relAddr(105)]), "p1_v_ab_rms": toVoltage(regs[relAddr(109)]), "p1_v_bc_rms": toVoltage(regs[relAddr(110)]), "p1_v_ca_rms": toVoltage(regs[relAddr(111)]), "p1_v_an_rms": toVoltage(regs[relAddr(112)]), "p1_v_bn_rms": toVoltage(regs[relAddr(113)]), "p1_v_cn_rms": toVoltage(regs[relAddr(114)]), "p1_power_factor": toPwrFactor(regs[relAddr(118)]), "p1_real_power": toPower(regs[relAddr(119)]), "p1_reactive_power": toVar(regs[relAddr(120)]), "p1_apparent_power": toVa(regs[relAddr(121)]), "p1_i_a_int_rms": toCurrent(regs[relAddr(122)]), "p1_i_b_int_rms": toCurrent(regs[relAddr(123)]), "p1_i_c_int_rms": toCurrent(regs[relAddr(124)]), "p1_i_a_ext_rms": toCurrent(regs[relAddr(125)]), "p1_i_b_ext_rms": toCurrent(regs[relAddr(126)]), "p1_i_c_ext_rms": toCurrent(regs[relAddr(127)])} status = p1_all_status['p1_port_status'] return selectMessages(status, p1_port_status), p1_all_status def getPortStatusDC2(self): '''Get Status of DC2 Power Port''' def relAddr(x): return x - addrStatusDC2 # regs = self.client.read_holdingregisters(addrStatusDC2, nRegsPortStatus) regs = self.registers[addrStatusDC2:addrStatusDC2+nRegsPortStatus] p2_all_status = {'p2_port_status': toBitString(regs[relAddr(160)]), 'p2_current_ramped': toCurrent(regs[relAddr(164)]), 'p2_power_ramped': toPower(regs[relAddr(165)]), 'p2_voltage_ramped': toVoltage(regs[relAddr(166)]), 'pv_tod_stat': toMinutes(regs[relAddr(167)]), 'p2_pv_restart_stat': regs[relAddr(168)], 'p2_v_pn': toVoltage(regs[relAddr(173)]), 'p2_v_pg': toVoltage(regs[relAddr(176)]), 'dc_com_voltage': toVoltage(regs[relAddr(177)]), 'p2_power': toPower(regs[relAddr(185)]), 'p2_current': toCurrent(regs[relAddr(186)])} status = p2_all_status['p2_port_status'] return selectMessages(status, p2_port_status), p2_all_status def getPortStatusDC3(self): '''Get Status of DC3 Power Port''' def relAddr(x): return x - addrStatusDC3 regs = self.registers[addrStatusDC3:addrStatusDC3+nRegsPortStatus] # regs = self.client.read_holdingregisters(addrStatusDC3, nRegsPortStatus) # self.registers[addrStatusDC3, nRegsPortStatus] = regs p3_all_status = {'p3_port_status': toBitString(regs[relAddr(224)]), 'p3_current_ramped': toCurrent(regs[relAddr(228)]), 'p3_power_ramped': toPower(regs[relAddr(229)]), 'p3_voltage_ramped': toVoltage(regs[relAddr(230)]), 'p3_pv_restart_stat': regs[relAddr(232)], 'p3_v_pn': toVoltage(regs[relAddr(237)]), 'p3_v_pg': toVoltage(regs[relAddr(240)]), 'p3_power': toPower(regs[relAddr(249)]), 'p3_current': toCurrent(regs[relAddr(250)])} status = p3_all_status['p3_port_status'] return selectMessages(status, p3_port_status), p3_all_status