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:
2026-09-02 20:46:59 +02:00
co-authored by Claude Opus 5
commit 79843aa2ae
968 changed files with 261182 additions and 0 deletions
View File
+45
View File
@@ -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)
+52
View File
@@ -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
+612
View File
@@ -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
}
]
}
+213
View File
@@ -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
}
]
}
]
}
+8
View File
@@ -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,,
1 Address Offset Group Offset Name Value Count Type Size Scale Factor Units RW Access (RW) Mandatory (M) Static (S) Label Description Detailed Description Standards
2 inclinometer group Inclinometer Model Include to support orientation measurements
3 0 ID 304 uint16 M S Model ID Model identifier
4 1 L uint16 M S Model Length Model length
5 inclinometer.incl 0 group
6 0 Inclx int32 -2 Degrees M X X-Axis inclination
7 2 Incly int32 -2 Degrees Y Y-Axis inclination
8 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
+221
View File
@@ -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')
+313
View File
@@ -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
+223
View File
@@ -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
+217
View File
@@ -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
+526
View File
@@ -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
+210
View File
@@ -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