SolarManager unter Versionsverwaltung
Erster Stand der Hintergrundprozesse, die auf der Synology unter /volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins Web-Verzeichnis. Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot), zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat. Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py, gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte. Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein pip, sie muessen neben den Skripten liegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
class MockPort(object):
|
||||
PARITY_NONE = 'N'
|
||||
PARITY_EVEN = 'E'
|
||||
|
||||
def __init__(self, port, baudrate, bytesize, parity, stopbits, xonxoff, timeout):
|
||||
self.connected = True
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.xonxoff = xonxoff
|
||||
self.timeout = timeout
|
||||
|
||||
self.buffer = []
|
||||
self.request = []
|
||||
|
||||
def open(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.connected = False
|
||||
|
||||
def read(self, count):
|
||||
if len(self.buffer) == 0:
|
||||
return b''
|
||||
print(f"MockPort.read: count={count}. Message: {self.buffer[0]}")
|
||||
return self.buffer.pop(0) # get the first element
|
||||
|
||||
def write(self, data):
|
||||
self.request.append(data)
|
||||
|
||||
def flushInput(self):
|
||||
pass
|
||||
|
||||
def _set_buffer(self, resp_list):
|
||||
for bs in resp_list:
|
||||
self.buffer.append(bs)
|
||||
|
||||
def clear_buffer(self):
|
||||
self.buffer = []
|
||||
|
||||
|
||||
def mock_port(port, baudrate, bytesize, parity, stopbits, xonxoff, timeout):
|
||||
return MockPort(port, baudrate, bytesize, parity, stopbits, xonxoff, timeout)
|
||||
@@ -0,0 +1,52 @@
|
||||
class MockSocket(object):
|
||||
def __init__(self):
|
||||
self.connected = False
|
||||
self.timeout = 0
|
||||
self.ipaddr = None
|
||||
self.ipport = None
|
||||
self.buffer = []
|
||||
|
||||
self.request = []
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.timeout = timeout
|
||||
|
||||
def connect(self, ipaddrAndipportTup):
|
||||
self.connected = True
|
||||
self.ipaddr = ipaddrAndipportTup[0]
|
||||
self.ipport = ipaddrAndipportTup[1]
|
||||
|
||||
def close(self):
|
||||
self.connected = False
|
||||
|
||||
def recv(self, size):
|
||||
if len(self.buffer) == 0:
|
||||
return b''
|
||||
print(f"MockSocket.recv: size={size}. Message: {self.buffer[0]}")
|
||||
return self.buffer.pop(0)
|
||||
|
||||
def sendall(self, data):
|
||||
self.request.append(data)
|
||||
|
||||
def _set_buffer(self, resp_list):
|
||||
for bs in resp_list:
|
||||
self.buffer.append(bs)
|
||||
|
||||
def clear_buffer(self):
|
||||
self.buffer = []
|
||||
|
||||
|
||||
def mock_socket(AF_INET, SOCK_STREAM):
|
||||
return MockSocket()
|
||||
|
||||
|
||||
def mock_tcp_connect(self):
|
||||
if self.client.socket is None:
|
||||
self.client.socket = mock_socket('foo', 'bar')
|
||||
self.client.socket.settimeout(999)
|
||||
self.client.socket.connect((999, 999))
|
||||
pass
|
||||
|
||||
|
||||
def mock_tcp_disconnect(self):
|
||||
pass
|
||||
@@ -0,0 +1,612 @@
|
||||
{
|
||||
"name": "device_1547",
|
||||
"models": [
|
||||
{
|
||||
"ID": 1,
|
||||
"Mn": "SunSpecTest",
|
||||
"Md": "Test-1547-1",
|
||||
"Opt": "opt_a_b_c",
|
||||
"Vr": "1.2.3",
|
||||
"SN": "sn-123456789",
|
||||
"DA": 1,
|
||||
"Pad": 0
|
||||
},
|
||||
{
|
||||
"ID": 701,
|
||||
"L": null,
|
||||
"ACType": 3,
|
||||
"St": 2,
|
||||
"Alrm": 0,
|
||||
"W": 9800,
|
||||
"VA": 10000,
|
||||
"Var": 200,
|
||||
"PF": 985,
|
||||
"A": 411,
|
||||
"LLV": 2400,
|
||||
"LNV": 2400,
|
||||
"Hz": 60010,
|
||||
"TotWhInj": 150,
|
||||
"TotWhAbs": 0,
|
||||
"TotVarhInj": 9,
|
||||
"TotVarhAbs": 0,
|
||||
"TmpAmb": 450,
|
||||
"TmpCab": 550,
|
||||
"TmpSnk": 650,
|
||||
"TmpTrns": 500,
|
||||
"TmpSw": 400,
|
||||
"TmpOt": 420,
|
||||
"WL1": 3200,
|
||||
"VAL1": 3333,
|
||||
"VarL1": 80,
|
||||
"PFL1": 984,
|
||||
"AL1": 137,
|
||||
"VL1L2": 120,
|
||||
"VL1": 120,
|
||||
"TotWhInjL1": 49,
|
||||
"TotWhAbsL1": 0,
|
||||
"TotVarhInjL1": 2,
|
||||
"TotVarhAbsL1": 0,
|
||||
"WL2": 3300,
|
||||
"VAL2": 3333,
|
||||
"VarL2": 80,
|
||||
"PFL2": 986,
|
||||
"AL2": 136,
|
||||
"VL2L3": 120,
|
||||
"VL2": 120,
|
||||
"TotWhInjL2": 50,
|
||||
"TotWhAbsL2": 0,
|
||||
"TotVarhInjL2": 3,
|
||||
"TotVarhAbsL2": 0,
|
||||
"WL3": 3500,
|
||||
"VAL3": 3333,
|
||||
"VarL3": 40,
|
||||
"PFL3": 987,
|
||||
"AL3": 138,
|
||||
"VL3L1": 120,
|
||||
"VL3N": 120,
|
||||
"TotWhInjL3": 51,
|
||||
"TotWhAbsL3": 0,
|
||||
"TotVarhInjL3": 4,
|
||||
"TotVarhAbsL3": 0,
|
||||
"A_SF": -1,
|
||||
"V_SF": -1,
|
||||
"Hz_SF": -3,
|
||||
"W_SF": 0,
|
||||
"PF_SF": -3,
|
||||
"VA_SF": 0,
|
||||
"Var_SF": 0,
|
||||
"TotWh_SF": 3,
|
||||
"TotVarh_SF": 3,
|
||||
"Tmp_SF": -1
|
||||
},
|
||||
{
|
||||
"ID": 702,
|
||||
"L": null,
|
||||
"WMaxRtg": 10000,
|
||||
"WOvrExtRtg": 10000,
|
||||
"WOvrExtRtgPF": 1000,
|
||||
"WUndExtRtg": 10000,
|
||||
"WUndExtRtgPF": 1000,
|
||||
"VAMaxRtg": 11000,
|
||||
"VarMaxInjRtg": 2500,
|
||||
"VarMaxAbsRtg": 0,
|
||||
"WChaRteMaxRtg": 0,
|
||||
"WDisChaRteMaxRtg": 0,
|
||||
"VAChaRteMaxRtg": 0,
|
||||
"VADisChaRteMaxRtg": 0,
|
||||
"VNomRtg": 240,
|
||||
"VMaxRtg": 270,
|
||||
"VMinRtg": 210,
|
||||
"AMaxRtg": 50,
|
||||
"PFOvrExtRtg": 850,
|
||||
"PFUndExtRtg": 850,
|
||||
"ReactSusceptRtg": null,
|
||||
"NorOpCatRtg": 2,
|
||||
"AbnOpCatRtg": 3,
|
||||
"CtrlModes": null,
|
||||
"IntIslandCatRtg": null,
|
||||
"WMax": 10000,
|
||||
"WMaxOvrExt": null,
|
||||
"WOvrExtPF": null,
|
||||
"WMaxUndExt": null,
|
||||
"WUndExtPF": null,
|
||||
"VAMax": 10000,
|
||||
"AMax": null,
|
||||
"Vnom": null,
|
||||
"VRefOfs": null,
|
||||
"VMax": null,
|
||||
"VMin": null,
|
||||
"VarMaxInj": null,
|
||||
"VarMaxAbs": null,
|
||||
"WChaRteMax": null,
|
||||
"WDisChaRteMax": null,
|
||||
"VAChaRteMax": null,
|
||||
"VADisChaRteMax": null,
|
||||
"IntIslandCat": null,
|
||||
"W_SF": 0,
|
||||
"PF_SF": -3,
|
||||
"VA_SF": 0,
|
||||
"Var_SF": 0,
|
||||
"V_SF": 0,
|
||||
"A_SF": 0,
|
||||
"S_SF": 0
|
||||
},
|
||||
{
|
||||
"ID": 703,
|
||||
"ES": 1,
|
||||
"ESVHi": 1050,
|
||||
"ESVLo": 917,
|
||||
"ESHzHi": 6010,
|
||||
"ESHzLo": 5950,
|
||||
"ESDlyTms": 300,
|
||||
"ESRndTms": 100,
|
||||
"ESRmpTms": 60,
|
||||
"V_SF": -3,
|
||||
"Hz_SF": -2
|
||||
},
|
||||
{
|
||||
"ID": 704,
|
||||
"L": null,
|
||||
"PFWInjEna": 0,
|
||||
"PFWInjEnaRvrt": null,
|
||||
"PFWInjRvrtTms": null,
|
||||
"PFWInjRvrtRem": null,
|
||||
"PFWAbsEna": 0,
|
||||
"PFWAbsEnaRvrt": null,
|
||||
"PFWAbsRvrtTms": null,
|
||||
"PFWAbsRvrtRem": null,
|
||||
"WMaxLimEna": 0,
|
||||
"WMaxLim": 1000,
|
||||
"WMaxLimRvrt": null,
|
||||
"WMaxLimEnaRvrt": null,
|
||||
"WMaxLimRvrtTms": null,
|
||||
"WMaxLimRvrtRem": null,
|
||||
"WSetEna": null,
|
||||
"WSetMod": null,
|
||||
"WSet": null,
|
||||
"WSetRvrt": null,
|
||||
"WSetPct": null,
|
||||
"WSetPctRvrt": null,
|
||||
"WSetEnaRvrt": null,
|
||||
"WSetRvrtTms": null,
|
||||
"WSetRvrtRem": null,
|
||||
"VarSetEna": null,
|
||||
"VarSetMod": null,
|
||||
"VarSetPri": null,
|
||||
"VarSet": null,
|
||||
"VarSetRvrt": null,
|
||||
"VarSetPct": null,
|
||||
"VarSetPctRvrt": null,
|
||||
"VarSetRvrtTms": null,
|
||||
"VarSetRvrtRem": null,
|
||||
"RGra": null,
|
||||
"PF_SF": -3,
|
||||
"WMaxLim_SF": -1,
|
||||
"WSet_SF": null,
|
||||
"WSetPct_SF": null,
|
||||
"VarSet_SF": null,
|
||||
"VarSetPct_SF": null,
|
||||
"PFWInj": {
|
||||
"PF": 950,
|
||||
"Ext": 1
|
||||
},
|
||||
"PFWInjRvrt": {
|
||||
"PF": null,
|
||||
"Ext": null
|
||||
},
|
||||
"PFWAbs": {
|
||||
"PF": null,
|
||||
"Ext": null
|
||||
},
|
||||
"PFWAbsRvrt": {
|
||||
"PF": null,
|
||||
"Ext": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 705,
|
||||
"Ena": 1,
|
||||
"CrvSt": 1,
|
||||
"AdptCrvReq": 0,
|
||||
"AdptCrvRslt": 0,
|
||||
"NPt": 4,
|
||||
"NCrv": 3,
|
||||
"RvrtTms": 0,
|
||||
"RvrtRem": 0,
|
||||
"RvrtCrv": 0,
|
||||
"V_SF": -2,
|
||||
"DeptRef_SF": -2,
|
||||
"Crv": [
|
||||
{
|
||||
"ActPt": 4,
|
||||
"DeptRef": 1,
|
||||
"Pri": 1,
|
||||
"VRef": 1,
|
||||
"VRefAuto": 0,
|
||||
"VRefTms": 5,
|
||||
"RspTms": 6,
|
||||
"ReadOnly": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 9200,
|
||||
"Var": 3000
|
||||
},
|
||||
{
|
||||
"V": 9670,
|
||||
"Var": 0
|
||||
},
|
||||
{
|
||||
"V": 10300,
|
||||
"Var": 0
|
||||
},
|
||||
{
|
||||
"V": 10700,
|
||||
"Var": -3000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ActPt": 4,
|
||||
"DeptRef": 1,
|
||||
"Pri": 1,
|
||||
"VRef": 1,
|
||||
"VRefAuto": 0,
|
||||
"VRefTms": 5,
|
||||
"RspTms": 6,
|
||||
"ReadOnly": 0,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 9300,
|
||||
"Var": 3000
|
||||
},
|
||||
{
|
||||
"V": 9570,
|
||||
"Var": 0
|
||||
},
|
||||
{
|
||||
"V": 10200,
|
||||
"Var": 0
|
||||
},
|
||||
{
|
||||
"V": 10600,
|
||||
"Var": -4000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ActPt": 4,
|
||||
"DeptRef": 1,
|
||||
"Pri": 1,
|
||||
"VRef": 1,
|
||||
"VRefAuto": 0,
|
||||
"VRefTms": 5,
|
||||
"RspTms": 6,
|
||||
"ReadOnly": 0,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 9400,
|
||||
"Var": 2000
|
||||
},
|
||||
{
|
||||
"V": 9570,
|
||||
"Var": 0
|
||||
},
|
||||
{
|
||||
"V": 10500,
|
||||
"Var": 0
|
||||
},
|
||||
{
|
||||
"V": 10800,
|
||||
"Var": -2000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 706,
|
||||
"Ena": 0,
|
||||
"CrvSt": 1,
|
||||
"AdptCrvReq": 0,
|
||||
"AdptCrvRslt": 0,
|
||||
"NPt": 2,
|
||||
"NCrv": 2,
|
||||
"RvrtTms": null,
|
||||
"RvrtRem": null,
|
||||
"RvrtCrv": null,
|
||||
"V_SF": 0,
|
||||
"DeptRef_SF": 0,
|
||||
"Crv": [
|
||||
{
|
||||
"ActPt": 2,
|
||||
"DeptRef": 1,
|
||||
"RspTms": 10,
|
||||
"ReadOnly": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 106,
|
||||
"W": 100
|
||||
},
|
||||
{
|
||||
"V": 110,
|
||||
"W": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ActPt": 2,
|
||||
"DeptRef": 1,
|
||||
"RspTms": 5,
|
||||
"ReadOnly": 0,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 105,
|
||||
"W": 100
|
||||
},
|
||||
{
|
||||
"V": 109,
|
||||
"W": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 707,
|
||||
"L": null,
|
||||
"Ena": 1,
|
||||
"CrvSt": null,
|
||||
"AdptCrvReq": null,
|
||||
"AdptCrvRslt": null,
|
||||
"NPt": 1,
|
||||
"NCrvSet": 1,
|
||||
"V_SF": -2,
|
||||
"Tms_SF": 0,
|
||||
"Crv": [
|
||||
{
|
||||
"MustTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 5000,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MayTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 7000,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MomCess": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 6000,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 708,
|
||||
"L": null,
|
||||
"Ena": 1,
|
||||
"CrvSt": null,
|
||||
"AdptCrvReq": null,
|
||||
"AdptCrvRslt": null,
|
||||
"NPt": 1,
|
||||
"NCrvSet": 1,
|
||||
"V_SF": -2,
|
||||
"Tms_SF": 0,
|
||||
"Crv": [
|
||||
{
|
||||
"MustTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 12000,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MayTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 10000,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MomCess": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"V": 10000,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 709,
|
||||
"L": null,
|
||||
"Ena": 1,
|
||||
"CrvSt": null,
|
||||
"AdptCrvReq": null,
|
||||
"AdptCrvRslt": null,
|
||||
"NPt": 1,
|
||||
"NCrvSet": 1,
|
||||
"Freq_SF": null,
|
||||
"Tms_SF": -2,
|
||||
"Crv": [
|
||||
{
|
||||
"MustTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"Freq": 5300,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MayTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"Freq": 5850,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MomCess": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"Freq": 5850,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 710,
|
||||
"L": null,
|
||||
"Ena": null,
|
||||
"CrvSt": null,
|
||||
"AdptCrvReq": null,
|
||||
"AdptCrvRslt": null,
|
||||
"NPt": 1,
|
||||
"NCrvSet": 1,
|
||||
"Freq_SF": null,
|
||||
"Tms_SF": -2,
|
||||
"Crv": [
|
||||
{
|
||||
"MustTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"Freq": 6500,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MayTrip": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"Freq": 6050,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"MomCess": {
|
||||
"ActPt": 1,
|
||||
"Pt": [
|
||||
{
|
||||
"Freq": 6050,
|
||||
"Tms": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 711,
|
||||
"L": null,
|
||||
"Ena": null,
|
||||
"CrvSt": null,
|
||||
"AdptCrvReq": null,
|
||||
"AdptCrvRslt": null,
|
||||
"NCtl": 1,
|
||||
"RvrtTms": 0,
|
||||
"RvrtRem": 0,
|
||||
"RvrtCrv": 0,
|
||||
"Db_SF": -2,
|
||||
"K_SF": -2,
|
||||
"RspTms_SF": 0,
|
||||
"Ctl": [
|
||||
{
|
||||
"DbOf": 60030,
|
||||
"DbUf": 59970,
|
||||
"KOf": 40,
|
||||
"KUf": 40,
|
||||
"RspTms": 600
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 712,
|
||||
"L": null,
|
||||
"Ena": null,
|
||||
"CrvSt": null,
|
||||
"AdptCrvReq": null,
|
||||
"AdptCrvRslt": null,
|
||||
"NPt": 1,
|
||||
"NCrv": 1,
|
||||
"RvrtTms": 0,
|
||||
"RvrtRem": 0,
|
||||
"RvrtCrv": 0,
|
||||
"DeptRef_SF": -2,
|
||||
"Crv": [
|
||||
{
|
||||
"ActPt": 1,
|
||||
"DeptRef": null,
|
||||
"Pri": null,
|
||||
"ReadOnly": null,
|
||||
"Pt": [
|
||||
{
|
||||
"W": null,
|
||||
"Var": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 713,
|
||||
"L": null,
|
||||
"PrtAlrms": null,
|
||||
"NPrt": 1,
|
||||
"DCA": null,
|
||||
"DCW": null,
|
||||
"DCWhInj": null,
|
||||
"DCWhAbs": null,
|
||||
"DCA_SF": null,
|
||||
"DCV_SF": null,
|
||||
"DCW_SF": null,
|
||||
"DCWH_SF": null,
|
||||
"Prt": [
|
||||
{
|
||||
"PrtTyp": null,
|
||||
"ID": null,
|
||||
"IDStr": null,
|
||||
"DCA": null,
|
||||
"DCV": null,
|
||||
"DCW": null,
|
||||
"DCWhInj": null,
|
||||
"DCWhAbs": null,
|
||||
"Tmp": null,
|
||||
"DCSt": null,
|
||||
"DCAlrm": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 65535,
|
||||
"L": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"name": null,
|
||||
"did": "fa0a9f2d-d503-470e-8ce8-ec9c51428829",
|
||||
"models": [
|
||||
{
|
||||
"ID": 1,
|
||||
"L": 66,
|
||||
"Mn": "Device Manufacturer",
|
||||
"Md": "Inverter 123",
|
||||
"Opt": null,
|
||||
"Vr": "v0.0.1",
|
||||
"SN": "9999abcd",
|
||||
"DA": null,
|
||||
"Pad": 32768
|
||||
},
|
||||
{
|
||||
"ID": 129,
|
||||
"L": 210,
|
||||
"ActCrv": 1,
|
||||
"ModEna": 0,
|
||||
"WinTms": null,
|
||||
"RvrtTms": null,
|
||||
"RmpTms": null,
|
||||
"NCrv": 4,
|
||||
"NPt": 10,
|
||||
"Tms_SF": -2,
|
||||
"V_SF": -1,
|
||||
"Pad": 32768,
|
||||
"curve": [
|
||||
{
|
||||
"ActPt": 4,
|
||||
"Tms1": 200,
|
||||
"V1": 880,
|
||||
"Tms2": 71,
|
||||
"V2": 650,
|
||||
"Tms3": 20,
|
||||
"V3": 450,
|
||||
"Tms4": 0,
|
||||
"V4": 300,
|
||||
"Tms5": 0,
|
||||
"V5": 0,
|
||||
"Tms6": 0,
|
||||
"V6": 0,
|
||||
"Tms7": 0,
|
||||
"V7": 0,
|
||||
"Tms8": 0,
|
||||
"V8": 0,
|
||||
"Tms9": 0,
|
||||
"V9": 0,
|
||||
"Tms10": 0,
|
||||
"V10": 0,
|
||||
"Tms11": null,
|
||||
"V11": null,
|
||||
"Tms12": null,
|
||||
"V12": null,
|
||||
"Tms13": null,
|
||||
"V13": null,
|
||||
"Tms14": null,
|
||||
"V14": null,
|
||||
"Tms15": null,
|
||||
"V15": null,
|
||||
"Tms16": null,
|
||||
"V16": null,
|
||||
"Tms17": null,
|
||||
"V17": null,
|
||||
"Tms18": null,
|
||||
"V18": null,
|
||||
"Tms19": null,
|
||||
"V19": null,
|
||||
"Tms20": null,
|
||||
"V20": null,
|
||||
"CrvNam": null,
|
||||
"ReadOnly": 1
|
||||
},
|
||||
{
|
||||
"ActPt": 0,
|
||||
"Tms1": 0,
|
||||
"V1": 0,
|
||||
"Tms2": 0,
|
||||
"V2": 0,
|
||||
"Tms3": 0,
|
||||
"V3": 0,
|
||||
"Tms4": 0,
|
||||
"V4": 0,
|
||||
"Tms5": 0,
|
||||
"V5": 0,
|
||||
"Tms6": 0,
|
||||
"V6": 0,
|
||||
"Tms7": 0,
|
||||
"V7": 0,
|
||||
"Tms8": 0,
|
||||
"V8": 0,
|
||||
"Tms9": 0,
|
||||
"V9": 0,
|
||||
"Tms10": 0,
|
||||
"V10": 0,
|
||||
"Tms11": null,
|
||||
"V11": null,
|
||||
"Tms12": null,
|
||||
"V12": null,
|
||||
"Tms13": null,
|
||||
"V13": null,
|
||||
"Tms14": null,
|
||||
"V14": null,
|
||||
"Tms15": null,
|
||||
"V15": null,
|
||||
"Tms16": null,
|
||||
"V16": null,
|
||||
"Tms17": null,
|
||||
"V17": null,
|
||||
"Tms18": null,
|
||||
"V18": null,
|
||||
"Tms19": null,
|
||||
"V19": null,
|
||||
"Tms20": null,
|
||||
"V20": null,
|
||||
"CrvNam": null,
|
||||
"ReadOnly": 0
|
||||
},
|
||||
{
|
||||
"ActPt": 0,
|
||||
"Tms1": 0,
|
||||
"V1": 0,
|
||||
"Tms2": 0,
|
||||
"V2": 0,
|
||||
"Tms3": 0,
|
||||
"V3": 0,
|
||||
"Tms4": 0,
|
||||
"V4": 0,
|
||||
"Tms5": 0,
|
||||
"V5": 0,
|
||||
"Tms6": 0,
|
||||
"V6": 0,
|
||||
"Tms7": 0,
|
||||
"V7": 0,
|
||||
"Tms8": 0,
|
||||
"V8": 0,
|
||||
"Tms9": 0,
|
||||
"V9": 0,
|
||||
"Tms10": 0,
|
||||
"V10": 0,
|
||||
"Tms11": null,
|
||||
"V11": null,
|
||||
"Tms12": null,
|
||||
"V12": null,
|
||||
"Tms13": null,
|
||||
"V13": null,
|
||||
"Tms14": null,
|
||||
"V14": null,
|
||||
"Tms15": null,
|
||||
"V15": null,
|
||||
"Tms16": null,
|
||||
"V16": null,
|
||||
"Tms17": null,
|
||||
"V17": null,
|
||||
"Tms18": null,
|
||||
"V18": null,
|
||||
"Tms19": null,
|
||||
"V19": null,
|
||||
"Tms20": null,
|
||||
"V20": null,
|
||||
"CrvNam": null,
|
||||
"ReadOnly": 0
|
||||
},
|
||||
{
|
||||
"ActPt": 0,
|
||||
"Tms1": 0,
|
||||
"V1": 0,
|
||||
"Tms2": 0,
|
||||
"V2": 0,
|
||||
"Tms3": 0,
|
||||
"V3": 0,
|
||||
"Tms4": 0,
|
||||
"V4": 0,
|
||||
"Tms5": 0,
|
||||
"V5": 0,
|
||||
"Tms6": 0,
|
||||
"V6": 0,
|
||||
"Tms7": 0,
|
||||
"V7": 0,
|
||||
"Tms8": 0,
|
||||
"V8": 0,
|
||||
"Tms9": 0,
|
||||
"V9": 0,
|
||||
"Tms10": 0,
|
||||
"V10": 0,
|
||||
"Tms11": null,
|
||||
"V11": null,
|
||||
"Tms12": null,
|
||||
"V12": null,
|
||||
"Tms13": null,
|
||||
"V13": null,
|
||||
"Tms14": null,
|
||||
"V14": null,
|
||||
"Tms15": null,
|
||||
"V15": null,
|
||||
"Tms16": null,
|
||||
"V16": null,
|
||||
"Tms17": null,
|
||||
"V17": null,
|
||||
"Tms18": null,
|
||||
"V18": null,
|
||||
"Tms19": null,
|
||||
"V19": null,
|
||||
"Tms20": null,
|
||||
"V20": null,
|
||||
"CrvNam": null,
|
||||
"ReadOnly": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Address Offset,Group Offset,Name,Value,Count,Type,Size,Scale Factor,Units,RW Access (RW),Mandatory (M),Static (S),Label,Description,Detailed Description,Standards
|
||||
,,inclinometer,,,group,,,,,,,Inclinometer Model,Include to support orientation measurements,,
|
||||
0,,ID,304,,uint16,,,,,M,S,Model ID,Model identifier,,
|
||||
1,,L,,,uint16,,,,,M,S,Model Length,Model length,,
|
||||
,,inclinometer.incl,,0,group,,,,,,,,,,
|
||||
,0,Inclx,,,int32,,-2,Degrees,,M,,X,X-Axis inclination,,
|
||||
,2,Incly,,,int32,,-2,Degrees,,,,Y,Y-Axis inclination,,
|
||||
,4,Inclz,,,int32,,-2,Degrees,,,,Z,Z-Axis inclination,,
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,221 @@
|
||||
import sunspec2.mb as mb
|
||||
import pytest
|
||||
|
||||
|
||||
def test_create_unimpl_value():
|
||||
with pytest.raises(ValueError):
|
||||
mb.create_unimpl_value(None)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
mb.create_unimpl_value('string')
|
||||
|
||||
assert mb.create_unimpl_value('string', len=8) == b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('ipv6addr') == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('int16') == b'\x80\x00'
|
||||
assert mb.create_unimpl_value('uint16') == b'\xff\xff'
|
||||
assert mb.create_unimpl_value('acc16') == b'\x00\x00'
|
||||
assert mb.create_unimpl_value('enum16') == b'\xff\xff'
|
||||
assert mb.create_unimpl_value('bitfield16') == b'\xff\xff'
|
||||
assert mb.create_unimpl_value('int32') == b'\x80\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('uint32') == b'\xff\xff\xff\xff'
|
||||
assert mb.create_unimpl_value('acc32') == b'\x00\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('enum32') == b'\xff\xff\xff\xff'
|
||||
assert mb.create_unimpl_value('bitfield32') == b'\xff\xff\xff\xff'
|
||||
assert mb.create_unimpl_value('ipaddr') == b'\x00\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('int64') == b'\x80\x00\x00\x00\x00\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('uint64') == b'\xff\xff\xff\xff\xff\xff\xff\xff'
|
||||
assert mb.create_unimpl_value('acc64') == b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
assert mb.create_unimpl_value('float32') == b'N\xff\x80\x00'
|
||||
assert mb.create_unimpl_value('sunssf') == b'\x80\x00'
|
||||
assert mb.create_unimpl_value('eui48') == b'\x00\x00\xff\xff\xff\xff\xff\xff'
|
||||
assert mb.create_unimpl_value('pad') == b'\x00\x00'
|
||||
|
||||
|
||||
def test_data_to_s16():
|
||||
assert mb.data_to_s16(b'\x13\x88') == 5000
|
||||
|
||||
|
||||
def test_data_to_u16():
|
||||
assert mb.data_to_u16(b'\x27\x10') == 10000
|
||||
|
||||
|
||||
def test_data_to_s32():
|
||||
assert mb.data_to_s32(b'\x12\x34\x56\x78') == 305419896
|
||||
assert mb.data_to_s32(b'\xED\xCB\xA9\x88') == -305419896
|
||||
|
||||
|
||||
def test_data_to_u32():
|
||||
assert mb.data_to_u32(b'\x12\x34\x56\x78') == 305419896
|
||||
|
||||
|
||||
def test_data_to_s64():
|
||||
assert mb.data_to_s64(b'\x12\x34\x56\x78\x12\x34\x56\x78') == 1311768465173141112
|
||||
assert mb.data_to_s64(b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88') == -1311768465173141112
|
||||
|
||||
|
||||
def test_data_to_u64():
|
||||
assert mb.data_to_u64(b'\xff\xff\xff\xff\xff\xff\xff\xff') == 18446744073709551615
|
||||
|
||||
|
||||
def test_data_to_ipv6addr():
|
||||
assert mb.data_to_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') == '20010DB8:85A30000:00008A2E:03707334'
|
||||
|
||||
|
||||
def test_data_to_eui48():
|
||||
# need test to test for python 2
|
||||
assert mb.data_to_eui48(b'\x00\x00\x12\x34\x56\x78\x90\xAB') == '12:34:56:78:90:AB'
|
||||
|
||||
|
||||
def test_data_to_f64():
|
||||
assert mb.data_to_f64(b'\x44\x9a\x43\xf3\x00\x00\x00\x00') == 3.1008742600725133e+22
|
||||
|
||||
|
||||
def test_data_to_str():
|
||||
assert mb.data_to_str(b'test') == 'test'
|
||||
assert mb.data_to_str(b'444444') == '444444'
|
||||
|
||||
|
||||
def test_s16_to_data():
|
||||
assert mb.s16_to_data(5000) == b'\x13\x88'
|
||||
|
||||
|
||||
def test_u16_to_data():
|
||||
assert mb.u16_to_data(10000) == b'\x27\x10'
|
||||
|
||||
|
||||
def test_s32_to_data():
|
||||
assert mb.s32_to_data(305419896) == b'\x12\x34\x56\x78'
|
||||
assert mb.s32_to_data(-305419896) == b'\xED\xCB\xA9\x88'
|
||||
|
||||
|
||||
def test_u32_to_data():
|
||||
assert mb.u32_to_data(305419896) == b'\x12\x34\x56\x78'
|
||||
|
||||
|
||||
def test_s64_to_data():
|
||||
assert mb.s64_to_data(1311768465173141112) == b'\x12\x34\x56\x78\x12\x34\x56\x78'
|
||||
assert mb.s64_to_data(-1311768465173141112) == b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88'
|
||||
|
||||
|
||||
def test_u64_to_data():
|
||||
assert mb.u64_to_data(18446744073709551615) == b'\xff\xff\xff\xff\xff\xff\xff\xff'
|
||||
|
||||
|
||||
def test_ipv6addr_to_data():
|
||||
assert mb.ipv6addr_to_data('20010DB8:85A30000:00008A2E:03707334') == \
|
||||
b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34'
|
||||
# need additional test to test for python 2
|
||||
|
||||
|
||||
def test_f32_to_data():
|
||||
assert mb.f32_to_data(32500.43359375) == b'F\xfd\xe8\xde'
|
||||
|
||||
|
||||
def test_f64_to_data():
|
||||
assert mb.f64_to_data(3.1008742600725133e+22) == b'\x44\x9a\x43\xf3\x00\x00\x00\x00'
|
||||
|
||||
|
||||
def test_str_to_data():
|
||||
assert mb.str_to_data('test') == b'test'
|
||||
assert mb.str_to_data('444444') == b'444444'
|
||||
assert mb.str_to_data('test', 5) == b'test\x00'
|
||||
|
||||
|
||||
def test_eui48_to_data():
|
||||
assert mb.eui48_to_data('12:34:56:78:90:AB') == b'\x00\x00\x12\x34\x56\x78\x90\xAB'
|
||||
|
||||
|
||||
def test_is_impl_int16():
|
||||
assert not mb.is_impl_int16(-32768)
|
||||
assert mb.is_impl_int16(1111)
|
||||
assert mb.is_impl_int16(None)
|
||||
|
||||
|
||||
def test_is_impl_uint16():
|
||||
assert not mb.is_impl_uint16(0xffff)
|
||||
assert mb.is_impl_uint16(0x1111)
|
||||
|
||||
|
||||
def test_is_impl_acc16():
|
||||
assert not mb.is_impl_acc16(0)
|
||||
assert mb.is_impl_acc16(1111)
|
||||
|
||||
|
||||
def test_is_impl_enum16():
|
||||
assert not mb.is_impl_enum16(0xffff)
|
||||
assert mb.is_impl_enum16(0x1111)
|
||||
|
||||
|
||||
def test_is_impl_bitfield16():
|
||||
assert not mb.is_impl_bitfield16(0xffff)
|
||||
assert mb.is_impl_bitfield16(0x1111)
|
||||
|
||||
|
||||
def test_is_impl_int32():
|
||||
assert not mb.is_impl_int32(-2147483648)
|
||||
assert mb.is_impl_int32(1111111)
|
||||
|
||||
|
||||
def test_is_impl_uint32():
|
||||
assert not mb.is_impl_uint32(0xffffffff)
|
||||
assert mb.is_impl_uint32(0x11111111)
|
||||
|
||||
|
||||
def test_is_impl_acc32():
|
||||
assert not mb.is_impl_acc32(0)
|
||||
assert mb.is_impl_acc32(1)
|
||||
|
||||
|
||||
def test_is_impl_enum32():
|
||||
assert not mb.is_impl_enum32(0xffffffff)
|
||||
assert mb.is_impl_enum32(0x11111111)
|
||||
|
||||
|
||||
def test_is_impl_bitfield32():
|
||||
assert not mb.is_impl_bitfield32(0xffffffff)
|
||||
assert mb.is_impl_bitfield32(0x11111111)
|
||||
|
||||
|
||||
def test_is_impl_ipaddr():
|
||||
assert not mb.is_impl_ipaddr(0)
|
||||
assert mb.is_impl_ipaddr('192.168.0.1')
|
||||
|
||||
|
||||
def test_is_impl_int64():
|
||||
assert not mb.is_impl_int64(-9223372036854775808)
|
||||
assert mb.is_impl_int64(111111111111111)
|
||||
|
||||
|
||||
def test_is_impl_uint64():
|
||||
assert not mb.is_impl_uint64(0xffffffffffffffff)
|
||||
assert mb.is_impl_uint64(0x1111111111111111)
|
||||
|
||||
|
||||
def test_is_impl_acc64():
|
||||
assert not mb.is_impl_acc64(0)
|
||||
assert mb.is_impl_acc64(1)
|
||||
|
||||
|
||||
def test_is_impl_ipv6addr():
|
||||
assert not mb.is_impl_ipv6addr('\0')
|
||||
assert mb.is_impl_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34')
|
||||
|
||||
|
||||
def test_is_impl_float32():
|
||||
assert not mb.is_impl_float32(None)
|
||||
assert mb.is_impl_float32(0x123456)
|
||||
|
||||
|
||||
def test_is_impl_string():
|
||||
assert not mb.is_impl_string('\0')
|
||||
assert mb.is_impl_string(b'\x74\x65\x73\x74')
|
||||
|
||||
|
||||
def test_is_impl_sunssf():
|
||||
assert not mb.is_impl_sunssf(-32768)
|
||||
assert mb.is_impl_sunssf(30000)
|
||||
|
||||
|
||||
def test_is_impl_eui48():
|
||||
assert not mb.is_impl_eui48('FF:FF:FF:FF:FF:FF')
|
||||
assert mb.is_impl_eui48('00:00:00:00:00:00')
|
||||
@@ -0,0 +1,313 @@
|
||||
import sunspec2.mdef as mdef
|
||||
import json
|
||||
import copy
|
||||
import pytest
|
||||
|
||||
|
||||
def test_to_int():
|
||||
assert mdef.to_int('4') == 4
|
||||
assert isinstance(mdef.to_int('4'), int)
|
||||
assert isinstance(mdef.to_int(4.0), int)
|
||||
|
||||
|
||||
def test_to_str():
|
||||
assert mdef.to_str(4) == '4'
|
||||
assert isinstance(mdef.to_str('4'), str)
|
||||
|
||||
|
||||
def test_to_float():
|
||||
assert mdef.to_float('4') == 4.0
|
||||
assert isinstance(mdef.to_float('4'), float)
|
||||
assert mdef.to_float('z') is None
|
||||
|
||||
|
||||
def test_to_number_type():
|
||||
assert mdef.to_number_type('4') == 4
|
||||
assert mdef.to_number_type('4.0') == 4.0
|
||||
assert mdef.to_number_type('z') == 'z'
|
||||
|
||||
|
||||
def test_validate_find_point():
|
||||
with open('./sunspec2/models/json/model_702.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
assert mdef.validate_find_point(model_json['group'], 'ID') == model_json['group']['points'][0]
|
||||
assert mdef.validate_find_point(model_json['group'], 'abc') is None
|
||||
|
||||
|
||||
def test_validate_attrs():
|
||||
with open('./sunspec2/models/json/model_701.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
# model
|
||||
assert mdef.validate_attrs(model_json, mdef.model_attr) == ''
|
||||
|
||||
model_unexp_attr_err = copy.deepcopy(model_json)
|
||||
model_unexp_attr_err['abc'] = 'def'
|
||||
assert mdef.validate_attrs(model_unexp_attr_err, mdef.model_attr)[0:37] == 'Unexpected model definition attribute'
|
||||
|
||||
model_unexp_type_err = copy.deepcopy(model_json)
|
||||
model_unexp_type_err['id'] = '701'
|
||||
assert mdef.validate_attrs(model_unexp_type_err, mdef.model_attr)[0:15] == 'Unexpected type'
|
||||
|
||||
model_attr_missing = copy.deepcopy(model_json)
|
||||
del model_attr_missing['id']
|
||||
assert mdef.validate_attrs(model_attr_missing, mdef.model_attr)[0:27] == 'Mandatory attribute missing'
|
||||
|
||||
# group
|
||||
assert mdef.validate_attrs(model_json['group'], mdef.group_attr) == ''
|
||||
group_unexp_attr_err = copy.deepcopy(model_json)['group']
|
||||
group_unexp_attr_err['abc'] = 'def'
|
||||
assert mdef.validate_attrs(group_unexp_attr_err, mdef.group_attr)[0:37] == 'Unexpected model definition attribute'
|
||||
|
||||
group_unexp_type_err = copy.deepcopy(model_json)['group']
|
||||
group_unexp_type_err['name'] = 1
|
||||
assert mdef.validate_attrs(group_unexp_type_err, mdef.group_attr)[0:15] == 'Unexpected type'
|
||||
|
||||
group_attr_missing = copy.deepcopy(model_json)['group']
|
||||
del group_attr_missing['name']
|
||||
assert mdef.validate_attrs(group_attr_missing, mdef.group_attr)[0:27] == 'Mandatory attribute missing'
|
||||
|
||||
# point
|
||||
assert mdef.validate_attrs(model_json['group']['points'][0], mdef.point_attr) == ''
|
||||
|
||||
point_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][0]
|
||||
point_unexp_attr_err['abc'] = 'def'
|
||||
assert mdef.validate_attrs(point_unexp_attr_err, mdef.point_attr)[0:37] == 'Unexpected model definition attribute'
|
||||
|
||||
point_unexp_type_err = copy.deepcopy(model_json)['group']['points'][0]
|
||||
point_unexp_type_err['name'] = 1
|
||||
assert mdef.validate_attrs(point_unexp_type_err, mdef.point_attr)[0:15] == 'Unexpected type'
|
||||
|
||||
point_unexp_value_err = copy.deepcopy(model_json)['group']['points'][1]
|
||||
point_unexp_value_err['access'] = 'z'
|
||||
assert mdef.validate_attrs(point_unexp_value_err, mdef.point_attr)[0:16] == 'Unexpected value'
|
||||
|
||||
point_attr_missing = copy.deepcopy(model_json)['group']['points'][0]
|
||||
del point_attr_missing['name']
|
||||
assert mdef.validate_attrs(point_attr_missing, mdef.point_attr)[0:27] == 'Mandatory attribute missing'
|
||||
|
||||
# symbol
|
||||
assert mdef.validate_attrs(model_json['group']['points'][2]['symbols'][0], mdef.symbol_attr) == ''
|
||||
|
||||
symbol_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0]
|
||||
symbol_unexp_attr_err['abc'] = 'def'
|
||||
assert mdef.validate_attrs(symbol_unexp_attr_err, mdef.symbol_attr)[0:37] == 'Unexpected model definition attribute'
|
||||
|
||||
symbol_unexp_type_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0]
|
||||
symbol_unexp_type_err['name'] = 1
|
||||
assert mdef.validate_attrs(symbol_unexp_type_err, mdef.symbol_attr)[0:15] == 'Unexpected type'
|
||||
|
||||
symbol_attr_missing = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0]
|
||||
del symbol_attr_missing['name']
|
||||
assert mdef.validate_attrs(symbol_attr_missing, mdef.symbol_attr)[0:27] == 'Mandatory attribute missing'
|
||||
|
||||
|
||||
def test_validate_group_point_dup():
|
||||
with open('./sunspec2/models/json/model_704.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
assert mdef.validate_group_point_dup(model_json['group']) == ''
|
||||
|
||||
dup_group_id_model = copy.deepcopy(model_json)
|
||||
dup_group_id_group = dup_group_id_model['group']
|
||||
dup_group_id_group['groups'][0]['name'] = 'PFWInjRvrt'
|
||||
assert mdef.validate_group_point_dup(dup_group_id_group)[0:18] == 'Duplicate group id'
|
||||
|
||||
dup_group_point_id_model = copy.deepcopy(model_json)
|
||||
dup_group_point_id_group = dup_group_point_id_model['group']
|
||||
dup_group_point_id_group['groups'][0]['name'] = 'PFWInjEna'
|
||||
assert mdef.validate_group_point_dup(dup_group_point_id_group)[0:28] == 'Duplicate group and point id'
|
||||
|
||||
mand_attr_miss_model = copy.deepcopy(model_json)
|
||||
mand_attr_miss_group = mand_attr_miss_model['group']
|
||||
del mand_attr_miss_group['groups'][0]['name']
|
||||
assert mdef.validate_group_point_dup(mand_attr_miss_group)[0:32] == 'Mandatory name attribute missing'
|
||||
|
||||
dup_point_id_model = copy.deepcopy(model_json)
|
||||
dup_point_id_group = dup_point_id_model['group']
|
||||
dup_point_id_group['points'][1]['name'] = 'ID'
|
||||
assert mdef.validate_group_point_dup(dup_point_id_group)[0:30] == 'Duplicate point id ID in group'
|
||||
|
||||
mand_attr_miss_point_model = copy.deepcopy(model_json)
|
||||
mand_attr_miss_point_group = mand_attr_miss_point_model['group']
|
||||
del mand_attr_miss_point_group['points'][1]['name']
|
||||
assert mdef.validate_group_point_dup(mand_attr_miss_point_group)[0:55] == 'Mandatory attribute missing in point ' \
|
||||
'definition element'
|
||||
|
||||
|
||||
def test_validate_symbols():
|
||||
symbols = [
|
||||
{'name': 'CAT_A', 'value': 1},
|
||||
{'name': 'CAT_B', 'value': 2}
|
||||
]
|
||||
assert mdef.validate_symbols(symbols, mdef.symbol_attr) == ''
|
||||
|
||||
|
||||
def test_validate_sf():
|
||||
with open('./sunspec2/models/json/model_702.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
model_point = model_json['group']['points'][2]
|
||||
model_group = model_json['group']
|
||||
model_group_arr = [model_group, model_group]
|
||||
assert mdef.validate_sf(model_point, 'W_SF', model_group_arr) == ''
|
||||
|
||||
not_sf_type_model = copy.deepcopy(model_json)
|
||||
not_sf_type_point = not_sf_type_model['group']['points'][2]
|
||||
not_sf_type_group = not_sf_type_model['group']
|
||||
not_sf_type_group_arr = [not_sf_type_group, not_sf_type_group]
|
||||
for point in not_sf_type_model['group']['points']:
|
||||
if point['name'] == 'W_SF':
|
||||
point['type'] = 'abc'
|
||||
assert mdef.validate_sf(not_sf_type_point, 'W_SF', not_sf_type_group_arr)[0:60] == 'Scale factor W_SF for point ' \
|
||||
'WMaxRtg is not scale factor ' \
|
||||
'type'
|
||||
|
||||
sf_not_found_model = copy.deepcopy(model_json)
|
||||
sf_not_found_point = sf_not_found_model['group']['points'][2]
|
||||
sf_not_found_group = sf_not_found_model['group']
|
||||
sf_not_found_group_arr = [sf_not_found_group, sf_not_found_group]
|
||||
assert mdef.validate_sf(sf_not_found_point, 'ABC', sf_not_found_group_arr)[0:44] == 'Scale factor ABC for point ' \
|
||||
'WMaxRtg not found'
|
||||
|
||||
sf_out_range_model = copy.deepcopy(model_json)
|
||||
sf_out_range_point = sf_out_range_model['group']['points'][2]
|
||||
sf_out_range_group = sf_out_range_model['group']
|
||||
sf_out_range_group_arr = [sf_out_range_group, sf_out_range_group]
|
||||
assert mdef.validate_sf(sf_out_range_point, 11, sf_out_range_group_arr)[0:46] == 'Scale factor 11 for point ' \
|
||||
'WMaxRtg out of range'
|
||||
|
||||
sf_invalid_type_model = copy.deepcopy(model_json)
|
||||
sf_invalid_type_point = sf_invalid_type_model['group']['points'][2]
|
||||
sf_invalid_type_group = sf_invalid_type_model['group']
|
||||
sf_invalid_type_group_arr = [sf_invalid_type_group, sf_invalid_type_group]
|
||||
assert mdef.validate_sf(sf_invalid_type_point, 4.0, sf_invalid_type_group_arr)[0:51] == 'Scale factor 4.0 for' \
|
||||
' point WMaxRtg has ' \
|
||||
'invalid type'
|
||||
|
||||
|
||||
def test_validate_point_def():
|
||||
with open('./sunspec2/models/json/model_702.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
model_group = model_json['group']
|
||||
group = model_json['group']
|
||||
point = model_json['group']['points'][0]
|
||||
assert mdef.validate_point_def(point, model_group, group) == ''
|
||||
|
||||
unk_point_type_model = copy.deepcopy(model_json)
|
||||
unk_point_type_model_group = unk_point_type_model['group']
|
||||
unk_point_type_group = unk_point_type_model['group']
|
||||
unk_point_type_point = unk_point_type_model['group']['points'][0]
|
||||
unk_point_type_point['type'] = 'abc'
|
||||
assert mdef.validate_point_def(unk_point_type_point, unk_point_type_model_group,
|
||||
unk_point_type_group)[0:35] == 'Unknown point type abc for point ID'
|
||||
|
||||
dup_symbol_model = copy.deepcopy(model_json)
|
||||
dup_symbol_model_group = dup_symbol_model['group']
|
||||
dup_symbol_group = dup_symbol_model['group']
|
||||
dup_symbol_point = dup_symbol_model['group']['points'][21]
|
||||
dup_symbol_point['symbols'][0]['name'] = 'CAT_B'
|
||||
assert mdef.validate_point_def(dup_symbol_point, dup_symbol_model_group,
|
||||
dup_symbol_group)[0:19] == 'Duplicate symbol id'
|
||||
|
||||
mand_attr_missing = copy.deepcopy(model_json)
|
||||
mand_attr_missing_model_group = mand_attr_missing['group']
|
||||
mand_attr_missing_group = mand_attr_missing['group']
|
||||
mand_attr_missing_point = mand_attr_missing['group']['points'][0]
|
||||
del mand_attr_missing_point['name']
|
||||
assert mdef.validate_point_def(mand_attr_missing_point, mand_attr_missing_model_group,
|
||||
mand_attr_missing_group)[0:27] == 'Mandatory attribute missing'
|
||||
|
||||
|
||||
def test_validate_group_def():
|
||||
with open('./sunspec2/models/json/model_702.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
assert mdef.validate_group_def(model_json['group'], model_json['group']) == ''
|
||||
|
||||
|
||||
def test_validate_model_group_def():
|
||||
with open('./sunspec2/models/json/model_702.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
assert mdef.validate_model_group_def(model_json, model_json['group']) == ''
|
||||
|
||||
missing_id_model = copy.deepcopy(model_json)
|
||||
missing_id_group = missing_id_model['group']
|
||||
missing_id_group['points'][0]['name'] = 'abc'
|
||||
assert mdef.validate_model_group_def(missing_id_model, missing_id_group)[0:41] == 'First point in top-level' \
|
||||
' group must be ID'
|
||||
|
||||
wrong_model_id_model = copy.deepcopy(model_json)
|
||||
wrong_model_id_group = wrong_model_id_model['group']
|
||||
wrong_model_id_group['points'][0]['value'] = 0
|
||||
assert mdef.validate_model_group_def(wrong_model_id_model, wrong_model_id_group)[0:42] == 'Model ID does not ' \
|
||||
'match top-level group ID'
|
||||
|
||||
missing_len_model = copy.deepcopy(model_json)
|
||||
missing_len_group = missing_len_model['group']
|
||||
missing_len_group['points'][1]['name'] = 'abc'
|
||||
assert mdef.validate_model_group_def(missing_len_model, missing_len_group)[0:41] == 'Second point in top-level ' \
|
||||
'group must be L'
|
||||
|
||||
missing_two_p_model = copy.deepcopy(model_json)
|
||||
missing_two_p_group = missing_two_p_model['group']
|
||||
missing_two_p_point = missing_two_p_group['points'][0]
|
||||
del missing_two_p_group['points']
|
||||
missing_two_p_group['points'] = [missing_two_p_point]
|
||||
assert mdef.validate_model_group_def(missing_two_p_model, missing_two_p_group)[0:48] == 'Top-level group must' \
|
||||
' contain at least two ' \
|
||||
'points'
|
||||
|
||||
missing_p_def_model = copy.deepcopy(model_json)
|
||||
missing_p_def_group = missing_p_def_model['group']
|
||||
del missing_p_def_group['points']
|
||||
assert mdef.validate_model_group_def(missing_p_def_model, missing_p_def_group)[0:41] == 'Top-level group' \
|
||||
' missing point definitions'
|
||||
|
||||
|
||||
def test_validate_model_def():
|
||||
with open('./sunspec2/models/json/model_702.json') as f:
|
||||
model_json = json.load(f)
|
||||
|
||||
assert mdef.validate_model_def(model_json) == ''
|
||||
|
||||
|
||||
def test_from_json_str():
|
||||
with open('./sunspec2/models/json/model_63001.json') as f:
|
||||
model_json = json.load(f)
|
||||
model_json_str = json.dumps(model_json)
|
||||
assert isinstance(mdef.from_json_str(model_json_str), dict)
|
||||
|
||||
|
||||
def test_from_json_file():
|
||||
assert isinstance(mdef.from_json_file('./sunspec2/models/json/model_63001.json'), dict)
|
||||
|
||||
|
||||
def test_to_json_str():
|
||||
with open('./sunspec2/models/json/model_63001.json') as f:
|
||||
model_json = json.load(f)
|
||||
assert isinstance(mdef.to_json_str(model_json), str)
|
||||
|
||||
|
||||
def test_to_json_filename():
|
||||
assert mdef.to_json_filename('63001') == 'model_63001.json'
|
||||
|
||||
|
||||
def test_to_json_file(tmp_path):
|
||||
with open('./sunspec2/models/json/model_63001.json') as f:
|
||||
model_json = json.load(f)
|
||||
mdef.to_json_file(model_json, filedir=tmp_path)
|
||||
|
||||
with open(tmp_path / 'model_63001.json') as f:
|
||||
model_json = json.load(f)
|
||||
assert isinstance(model_json, dict)
|
||||
|
||||
|
||||
def test_model_filename_to_id():
|
||||
assert mdef.model_filename_to_id('model_00077.json') == 77
|
||||
with pytest.raises(Exception) as exc:
|
||||
mdef.model_filename_to_id('model_abc.json')
|
||||
assert 'Error extracting model id from filename' in str(exc.value)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
||||
import sunspec2.modbus.modbus as modbus_client
|
||||
import pytest
|
||||
import socket
|
||||
import serial
|
||||
import sunspec2.tests.mock_socket as MockSocket
|
||||
import sunspec2.tests.mock_port as MockPort
|
||||
|
||||
|
||||
def test_modbus_rtu_client(monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.modbus_rtu_client('COMM2')
|
||||
assert c.baudrate == 9600
|
||||
assert c.parity == "N"
|
||||
assert modbus_client.modbus_rtu_clients['COMM2']
|
||||
|
||||
with pytest.raises(modbus_client.ModbusClientError) as exc1:
|
||||
c2 = modbus_client.modbus_rtu_client('COMM2', baudrate=99)
|
||||
assert 'Modbus client baudrate mismatch' in str(exc1.value)
|
||||
|
||||
with pytest.raises(modbus_client.ModbusClientError) as exc2:
|
||||
c2 = modbus_client.modbus_rtu_client('COMM2', parity='E')
|
||||
assert 'Modbus client parity mismatch' in str(exc2.value)
|
||||
|
||||
|
||||
def test_modbus_rtu_client_remove(monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.modbus_rtu_client('COMM2')
|
||||
assert modbus_client.modbus_rtu_clients['COMM2']
|
||||
modbus_client.modbus_rtu_client_remove('COMM2')
|
||||
assert modbus_client.modbus_rtu_clients.get('COMM2') is None
|
||||
|
||||
|
||||
def test___generate_crc16_table():
|
||||
pass
|
||||
|
||||
|
||||
def test_computeCRC():
|
||||
pass
|
||||
|
||||
|
||||
def test_checkCRC():
|
||||
pass
|
||||
|
||||
|
||||
class TestModbusClientRTU:
|
||||
def test___init__(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
assert c.name == "COM2"
|
||||
assert c.baudrate == 9600
|
||||
assert c.parity is None
|
||||
assert c.serial is not None
|
||||
assert c.timeout == .5
|
||||
assert c.write_timeout == .5
|
||||
assert not c.devices
|
||||
|
||||
def test_open(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
c.open()
|
||||
assert c.serial.connected
|
||||
|
||||
def test_close(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
c.open()
|
||||
c.close()
|
||||
assert not c.serial.connected
|
||||
|
||||
def test_add_device(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
c.add_device(1, "1")
|
||||
assert c.devices.get(1) is not None
|
||||
assert c.devices[1] == "1"
|
||||
|
||||
def test_remove_device(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
c.add_device(1, "1")
|
||||
assert c.devices.get(1) is not None
|
||||
assert c.devices[1] == "1"
|
||||
c.remove_device(1)
|
||||
assert c.devices.get(1) is None
|
||||
|
||||
def test__read(self):
|
||||
pass
|
||||
|
||||
def test_read(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00'
|
||||
b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d']
|
||||
check_req = b'\x01\x03\x9c@\x00F\xeb\xbc'
|
||||
c.open()
|
||||
c.serial._set_buffer(in_buff)
|
||||
|
||||
check_read = in_buff[0] + in_buff[1]
|
||||
assert c.read(1, 40000, 70) == check_read[3:-2]
|
||||
assert c.serial.request[0] == check_req
|
||||
|
||||
def test__write(self):
|
||||
pass
|
||||
|
||||
def test_write(self, monkeypatch):
|
||||
monkeypatch.setattr(serial, 'Serial', MockPort.mock_port)
|
||||
c = modbus_client.ModbusClientRTU(name="COM2")
|
||||
c.open()
|
||||
data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00\x00\x00\x00\x00\x00\x00' \
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
buffer = [b'\x01\x10\x9cl\x00', b'\x18.N']
|
||||
c.serial._set_buffer(buffer)
|
||||
c.write(1, 40044, data_to_write)
|
||||
|
||||
check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
|
||||
b'\x00\x00\x00\x00\x00\xad\xff'
|
||||
assert c.serial.request[0] == check_req
|
||||
|
||||
|
||||
class TestModbusClientTCP:
|
||||
def test___init__(self):
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
assert c.slave_id == 1
|
||||
assert c.ipaddr == '127.0.0.1'
|
||||
assert c.ipport == 502
|
||||
assert c.timeout == 2
|
||||
assert c.ctx is None
|
||||
assert c.trace_func is None
|
||||
assert c.max_count == 125
|
||||
|
||||
def test_close(self, monkeypatch):
|
||||
monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket)
|
||||
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
c.connect()
|
||||
assert c.socket
|
||||
c.disconnect()
|
||||
assert c.socket is None
|
||||
|
||||
def test_connect(self, monkeypatch):
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
c.connect()
|
||||
assert 'Connection error' in str(exc.value)
|
||||
|
||||
monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket)
|
||||
c.connect()
|
||||
assert c.socket is not None
|
||||
assert c.socket.connected is True
|
||||
assert c.socket.ipaddr == '127.0.0.1'
|
||||
assert c.socket.ipport == 502
|
||||
assert c.socket.timeout == 2
|
||||
|
||||
def test_disconnect(self, monkeypatch):
|
||||
monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket)
|
||||
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
c.connect()
|
||||
assert c.socket
|
||||
c.disconnect()
|
||||
assert c.socket is None
|
||||
|
||||
def test__read(self, monkeypatch):
|
||||
pass
|
||||
|
||||
def test_read(self, monkeypatch):
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket)
|
||||
in_buff = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c'
|
||||
b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x01\x00\x00']
|
||||
check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F'
|
||||
c.connect()
|
||||
c.socket._set_buffer(in_buff)
|
||||
assert c.read(40000, 70) == in_buff[1]
|
||||
assert c.socket.request[0] == check_req
|
||||
|
||||
def test__write(self, monkeypatch):
|
||||
pass
|
||||
|
||||
def test_write(self, monkeypatch):
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket)
|
||||
c.connect()
|
||||
data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10']
|
||||
c.socket._set_buffer(buffer)
|
||||
c.write(40052, data_to_write)
|
||||
|
||||
check_req = b"\x00\x00\x00\x00\x00'\x01\x10\x9ct\x00\x10 sn-000\x00\x00\x00\x00\x00\x00\x00" \
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
assert c.socket.request[0] == check_req
|
||||
|
||||
def test_write_over_max_size(self, monkeypatch):
|
||||
c = modbus_client.ModbusClientTCP()
|
||||
monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket)
|
||||
c.connect()
|
||||
data_to_write = bytearray((c.max_write_count+1)*2)
|
||||
data_to_write[:6] = b'sn-000'
|
||||
|
||||
buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9ct\x00\x7b',
|
||||
b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c\xef\x00\x01']
|
||||
c.socket._set_buffer(buffer)
|
||||
c.write(40052, data_to_write)
|
||||
|
||||
check_req0 = b"\x00\x00\x00\x00\x00\xfd\x01" + b"\x10\x9ct\x00{\xf6" + data_to_write[:(c.max_write_count*2)]
|
||||
check_req1 = b"\x00\x00\x00\x00\x00\x09\x01" + b"\x10\x9c\xef\x00\x01\x02\x00\x00"
|
||||
assert c.socket.request[0] == check_req0
|
||||
assert c.socket.request[1] == check_req1
|
||||
@@ -0,0 +1,217 @@
|
||||
import sunspec2.smdx as smdx
|
||||
import sunspec2.mdef as mdef
|
||||
import xml.etree.ElementTree as ET
|
||||
import pytest
|
||||
import copy
|
||||
|
||||
|
||||
def test_to_smdx_filename():
|
||||
assert smdx.to_smdx_filename(77) == 'smdx_00077.xml'
|
||||
|
||||
|
||||
def test_model_filename_to_id():
|
||||
assert smdx.model_filename_to_id('smdx_00077.xml') == 77
|
||||
with pytest.raises(Exception) as exc:
|
||||
smdx.model_filename_to_id('smdx_abc.xml')
|
||||
assert 'Error extracting model id from filename' in str(exc.value)
|
||||
|
||||
|
||||
def test_from_smdx_file():
|
||||
smdx_304 = {'id': 304, 'group': {'name': 'inclinometer', 'type': 'group', 'points': [
|
||||
{'name': 'ID', 'value': 304, 'desc': 'Model identifier', 'label': 'Model ID', 'size': 1, 'mandatory': 'M',
|
||||
'static': 'S', 'type': 'uint16'},
|
||||
{'name': 'L', 'desc': 'Model length', 'label': 'Model Length', 'size': 1, 'mandatory': 'M', 'static': 'S',
|
||||
'type': 'uint16'}], 'groups': [{'name': 'incl', 'type': 'group', 'count': 0, 'points': [
|
||||
{'name': 'Inclx', 'type': 'int32', 'size': 2, 'mandatory': 'M', 'units': 'Degrees', 'sf': -2, 'label': 'X',
|
||||
'desc': 'X-Axis inclination'},
|
||||
{'name': 'Incly', 'type': 'int32', 'size': 2, 'units': 'Degrees', 'sf': -2, 'label': 'Y',
|
||||
'desc': 'Y-Axis inclination'},
|
||||
{'name': 'Inclz', 'type': 'int32', 'size': 2, 'units': 'Degrees', 'sf': -2, 'label': 'Z',
|
||||
'desc': 'Z-Axis inclination'}]}], 'label': 'Inclinometer Model',
|
||||
'desc': 'Include to support orientation measurements'}}
|
||||
assert smdx.from_smdx_file('./sunspec2/models/smdx/smdx_00304.xml') == smdx_304
|
||||
|
||||
|
||||
def test_from_smdx_file_symbols():
|
||||
mdef = smdx.from_smdx_file('sunspec2/models/smdx/smdx_00803.xml')
|
||||
for point_def in mdef["group"]["groups"][0]["points"]:
|
||||
if point_def["name"] != "StrSt":
|
||||
continue
|
||||
symbol = point_def["symbols"][1]
|
||||
assert symbol["name"] == "CONTACTOR_STATUS"
|
||||
assert symbol["label"] == "Contactor Status"
|
||||
assert symbol["desc"].startswith("String")
|
||||
assert symbol["detail"]
|
||||
break
|
||||
else:
|
||||
pytest.fail("Point not found")
|
||||
|
||||
|
||||
def test_from_smdx():
|
||||
tree = ET.parse('./sunspec2/models/smdx/smdx_00304.xml')
|
||||
root = tree.getroot()
|
||||
|
||||
mdef_not_found = copy.deepcopy(root)
|
||||
mdef_not_found.remove(mdef_not_found.find('model'))
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx(mdef_not_found)
|
||||
|
||||
duplicate_fixed_btype_str = '''
|
||||
<sunSpecModels v="1">
|
||||
|
||||
<model id="63001" len="13">
|
||||
<block len="4">
|
||||
<point id="sunssf_1" offset="0" type="sunssf" />
|
||||
<point id="sunssf_2" offset="1" type="sunssf" />
|
||||
<point id="sunssf_3" offset="2" type="sunssf" />
|
||||
<point id="sunssf_4" offset="3" type="sunssf" />
|
||||
</block>
|
||||
<block len="6">
|
||||
<point id="int16_1" offset="0" type="int16" sf="sunssf_1" />
|
||||
<point id="int16_2" offset="1" type="int16" sf="sunssf_2" />
|
||||
<point id="int16_3" offset="2" type="int16" sf="sunssf_3" />
|
||||
<point id="int16_4" offset="3" type="int16" sf="sunssf_4" access="rw" />
|
||||
<point id="int16_5" offset="4" type="int16" />
|
||||
<point id="int16_u" offset="5" type="int16" />
|
||||
</block>
|
||||
<block type="repeating" len="3">
|
||||
<point id="sunssf_8" offset="0" type="sunssf" />
|
||||
<point id="int16_11" offset="1" type="int16" sf="sunssf_8" access="rw" />
|
||||
<point id="int16_12" offset="2" type="int16" sf="sunssf_9" />
|
||||
</block>
|
||||
</model>
|
||||
<strings id="63001" locale="en">
|
||||
<model>
|
||||
<label>SunSpec Test Model 1</label>
|
||||
</model>
|
||||
</strings>
|
||||
</sunSpecModels>
|
||||
'''
|
||||
duplicate_fixed_btype_xml = ET.fromstring(duplicate_fixed_btype_str)
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx(duplicate_fixed_btype_xml)
|
||||
|
||||
dup_repeating_btype_str = '''
|
||||
<sunSpecModels v="1">
|
||||
<model id="63001" len="12">
|
||||
<block len="4">
|
||||
<point id="sunssf_1" offset="0" type="sunssf" />
|
||||
<point id="sunssf_2" offset="1" type="sunssf" />
|
||||
<point id="sunssf_3" offset="2" type="sunssf" />
|
||||
<point id="sunssf_4" offset="3" type="sunssf" />
|
||||
</block>
|
||||
<block type="repeating" len="3">
|
||||
<point id="sunssf_8" offset="0" type="sunssf" />
|
||||
<point id="int16_11" offset="1" type="int16" sf="sunssf_8" access="rw" />
|
||||
<point id="int16_12" offset="2" type="int16" sf="sunssf_9" />
|
||||
</block>
|
||||
<block type="repeating" len="5">
|
||||
<point id="int32_u" offset="0" type="int32" />
|
||||
<point id="uint32" offset="1" type="uint32" sf="sunssf_9" access="rw"/>
|
||||
<point id="uint32_u" offset="2" type="uint32" />
|
||||
<point id="sunssf_9" offset="3" type="sunssf" />
|
||||
<point id="pad_2" offset="4" type="pad" />
|
||||
</block>
|
||||
</model>
|
||||
<strings id="63001" locale="en">
|
||||
<model>
|
||||
<label>SunSpec Test Model 1</label>
|
||||
</model>
|
||||
</strings>
|
||||
</sunSpecModels>
|
||||
'''
|
||||
dup_repeating_btype_xml = ET.fromstring(dup_repeating_btype_str)
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx(dup_repeating_btype_xml)
|
||||
|
||||
invalid_btype_root = copy.deepcopy(root)
|
||||
invalid_btype_root.find('model').find('block').set('type', 'abc')
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx(invalid_btype_root)
|
||||
|
||||
dup_fixed_p_def_str = '''
|
||||
<sunSpecModels v="1">
|
||||
<model id="63001" len="4">
|
||||
<block len="4">
|
||||
<point id="sunssf_1" offset="0" type="sunssf" />
|
||||
<point id="sunssf_2" offset="1" type="sunssf" />
|
||||
<point id="sunssf_1" offset="2" type="sunssf" />
|
||||
<point id="sunssf_4" offset="3" type="sunssf" />
|
||||
</block>
|
||||
</model>
|
||||
<strings id="63001" locale="en">
|
||||
<model>
|
||||
<label>SunSpec Test Model 1</label>
|
||||
</model>
|
||||
</strings>
|
||||
</sunSpecModels>
|
||||
'''
|
||||
dup_fixed_p_def_xml = ET.fromstring(dup_fixed_p_def_str)
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx(dup_fixed_p_def_xml)
|
||||
|
||||
dup_repeating_p_def_str = '''
|
||||
<sunSpecModels v="1">
|
||||
<model id="63001" len="7">
|
||||
<block len="4">
|
||||
<point id="sunssf_1" offset="0" type="sunssf" />
|
||||
<point id="sunssf_2" offset="1" type="sunssf" />
|
||||
<point id="sunssf_3" offset="2" type="sunssf" />
|
||||
<point id="sunssf_4" offset="3" type="sunssf" />
|
||||
</block>
|
||||
<block type="repeating" len="3">
|
||||
<point id="sunssf_8" offset="0" type="sunssf" />
|
||||
<point id="sunssf_8" offset="1" type="int16" sf="sunssf_8" access="rw" />
|
||||
<point id="int16_12" offset="2" type="int16" sf="sunssf_9" />
|
||||
</block>
|
||||
</model>
|
||||
<strings id="63001" locale="en">
|
||||
<model>
|
||||
<label>SunSpec Test Model 1</label>
|
||||
</model>
|
||||
</strings>
|
||||
</sunSpecModels>
|
||||
'''
|
||||
dup_repeating_p_def_xml = ET.fromstring(dup_repeating_p_def_str)
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx(dup_repeating_p_def_xml)
|
||||
|
||||
|
||||
def test_from_smdx_point():
|
||||
smdx_point_str = """<point id="Mn" offset="0" type="string" len="16" mandatory="true" />"""
|
||||
smdx_point_xml = ET.fromstring(smdx_point_str)
|
||||
assert smdx.from_smdx_point(smdx_point_xml) == {'name': 'Mn', 'type': 'string', 'size': 16, 'mandatory': 'M'}
|
||||
|
||||
missing_pid_xml = copy.deepcopy(smdx_point_xml)
|
||||
del missing_pid_xml.attrib['id']
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx_point(missing_pid_xml)
|
||||
|
||||
missing_ptype = copy.deepcopy(smdx_point_xml)
|
||||
del missing_ptype.attrib['type']
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx_point(missing_ptype)
|
||||
|
||||
unk_ptype = copy.deepcopy(smdx_point_xml)
|
||||
unk_ptype.attrib['type'] = 'abc'
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx_point(unk_ptype)
|
||||
|
||||
missing_len = copy.deepcopy(smdx_point_xml)
|
||||
del missing_len.attrib['len']
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx_point(missing_len)
|
||||
|
||||
unk_mand_type = copy.deepcopy(smdx_point_xml)
|
||||
unk_mand_type.attrib['mandatory'] = 'abc'
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx_point(unk_mand_type)
|
||||
|
||||
unk_access_type = copy.deepcopy(smdx_point_xml)
|
||||
unk_access_type.attrib['access'] = 'abc'
|
||||
with pytest.raises(mdef.ModelDefinitionError):
|
||||
smdx.from_smdx_point(unk_access_type)
|
||||
|
||||
|
||||
def test_indent():
|
||||
pass
|
||||
@@ -0,0 +1,526 @@
|
||||
import sunspec2.spreadsheet as spreadsheet
|
||||
import pytest
|
||||
import csv
|
||||
import copy
|
||||
import json
|
||||
|
||||
|
||||
def test_idx():
|
||||
row = ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor',
|
||||
'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description']
|
||||
|
||||
assert spreadsheet.idx(row, 'Address Offset') == 0
|
||||
with pytest.raises(ValueError):
|
||||
del row[0]
|
||||
assert spreadsheet.idx(row, 'Address Offset', mandatory=True)
|
||||
|
||||
|
||||
def test_row_is_empty():
|
||||
row = [''] * 10
|
||||
assert spreadsheet.row_is_empty(row, 0)
|
||||
row[0] = 'abc'
|
||||
assert not spreadsheet.row_is_empty(row, 0)
|
||||
|
||||
|
||||
def test_find_name():
|
||||
points = [
|
||||
{
|
||||
"name": "Inclx",
|
||||
"type": "int32",
|
||||
"mandatory": "M",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "X",
|
||||
"desc": "X-Axis inclination"
|
||||
},
|
||||
{
|
||||
"name": "Incly",
|
||||
"type": "int32",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "Y",
|
||||
"desc": "Y-Axis inclination"
|
||||
},
|
||||
{
|
||||
"name": "Inclz",
|
||||
"type": "int32",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "Z",
|
||||
"desc": "Z-Axis inclination"
|
||||
}
|
||||
]
|
||||
assert spreadsheet.find_name(points, 'abc') is None
|
||||
assert spreadsheet.find_name(points, 'Incly') == points[1]
|
||||
|
||||
|
||||
def test_from_spreadsheet():
|
||||
model_spreadsheet = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', '', ''],
|
||||
[0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''],
|
||||
[1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''],
|
||||
['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''],
|
||||
['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', '']
|
||||
]
|
||||
model_def = {
|
||||
'group':
|
||||
{
|
||||
'name': 'inclinometer', 'type': 'group', 'label': 'Inclinometer Model',
|
||||
'desc': 'Include to support orientation measurements', 'points':
|
||||
[
|
||||
{'name': 'ID', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S',
|
||||
'label': 'Model ID',
|
||||
'desc': 'Model identifier', 'value': 304, 'standards': []},
|
||||
{'name': 'L', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S',
|
||||
'label': 'Model Length',
|
||||
'desc': 'Model length', 'standards': []}
|
||||
],
|
||||
'groups': [
|
||||
{'name': 'incl', 'type': 'group', 'count': 0, 'points': [
|
||||
{'name': 'Inclx', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees',
|
||||
'mandatory': 'M', 'label': 'X',
|
||||
'desc': 'X-Axis inclination', 'standards': []},
|
||||
{'name': 'Incly', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees',
|
||||
'label': 'Y',
|
||||
'desc': 'Y-Axis inclination', 'standards': []},
|
||||
{'name': 'Inclz', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees',
|
||||
'label': 'Z',
|
||||
'desc': 'Z-Axis inclination', 'standards': []}]}
|
||||
]
|
||||
},
|
||||
'id': 304
|
||||
}
|
||||
|
||||
spreadsheet_data = spreadsheet.from_spreadsheet(model_spreadsheet)
|
||||
|
||||
assert spreadsheet_data == model_def
|
||||
|
||||
|
||||
def test_to_spreadsheet():
|
||||
model_spreadsheet = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', '', ''],
|
||||
[0, '', 'ID', 304, '', 'uint16', 1, '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''],
|
||||
[1, '', 'L', '', '', 'uint16', 1, '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', 0, 'Inclx', '', '', 'int32', 2, -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''],
|
||||
['', 2, 'Incly', '', '', 'int32', 2, -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''],
|
||||
['', 4, 'Inclz', '', '', 'int32', 2, -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', '']
|
||||
]
|
||||
|
||||
model_def = {
|
||||
"id": 304,
|
||||
"group": {
|
||||
"name": "inclinometer",
|
||||
"type": "group",
|
||||
"points": [
|
||||
{
|
||||
"name": "ID",
|
||||
"value": 304,
|
||||
"desc": "Model identifier",
|
||||
"label": "Model ID",
|
||||
"mandatory": "M",
|
||||
"static": "S",
|
||||
"type": "uint16"
|
||||
},
|
||||
{
|
||||
"name": "L",
|
||||
"desc": "Model length",
|
||||
"label": "Model Length",
|
||||
"mandatory": "M",
|
||||
"static": "S",
|
||||
"type": "uint16"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"name": "incl",
|
||||
"type": "group",
|
||||
"count": 0,
|
||||
"points": [
|
||||
{
|
||||
"name": "Inclx",
|
||||
"type": "int32",
|
||||
"mandatory": "M",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "X",
|
||||
"desc": "X-Axis inclination"
|
||||
},
|
||||
{
|
||||
"name": "Incly",
|
||||
"type": "int32",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "Y",
|
||||
"desc": "Y-Axis inclination"
|
||||
},
|
||||
{
|
||||
"name": "Inclz",
|
||||
"type": "int32",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "Z",
|
||||
"desc": "Z-Axis inclination"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "Inclinometer Model",
|
||||
"desc": "Include to support orientation measurements"
|
||||
}
|
||||
}
|
||||
assert spreadsheet.to_spreadsheet(model_def) == model_spreadsheet
|
||||
|
||||
|
||||
def test_to_spreadsheet_group():
|
||||
model_def = {
|
||||
"group": {
|
||||
"desc": "DER capacity model.",
|
||||
"label": "DER Capacity",
|
||||
"name": "DERCapacity",
|
||||
"points": [
|
||||
{
|
||||
"access": "R",
|
||||
"desc": "DER capacity model id.",
|
||||
"label": "DER Capacity Model ID",
|
||||
"mandatory": "M",
|
||||
"name": "ID",
|
||||
"static": "S",
|
||||
"type": "uint16",
|
||||
"value": 702
|
||||
},
|
||||
{
|
||||
"access": "R",
|
||||
"desc": "DER capacity name model length.",
|
||||
"label": "DER Capacity Model Length",
|
||||
"mandatory": "M",
|
||||
"name": "L",
|
||||
"static": "S",
|
||||
"type": "uint16"
|
||||
},
|
||||
{
|
||||
"access": "R",
|
||||
"comments": [
|
||||
"Nameplate Ratings - Specifies capacity ratings"
|
||||
],
|
||||
"desc": "Maximum active power rating at unity power factor in watts.",
|
||||
"label": "Active Power Max Rating",
|
||||
"mandatory": "O",
|
||||
"name": "WMaxRtg",
|
||||
"sf": "W_SF",
|
||||
"type": "uint16",
|
||||
"units": "W",
|
||||
"symbols": [
|
||||
{
|
||||
"name": "CAT_A",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "CAT_B",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "group"
|
||||
},
|
||||
"id": 702
|
||||
}
|
||||
ss = []
|
||||
spreadsheet.to_spreadsheet_group(ss, model_def['group'], has_notes=False)
|
||||
assert ss == [
|
||||
['', '', 'DERCapacity', '', '', 'group', '', '', '', '', '', '', 'DER Capacity', 'DER capacity model.', '', ''],
|
||||
['', 0, 'ID', 702, '', 'uint16', 1, '', '', '', 'M', 'S', 'DER Capacity Model ID',
|
||||
'DER capacity model id.', '', ''],
|
||||
['', 1, 'L', '', '', 'uint16', 1, '', '', '', 'M', 'S', 'DER Capacity Model Length',
|
||||
'DER capacity name model length.', '', ''],
|
||||
['Nameplate Ratings - Specifies capacity ratings', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', 2, 'WMaxRtg', '', '', 'uint16', 1, 'W_SF', 'W', '', '', '', 'Active Power Max Rating',
|
||||
'Maximum active power rating at unity power factor in watts.', '', ''],
|
||||
['', '', 'CAT_A', 1, '', '', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', '', 'CAT_B', 2, '', '', '', '', '', '', '', '', '', '', '', '']]
|
||||
|
||||
|
||||
def test_to_spreadsheet_point():
|
||||
point = {
|
||||
"access": "R",
|
||||
"desc": "Abnormal operating performance category as specified in IEEE 1547-2018.",
|
||||
"label": "Abnormal Operating Category",
|
||||
"mandatory": "O",
|
||||
"name": "AbnOpCatRtg",
|
||||
"symbols": [
|
||||
{
|
||||
"name": "CAT_1",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "CAT_2",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"name": "CAT_3",
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"type": "enum16"
|
||||
}
|
||||
ss = []
|
||||
assert spreadsheet.to_spreadsheet_point(ss, point, has_notes=False) == 1
|
||||
|
||||
missing_name_p = copy.deepcopy(point)
|
||||
del missing_name_p['name']
|
||||
with pytest.raises(Exception) as exc1:
|
||||
spreadsheet.to_spreadsheet_point(ss, missing_name_p, has_notes=False)
|
||||
assert 'Point missing name attribute' in str(exc1.value)
|
||||
|
||||
missing_type_p = copy.deepcopy(point)
|
||||
del missing_type_p['type']
|
||||
with pytest.raises(Exception) as exc2:
|
||||
spreadsheet.to_spreadsheet_point(ss, missing_type_p, has_notes=False)
|
||||
assert 'Point AbnOpCatRtg missing type' in str(exc2.value)
|
||||
|
||||
unk_p_type = copy.deepcopy(point)
|
||||
unk_p_type['type'] = 'abc'
|
||||
with pytest.raises(Exception) as exc3:
|
||||
spreadsheet.to_spreadsheet_point(ss, unk_p_type, has_notes=False)
|
||||
assert 'Unknown point type' in str(exc3.value)
|
||||
|
||||
p_size_not_int = copy.deepcopy(point)
|
||||
p_size_not_int['type'] = 'string'
|
||||
p_size_not_int['size'] = 'abc'
|
||||
with pytest.raises(Exception) as exc4:
|
||||
spreadsheet.to_spreadsheet_point(ss, p_size_not_int, has_notes=False)
|
||||
assert 'Point size is for point AbnOpCatRtg not an integer value' in str(exc4.value)
|
||||
|
||||
|
||||
def test_to_spreadsheet_symbol():
|
||||
symbol = {"name": "MAX_W", "value": 0}
|
||||
ss = []
|
||||
spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False)
|
||||
assert ss[0][2] == 'MAX_W' and ss[0][3] == 0
|
||||
|
||||
ss = []
|
||||
del symbol['value']
|
||||
with pytest.raises(Exception) as exc1:
|
||||
spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False)
|
||||
assert 'Symbol MAX_W missing value' in str(exc1.value)
|
||||
|
||||
ss = []
|
||||
del symbol['name']
|
||||
with pytest.raises(Exception) as exc2:
|
||||
spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False)
|
||||
assert 'Symbol missing name attribute' in str(exc2.value)
|
||||
|
||||
|
||||
def test_to_spreadsheet_comment():
|
||||
ss = []
|
||||
spreadsheet.to_spreadsheet_comment(ss, 'Scaling Factors', has_notes=False)
|
||||
assert ss[0][0] == 'Scaling Factors'
|
||||
|
||||
|
||||
def test_spreadsheet_equal():
|
||||
spreadsheet_smdx_304 = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', ''],
|
||||
['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''],
|
||||
['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''],
|
||||
['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''],
|
||||
['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''],
|
||||
['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '']
|
||||
]
|
||||
ss_copy = copy.deepcopy(spreadsheet_smdx_304)
|
||||
|
||||
assert spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy)
|
||||
|
||||
with pytest.raises(Exception) as exc1:
|
||||
ss_copy[0][0] = 'abc'
|
||||
spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy)
|
||||
assert 'Line 1 different' in str(exc1.value)
|
||||
|
||||
with pytest.raises(Exception) as exc2:
|
||||
del ss_copy[0]
|
||||
spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy)
|
||||
assert 'Different length' in str(exc2.value)
|
||||
|
||||
|
||||
def test_from_csv():
|
||||
model_def = {
|
||||
'group': {
|
||||
'name': 'inclinometer', 'type': 'group', 'label': 'Inclinometer Model',
|
||||
'desc': 'Include to support orientation measurements',
|
||||
'points': [
|
||||
{'name': 'ID', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S', 'label': 'Model ID',
|
||||
'desc': 'Model identifier', 'value': 304, 'standards': []},
|
||||
{'name': 'L', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S', 'label': 'Model Length',
|
||||
'desc': 'Model length', 'standards': []}
|
||||
],
|
||||
'groups': [
|
||||
{'name': 'incl', 'type': 'group', 'count': 0,
|
||||
'points': [
|
||||
{'name': 'Inclx', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', 'mandatory': 'M',
|
||||
'label': 'X',
|
||||
'desc': 'X-Axis inclination', 'standards': []},
|
||||
{'name': 'Incly', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', 'label': 'Y',
|
||||
'desc': 'Y-Axis inclination', 'standards': []},
|
||||
{'name': 'Inclz', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', 'label': 'Z',
|
||||
'desc': 'Z-Axis inclination', 'standards': []}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'id': 304
|
||||
}
|
||||
|
||||
csv_data = spreadsheet.from_csv('./sunspec2/tests/test_data/smdx_304.csv')
|
||||
|
||||
assert model_def == csv_data
|
||||
|
||||
|
||||
def test_to_csv(tmp_path):
|
||||
model_def = {
|
||||
"id": 304,
|
||||
"group": {
|
||||
"name": "inclinometer",
|
||||
"type": "group",
|
||||
"points": [
|
||||
{
|
||||
"name": "ID",
|
||||
"value": 304,
|
||||
"desc": "Model identifier",
|
||||
"label": "Model ID",
|
||||
"mandatory": "M",
|
||||
"static": "S",
|
||||
"type": "uint16"
|
||||
},
|
||||
{
|
||||
"name": "L",
|
||||
"desc": "Model length",
|
||||
"label": "Model Length",
|
||||
"mandatory": "M",
|
||||
"static": "S",
|
||||
"type": "uint16"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"name": "incl",
|
||||
"type": "group",
|
||||
"count": 0,
|
||||
"points": [
|
||||
{
|
||||
"name": "Inclx",
|
||||
"type": "int32",
|
||||
"mandatory": "M",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "X",
|
||||
"desc": "X-Axis inclination"
|
||||
},
|
||||
{
|
||||
"name": "Incly",
|
||||
"type": "int32",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "Y",
|
||||
"desc": "Y-Axis inclination"
|
||||
},
|
||||
{
|
||||
"name": "Inclz",
|
||||
"type": "int32",
|
||||
"units": "Degrees",
|
||||
"sf": -2,
|
||||
"label": "Z",
|
||||
"desc": "Z-Axis inclination"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"label": "Inclinometer Model",
|
||||
"desc": "Include to support orientation measurements"
|
||||
}
|
||||
}
|
||||
ss = spreadsheet.to_spreadsheet(model_def)
|
||||
spreadsheet.to_csv(model_def, filename=tmp_path / 'smdx_304.csv')
|
||||
|
||||
same_data = True
|
||||
row_num = 0
|
||||
idx = 0
|
||||
with open(tmp_path / 'smdx_304.csv') as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
for row in csvreader:
|
||||
idx = 0
|
||||
for i in row:
|
||||
if str(ss[row_num][idx]) != str(i):
|
||||
same_data = False
|
||||
idx += 1
|
||||
row_num += 1
|
||||
assert same_data
|
||||
|
||||
|
||||
def test_spreadsheet_from_csv():
|
||||
spreadsheet_smdx_304 = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', '', ''],
|
||||
['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''],
|
||||
['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''],
|
||||
['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''],
|
||||
['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', '']
|
||||
]
|
||||
|
||||
counter = 0
|
||||
for row in spreadsheet.spreadsheet_from_csv('./sunspec2/tests/test_data/smdx_304.csv'):
|
||||
same = True
|
||||
counter2 = 0
|
||||
for i in row:
|
||||
if i != spreadsheet_smdx_304[counter][counter2]:
|
||||
same = False
|
||||
counter2 += 1
|
||||
counter += 1
|
||||
assert same
|
||||
|
||||
|
||||
def test_spreadsheet_to_csv(tmp_path):
|
||||
spreadsheet_smdx_304 = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', ''],
|
||||
[0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''],
|
||||
[1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''],
|
||||
['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''],
|
||||
['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''],
|
||||
['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '']
|
||||
]
|
||||
spreadsheet.spreadsheet_to_csv(spreadsheet_smdx_304, filename=tmp_path / 'smdx_304.csv')
|
||||
|
||||
same_data = True
|
||||
rowNum = 0
|
||||
idx = 0
|
||||
with open(tmp_path / 'smdx_304.csv') as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
for row in csvreader:
|
||||
idx = 0
|
||||
for i in row:
|
||||
if str(spreadsheet_smdx_304[rowNum][idx]) != str(i):
|
||||
same_data = False
|
||||
idx += 1
|
||||
rowNum += 1
|
||||
assert same_data
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import sunspec2.xlsx as xlsx
|
||||
import pytest
|
||||
import openpyxl
|
||||
import openpyxl.styles as styles
|
||||
import json
|
||||
|
||||
|
||||
def test___init__():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
assert wb.filename == './sunspec2/tests/test_data/wb_701-705.xlsx'
|
||||
assert wb.params == {}
|
||||
|
||||
wb2 = xlsx.ModelWorkbook()
|
||||
assert wb2.filename is None
|
||||
assert wb2.params == {}
|
||||
|
||||
|
||||
def test_get_models():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
assert wb.get_models() == [701, 702, 703, 704, 705]
|
||||
wb2 = xlsx.ModelWorkbook()
|
||||
assert wb2.get_models() == []
|
||||
|
||||
|
||||
def test_save(tmp_path):
|
||||
wb = xlsx.ModelWorkbook()
|
||||
wb.save(tmp_path / 'test.xlsx')
|
||||
wb2 = xlsx.ModelWorkbook(filename=tmp_path / 'test.xlsx')
|
||||
iter_rows = wb2.xlsx_iter_rows(wb.wb['Index'])
|
||||
assert next(iter_rows) == ['Model', 'Label', 'Description']
|
||||
|
||||
|
||||
def test_xlsx_iter_rows():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['704'])
|
||||
assert next(iter_rows) == ['Address Offset', 'Group Offset', 'Name',
|
||||
'Value', 'Count', 'Type', 'Size', 'Scale Factor',
|
||||
'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)',
|
||||
'Label', 'Description', 'Detailed Description', 'Standards']
|
||||
assert next(iter_rows) == [None, None, 'DERCtlAC', None, None, 'group',
|
||||
None, None, None, None, None, None, 'DER AC Controls',
|
||||
'DER AC controls model.', None, None]
|
||||
|
||||
|
||||
def test_spreadsheet_from_xlsx():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
assert wb.spreadsheet_from_xlsx(704)[0:2] == [['Address Offset', 'Group Offset', 'Name', 'Value', 'Count',
|
||||
'Type', 'Size', 'Scale Factor', 'Units', 'RW Access (RW)',
|
||||
'Mandatory (M)', 'Static (S)', 'Label', 'Description',
|
||||
'Detailed Description', 'Standards'],
|
||||
['', '', 'DERCtlAC', None, None, 'group', None, None, None,
|
||||
None, None, None, 'DER AC Controls', 'DER AC controls model.', None,
|
||||
None]]
|
||||
|
||||
|
||||
def sort_nested_dicts(d):
|
||||
for key, value in d.items():
|
||||
if isinstance(value, dict):
|
||||
d[key] = sort_nested_dicts(value) # Sort nested dictionaries
|
||||
elif key == 'points' and isinstance(value, list):
|
||||
d[key] = sorted(value, key=lambda x: x['name'])
|
||||
elif isinstance(value, list) and len(value) > 1:
|
||||
d[key] = sorted(value, key=lambda x: sorted(x.items()) if isinstance(x, dict) else x)
|
||||
return dict(sorted(d.items()))
|
||||
|
||||
|
||||
# need deep diff to compare from_xlsx to json file, right now just compares with its own output
|
||||
def test_from_xlsx():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
with open('./sunspec2/models/json/model_704.json') as f:
|
||||
from_xlsx_output = json.load(f)
|
||||
|
||||
a = sort_nested_dicts(wb.from_xlsx(704))
|
||||
b = sort_nested_dicts(from_xlsx_output)
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_set_cell():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
with pytest.raises(ValueError) as exc:
|
||||
wb.set_cell(wb.wb['704'], 1, 2, 3)
|
||||
assert 'Workbooks opened with existing file are read only' in str(exc.value)
|
||||
|
||||
wb2 = xlsx.ModelWorkbook()
|
||||
assert wb2.set_cell(wb2.wb['Index'], 2, 1, 3, style='suns_comment').value == 3
|
||||
|
||||
|
||||
def test_set_info():
|
||||
wb = xlsx.ModelWorkbook()
|
||||
values = [''] * 14
|
||||
values[13] = 'description'
|
||||
values[12] = 'label'
|
||||
wb.set_info(wb.wb['Index'], 2, values)
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['Index'])
|
||||
next(iter_rows)
|
||||
assert next(iter_rows) == [None, None, None, None, None, None,
|
||||
None, None, None, None, None, None, 'label', 'description']
|
||||
|
||||
|
||||
def test_set_group():
|
||||
wb = xlsx.ModelWorkbook()
|
||||
values = [''] * 16
|
||||
values[2] = 'name'
|
||||
values[5] = 'type'
|
||||
values[4] = 'count'
|
||||
values[13] = 'description'
|
||||
values[12] = 'label'
|
||||
wb.set_group(wb.wb['Index'], 2, values, 2)
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['Index'])
|
||||
next(iter_rows)
|
||||
assert next(iter_rows) == ['', '', 'name', '', 'count', 'type', '', '', '', '', '', '',
|
||||
'label', 'description', '', '']
|
||||
|
||||
|
||||
def test_set_point():
|
||||
wb = xlsx.ModelWorkbook()
|
||||
values = [''] * 16
|
||||
values[0] = 'addr_offset'
|
||||
values[1] = 'group_offset'
|
||||
values[2] = 'name'
|
||||
values[3] = 'value'
|
||||
values[4] = 'count'
|
||||
values[5] = 'type'
|
||||
values[6] = 'size'
|
||||
values[7] = 'sf'
|
||||
values[8] = 'units'
|
||||
values[9] = 'access'
|
||||
values[10] = 'mandatory'
|
||||
values[11] = 'static'
|
||||
wb.set_point(wb.wb['Index'], 2, values, 1)
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['Index'])
|
||||
next(iter_rows)
|
||||
assert next(iter_rows) == ['addr_offset', 'group_offset', 'name', 'value', 'count', 'type', '',
|
||||
'sf', 'units', 'access', 'mandatory', 'static', '', '', '', '']
|
||||
|
||||
|
||||
def test_set_symbol():
|
||||
wb = xlsx.ModelWorkbook()
|
||||
values = [''] * 16
|
||||
values[2] = 'name'
|
||||
values[3] = 'value'
|
||||
values[12] = 'label'
|
||||
values[13] = 'description'
|
||||
wb.set_symbol(wb.wb['Index'], 2, values)
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['Index'])
|
||||
next(iter_rows) # skip header (['Model', 'Label', 'Description', None, ...])
|
||||
assert next(iter_rows) == ['', '', 'name', 'value', '', '', '',
|
||||
'', '', '', '', '', 'label', 'description', '', '']
|
||||
|
||||
|
||||
def test_set_comment():
|
||||
wb = xlsx.ModelWorkbook()
|
||||
wb.set_comment(wb.wb['Index'], 2, ['This is a comment'])
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['Index'])
|
||||
next(iter_rows)
|
||||
assert next(iter_rows)[0] == 'This is a comment'
|
||||
|
||||
|
||||
def test_set_hdr():
|
||||
wb = xlsx.ModelWorkbook()
|
||||
wb.set_hdr(wb.wb['Index'], ['This', 'is', 'a', 'test', 'header'])
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['Index'])
|
||||
assert next(iter_rows) == ['This', 'is', 'a', 'test', 'header']
|
||||
|
||||
|
||||
def test_spreadsheet_to_xlsx():
|
||||
wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx')
|
||||
with pytest.raises(ValueError) as exc:
|
||||
wb.spreadsheet_to_xlsx(702, [])
|
||||
assert 'Workbooks opened with existing file are read only' in str(exc.value)
|
||||
|
||||
spreadsheet_smdx_304 = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', '', ''],
|
||||
[0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''],
|
||||
[1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''],
|
||||
['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''],
|
||||
['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', '']
|
||||
]
|
||||
wb2 = xlsx.ModelWorkbook()
|
||||
wb2.spreadsheet_to_xlsx(304, spreadsheet_smdx_304)
|
||||
iter_rows = wb2.xlsx_iter_rows(wb2.wb['304'])
|
||||
for row in spreadsheet_smdx_304:
|
||||
assert next(iter_rows) == row
|
||||
|
||||
|
||||
def test_to_xlsx(tmp_path):
|
||||
spreadsheet_smdx_304 = [
|
||||
['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units',
|
||||
'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'],
|
||||
['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model',
|
||||
'Include to support orientation measurements', '', ''],
|
||||
[0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''],
|
||||
[1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''],
|
||||
['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''],
|
||||
['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''],
|
||||
['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''],
|
||||
['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', '']
|
||||
]
|
||||
with open('./sunspec2/models/json/model_304.json') as f:
|
||||
m_703 = json.load(f)
|
||||
wb = xlsx.ModelWorkbook()
|
||||
wb.to_xlsx(m_703)
|
||||
iter_rows = wb.xlsx_iter_rows(wb.wb['304'])
|
||||
for row in spreadsheet_smdx_304:
|
||||
assert next(iter_rows) == row
|
||||
Reference in New Issue
Block a user