393 lines
14 KiB
Python
393 lines
14 KiB
Python
import easymodbus.modbusClient
|
||
from datetime import datetime as dt
|
||
from functools import reduce
|
||
|
||
nRegsPortStatus = 32
|
||
|
||
addrControlAC1 = 64
|
||
addrStatusAC1 = 96
|
||
|
||
addrControlDC2 = 128
|
||
addrStatusDC2 = 160
|
||
|
||
addrControlDC3 = 192
|
||
addrStatusDC3 = 224
|
||
|
||
addrControlSystem = 0x0100
|
||
addrStatusSystem = 0x0120
|
||
|
||
addrStatusThermal = 0x0160
|
||
|
||
addrStatusDiagnostic = 0x0180
|
||
|
||
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"}
|
||
|
||
|
||
def toPower(x):
|
||
return x * 10
|
||
|
||
def toVar(x):
|
||
return x * 10
|
||
|
||
def toVa(x):
|
||
return x * 10
|
||
|
||
def toPwrFactor(x):
|
||
return x * 0.01
|
||
|
||
def toCurrent(x):
|
||
return x * 0.1
|
||
|
||
def toVoltage(x):
|
||
return x
|
||
|
||
def toFreq(x):
|
||
return x * 0.001
|
||
|
||
def toTemperature(x):
|
||
return 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
|
||
|
||
|
||
class StabilitiRegister(object):
|
||
def __init__(self, name, proplist):
|
||
self.name = name
|
||
self.index = 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 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 = {}
|
||
|
||
def checkConnect(self):
|
||
if not self.client.is_connected():
|
||
self.client.connect()
|
||
|
||
def checkClose(self):
|
||
if self.client.is_connected():
|
||
self.client.close()
|
||
|
||
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'''
|
||
'''\
|
||
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
|
||
'''
|
||
regs = self.client.read_holdingregisters(addrStatusSystem, nRegsPortStatus)
|
||
pass
|
||
|
||
def getPortStatusAC1(self):
|
||
'''Get Status of AC1 Power Port'''
|
||
|
||
def relAddr(x):
|
||
return x - addrStatusAC1
|
||
|
||
regs = self.client.read_holdingregisters(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)
|
||
|
||
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.client.read_holdingregisters(addrStatusDC3, nRegsPortStatus)
|
||
|
||
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
|
||
|