482 lines
17 KiB
Python
482 lines
17 KiB
Python
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
|