webrelay-modbus/stabiliti.py
2020-02-19 14:30:17 -08:00

482 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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