pymscada 0.0.6__py3-none-any.whl → 0.0.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pymscada might be problematic. Click here for more details.
- pymscada/__init__.py +7 -3
- pymscada/demo/logixclient.yaml +50 -0
- pymscada/demo/modbus_plc.py +46 -29
- pymscada/demo/modbusclient.yaml +10 -1
- pymscada/demo/modbusserver.yaml +12 -3
- pymscada/demo/pymscada-bus.service +1 -1
- pymscada/demo/pymscada-files.service +1 -1
- pymscada/demo/pymscada-history.service +1 -1
- pymscada/demo/pymscada-io-logixclient.service +15 -0
- pymscada/demo/{pymscada-modbusclient.service → pymscada-io-modbusclient.service} +1 -1
- pymscada/demo/{pymscada-modbusserver.service → pymscada-io-modbusserver.service} +1 -1
- pymscada/demo/pymscada-io-snmpclient.service +15 -0
- pymscada/demo/pymscada-wwwserver.service +1 -1
- pymscada/demo/snmpclient.yaml +73 -0
- pymscada/demo/tags.yaml +110 -5
- pymscada/demo/wwwserver.yaml +16 -10
- pymscada/history.py +46 -97
- pymscada/iodrivers/__init__.py +0 -0
- pymscada/iodrivers/logix_client.py +80 -0
- pymscada/iodrivers/logix_map.py +147 -0
- pymscada/{modbus_client.py → iodrivers/modbus_client.py} +3 -3
- pymscada/{modbus_server.py → iodrivers/modbus_server.py} +1 -1
- pymscada/iodrivers/snmp_client.py +75 -0
- pymscada/iodrivers/snmp_client2.py +53 -0
- pymscada/iodrivers/snmp_map.py +71 -0
- pymscada/main.py +24 -4
- pymscada/periodic.py +1 -1
- pymscada/samplers.py +93 -0
- pymscada/tag.py +0 -27
- pymscada/tools/walk.py +55 -0
- pymscada/validate.py +356 -0
- pymscada-0.0.15.dist-info/METADATA +270 -0
- pymscada-0.0.15.dist-info/RECORD +58 -0
- {pymscada-0.0.6.dist-info → pymscada-0.0.15.dist-info}/WHEEL +1 -1
- pymscada-0.0.6.dist-info/METADATA +0 -58
- pymscada-0.0.6.dist-info/RECORD +0 -45
- /pymscada/{modbus_map.py → iodrivers/modbus_map.py} +0 -0
- {pymscada-0.0.6.dist-info → pymscada-0.0.15.dist-info}/entry_points.txt +0 -0
- {pymscada-0.0.6.dist-info → pymscada-0.0.15.dist-info}/licenses/LICENSE +0 -0
pymscada/main.py
CHANGED
|
@@ -8,10 +8,13 @@ from pymscada.config import Config
|
|
|
8
8
|
from pymscada.console import Console
|
|
9
9
|
from pymscada.files import Files
|
|
10
10
|
from pymscada.history import History
|
|
11
|
-
from pymscada.
|
|
12
|
-
from pymscada.
|
|
11
|
+
from pymscada.iodrivers.logix_client import LogixClient
|
|
12
|
+
from pymscada.iodrivers.modbus_client import ModbusClient
|
|
13
|
+
from pymscada.iodrivers.modbus_server import ModbusServer
|
|
14
|
+
from pymscada.iodrivers.snmp_client import SnmpClient
|
|
13
15
|
from pymscada.simulate import Simulate
|
|
14
16
|
from pymscada.www_server import WwwServer
|
|
17
|
+
from pymscada.validate import validate
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def args():
|
|
@@ -22,7 +25,11 @@ def args():
|
|
|
22
25
|
epilog='Python MobileSCADA.'
|
|
23
26
|
)
|
|
24
27
|
commands = ['bus', 'console', 'wwwserver', 'history', 'files',
|
|
25
|
-
'
|
|
28
|
+
'logixclient',
|
|
29
|
+
'modbusserver', 'modbusclient',
|
|
30
|
+
'snmpclient',
|
|
31
|
+
'simulate', 'checkout',
|
|
32
|
+
'validate']
|
|
26
33
|
parser.add_argument('module', type=str, choices=commands, metavar='action',
|
|
27
34
|
help=f'select one of: {", ".join(commands)}')
|
|
28
35
|
parser.add_argument('--config', metavar='file',
|
|
@@ -32,7 +39,7 @@ def args():
|
|
|
32
39
|
parser.add_argument('--verbose', action='store_true',
|
|
33
40
|
help="Set level to logging.INFO.")
|
|
34
41
|
parser.add_argument('--path', metavar='folder',
|
|
35
|
-
help="Working folder, used for history.")
|
|
42
|
+
help="Working folder, used for history and validate.")
|
|
36
43
|
parser.add_argument('--overwrite', action='store_true', default=False,
|
|
37
44
|
help='checkout may overwrite files, CARE!')
|
|
38
45
|
return parser.parse_args()
|
|
@@ -63,12 +70,18 @@ async def run():
|
|
|
63
70
|
elif options.module == 'files':
|
|
64
71
|
config = Config(options.config)
|
|
65
72
|
module = Files(**config)
|
|
73
|
+
elif options.module == 'logixclient':
|
|
74
|
+
config = Config(options.config)
|
|
75
|
+
module = LogixClient(**config)
|
|
66
76
|
elif options.module == 'modbusclient':
|
|
67
77
|
config = Config(options.config)
|
|
68
78
|
module = ModbusClient(**config)
|
|
69
79
|
elif options.module == 'modbusserver':
|
|
70
80
|
config = Config(options.config)
|
|
71
81
|
module = ModbusServer(**config)
|
|
82
|
+
elif options.module == 'snmpclient':
|
|
83
|
+
config = Config(options.config)
|
|
84
|
+
module = SnmpClient(**config)
|
|
72
85
|
elif options.module == 'simulate':
|
|
73
86
|
config = Config(options.config)
|
|
74
87
|
tag_info = dict(Config(options.tags))
|
|
@@ -76,6 +89,13 @@ async def run():
|
|
|
76
89
|
elif options.module == 'checkout':
|
|
77
90
|
checkout(overwrite=options.overwrite)
|
|
78
91
|
return
|
|
92
|
+
elif options.module == 'validate':
|
|
93
|
+
r, e = validate(options.path)
|
|
94
|
+
if r == True:
|
|
95
|
+
print(f'Config files in {options.path} valid.')
|
|
96
|
+
else:
|
|
97
|
+
print(e)
|
|
98
|
+
return
|
|
79
99
|
else:
|
|
80
100
|
logging.warning(f'no {options.module}')
|
|
81
101
|
await module.start()
|
pymscada/periodic.py
CHANGED
pymscada/samplers.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Sampling utilities."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shutil
|
|
4
|
+
from pymscada.tag import Tag
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CPU_TEMP = False
|
|
8
|
+
if Path('/sys/class/thermal/thermal_zone0/temp').is_file():
|
|
9
|
+
CPU_TEMP = True
|
|
10
|
+
with open('/proc/stat', 'r') as f:
|
|
11
|
+
cpu_load = [int(x) for x in f.readline().split(None)[1:]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_cpu_temp() -> float:
|
|
15
|
+
"""Return the CPU temp in °C."""
|
|
16
|
+
temp = 0.
|
|
17
|
+
if not CPU_TEMP:
|
|
18
|
+
return temp
|
|
19
|
+
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
|
|
20
|
+
temp = int(f.read()) / 1000
|
|
21
|
+
return temp
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_cpu_load() -> float:
|
|
25
|
+
"""Return cpu load in %."""
|
|
26
|
+
global cpu_load
|
|
27
|
+
old_cpu_load = cpu_load
|
|
28
|
+
with open('/proc/stat', 'r') as f:
|
|
29
|
+
cpu_load = [int(x) for x in f.readline().split(None)[1:]]
|
|
30
|
+
idle = cpu_load[3] - old_cpu_load[3]
|
|
31
|
+
used = sum(cpu_load) - sum(old_cpu_load)
|
|
32
|
+
if used == 0:
|
|
33
|
+
return 0
|
|
34
|
+
return 100 * (used - idle) / used
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_disk_use() -> float:
|
|
38
|
+
"""Return disk usage in %."""
|
|
39
|
+
stat = shutil.disk_usage(Path('/'))
|
|
40
|
+
return 100 * stat.used / stat.total
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Ping():
|
|
44
|
+
"""Async ping class."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, map: dict[str: Tag]) -> None:
|
|
47
|
+
"""Ping map, key as IP address, time placed into Tag.value."""
|
|
48
|
+
self.map = map
|
|
49
|
+
|
|
50
|
+
def step(self):
|
|
51
|
+
"""Step."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Ramp():
|
|
56
|
+
"""Ramp a value."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, ramp_tag: Tag, setpoint_tag: Tag, ramp=1) -> None:
|
|
59
|
+
"""Ramp."""
|
|
60
|
+
self.ramp_tag = ramp_tag
|
|
61
|
+
self.setpoint_tag = setpoint_tag
|
|
62
|
+
self.ramp = ramp
|
|
63
|
+
|
|
64
|
+
def step(self) -> None:
|
|
65
|
+
"""Step."""
|
|
66
|
+
if self.setpoint_tag.value is None:
|
|
67
|
+
return
|
|
68
|
+
if self.ramp_tag.value is None:
|
|
69
|
+
self.ramp_tag.value = self.setpoint_tag.value
|
|
70
|
+
return
|
|
71
|
+
if self.setpoint_tag.value > self.ramp_tag.value:
|
|
72
|
+
self.ramp_tag.value = min(self.ramp_tag.value + self.ramp,
|
|
73
|
+
self.setpoint_tag.value)
|
|
74
|
+
else:
|
|
75
|
+
self.ramp_tag.value = max(self.ramp_tag.value - self.ramp,
|
|
76
|
+
self.setpoint_tag.value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Average():
|
|
80
|
+
"""Average a value."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, tag: Tag, count=60) -> None:
|
|
83
|
+
"""Average."""
|
|
84
|
+
self.tag = tag
|
|
85
|
+
self.count = count
|
|
86
|
+
self.samples = []
|
|
87
|
+
|
|
88
|
+
def step(self, sample: float) -> None:
|
|
89
|
+
"""Take a sample, update tag value if count samples taken."""
|
|
90
|
+
self.samples.append(sample)
|
|
91
|
+
if len(self.samples) == self.count:
|
|
92
|
+
self.tag.value = sum(self.samples) / self.count
|
|
93
|
+
self.samples = []
|
pymscada/tag.py
CHANGED
|
@@ -18,33 +18,6 @@ TYPES = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def validate_tag(tag: dict):
|
|
22
|
-
"""Correct tag dictionary in place."""
|
|
23
|
-
if 'desc' not in tag:
|
|
24
|
-
logging.error(f"{tag} missing desc")
|
|
25
|
-
if 'multi' in tag:
|
|
26
|
-
if 'type' in tag:
|
|
27
|
-
logging.warning(f"{tag} redundant type cast for multi")
|
|
28
|
-
tag['type'] = int
|
|
29
|
-
else:
|
|
30
|
-
if 'type' not in tag:
|
|
31
|
-
tag['type'] = float
|
|
32
|
-
else:
|
|
33
|
-
try:
|
|
34
|
-
tag['type'] = TYPES[tag['type']]
|
|
35
|
-
except KeyError:
|
|
36
|
-
logging.error(f"{tag} invalid type {tag['type']}")
|
|
37
|
-
if tag['type'] is int:
|
|
38
|
-
if 'dp' in tag:
|
|
39
|
-
logging.warning(f"{tag} redundant dp for int")
|
|
40
|
-
tag['dp'] = 0
|
|
41
|
-
elif tag['type'] is float and 'dp' not in tag:
|
|
42
|
-
tag['dp'] = 2
|
|
43
|
-
elif tag['type'] in [str, bytes] and 'dp' in tag:
|
|
44
|
-
logging.warning(f"{tag} str/bytes cannot use dp")
|
|
45
|
-
del tag['dp']
|
|
46
|
-
|
|
47
|
-
|
|
48
21
|
class UniqueTag(type):
|
|
49
22
|
"""Super Tag class only create unique tags for unique tag names."""
|
|
50
23
|
|
pymscada/tools/walk.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Walk whole MIB
|
|
3
|
+
++++++++++++++
|
|
4
|
+
|
|
5
|
+
Send a series of SNMP GETNEXT requests using the following options:
|
|
6
|
+
|
|
7
|
+
* with SNMPv3, user 'usr-md5-none', MD5 authentication, no privacy
|
|
8
|
+
* over IPv4/UDP
|
|
9
|
+
* to an Agent at demo.snmplabs.com:161
|
|
10
|
+
* for all OIDs in IF-MIB
|
|
11
|
+
|
|
12
|
+
Functionally similar to:
|
|
13
|
+
|
|
14
|
+
| $ snmpwalk -v3 -lauthPriv -u usr-md5-none -A authkey1 -X privkey1 demo.snmplabs.com IF-MIB::
|
|
15
|
+
|
|
16
|
+
"""#
|
|
17
|
+
from pysnmp.hlapi import *
|
|
18
|
+
|
|
19
|
+
mibs = {
|
|
20
|
+
'MIKROTIK-MIB': '1.3.6.1.4.1.14988',
|
|
21
|
+
'MIB-2': '1.3.6.1.2.1',
|
|
22
|
+
'HOST-RESOURCES-MIB': '1.3.6.1.2.1.25',
|
|
23
|
+
'IF-MIB': '1.3.6.1.2.1.2',
|
|
24
|
+
'IP-MIB': '1.3.6.1.2.1.4',
|
|
25
|
+
'IP-FORWARD-MIB': '1.3.6.1.2.1.4.21',
|
|
26
|
+
'IPV6-MIB': '1.3.6.1.2.1.55',
|
|
27
|
+
'BRIDGE-MIB': '1.3.6.1.2.1.17',
|
|
28
|
+
'DHCP-SERVER-MIB': '1.3.6.1.4.1.14988.1.1.8',
|
|
29
|
+
'CISCO-AAA-SESSION-MIB': '1.3.6.1.4.1.9.9.39',
|
|
30
|
+
'ENTITY-MIB': '1.3.6.1.2.1.47',
|
|
31
|
+
'UPS-MIB': '1.3.6.1.2.1.33',
|
|
32
|
+
'SQUID-MIB': '1.3.6.1.4.1.3495',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
iterator = nextCmd(
|
|
36
|
+
SnmpEngine(),
|
|
37
|
+
UsmUserData('public'),
|
|
38
|
+
UdpTransportTarget(('172.26.3.254', 161)),
|
|
39
|
+
ContextData(),
|
|
40
|
+
# ObjectType(ObjectIdentity('1.3.6.1.2.1.1.1.0')),
|
|
41
|
+
# ObjectType(ObjectIdentity('1.3.6.1.2.1.1.6.0'))
|
|
42
|
+
ObjectType(ObjectIdentity(mibs['MIB-2']))
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for errorIndication, errorStatus, errorIndex, varBinds in iterator:
|
|
46
|
+
if errorIndication:
|
|
47
|
+
print(errorIndication)
|
|
48
|
+
break
|
|
49
|
+
elif errorStatus:
|
|
50
|
+
print('%s at %s' % (errorStatus.prettyPrint(),
|
|
51
|
+
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
|
|
52
|
+
break
|
|
53
|
+
else:
|
|
54
|
+
for varBind in varBinds:
|
|
55
|
+
print(' = '.join([x.prettyPrint() for x in varBind]))
|
pymscada/validate.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Config file validation."""
|
|
2
|
+
from cerberus import Validator
|
|
3
|
+
from yaml import dump
|
|
4
|
+
from pymscada import Config
|
|
5
|
+
from socket import inet_aton
|
|
6
|
+
|
|
7
|
+
INT_TAG = {
|
|
8
|
+
'desc': {'type': 'string'},
|
|
9
|
+
'type': {'type': 'string', 'allowed': ['int']},
|
|
10
|
+
'min': {'type': 'integer', 'required': False},
|
|
11
|
+
'max': {'type': 'integer', 'required': False},
|
|
12
|
+
'init': {'type': 'integer', 'required': False},
|
|
13
|
+
'units': {'type': 'string', 'maxlength': 5, 'required': False},
|
|
14
|
+
'format': {'type': 'string', 'allowed': ['date', 'time', 'datetime']}
|
|
15
|
+
}
|
|
16
|
+
FLOAT_TAG = {
|
|
17
|
+
'desc': {'type': 'string'},
|
|
18
|
+
'type': {'type': 'string', 'allowed': ['float']},
|
|
19
|
+
'min': {'type': 'float', 'required': False},
|
|
20
|
+
'max': {'type': 'float', 'required': False},
|
|
21
|
+
'init': {'type': 'float', 'required': False},
|
|
22
|
+
'units': {'type': 'string', 'maxlength': 5, 'required': False},
|
|
23
|
+
'dp': {'type': 'integer', 'min': 0, 'max': 6, 'required': False},
|
|
24
|
+
}
|
|
25
|
+
STR_TAG = {
|
|
26
|
+
'desc': {'type': 'string'},
|
|
27
|
+
'type': {'type': 'string', 'allowed': ['str']},
|
|
28
|
+
'init': {'type': 'string', 'required': False},
|
|
29
|
+
}
|
|
30
|
+
LIST_TAG = {
|
|
31
|
+
'desc': {'type': 'string'},
|
|
32
|
+
'type': {'type': 'string', 'allowed': ['list']},
|
|
33
|
+
}
|
|
34
|
+
DICT_TAG = {
|
|
35
|
+
'desc': {'type': 'string'},
|
|
36
|
+
'type': {'type': 'string', 'allowed': ['dict']},
|
|
37
|
+
'init': {}
|
|
38
|
+
}
|
|
39
|
+
MULTI_TAG = {
|
|
40
|
+
'desc': {'type': 'string'},
|
|
41
|
+
'multi': {'type': 'list'},
|
|
42
|
+
'init': {'type': 'integer', 'required': False},
|
|
43
|
+
}
|
|
44
|
+
BYTES_TAG = {
|
|
45
|
+
'desc': {'type': 'string'},
|
|
46
|
+
'type': {'type': 'string', 'allowed': ['bytes']},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
TAG_SCHEMA = {
|
|
50
|
+
'type': 'dict',
|
|
51
|
+
'keysrules': {
|
|
52
|
+
'type': 'string',
|
|
53
|
+
# tag name discovery, save for later checking
|
|
54
|
+
'ms_tagname': True
|
|
55
|
+
},
|
|
56
|
+
'valuesrules': {
|
|
57
|
+
'type': 'dict',
|
|
58
|
+
'oneof_schema': [
|
|
59
|
+
INT_TAG, FLOAT_TAG, STR_TAG, LIST_TAG, DICT_TAG, MULTI_TAG,
|
|
60
|
+
BYTES_TAG
|
|
61
|
+
],
|
|
62
|
+
# tag type discovery, save for later checking
|
|
63
|
+
'ms_tagtype': True
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
BUS_SCHEMA = {
|
|
68
|
+
'type': 'dict',
|
|
69
|
+
'schema': {
|
|
70
|
+
'ip': {'type': 'string', 'ms_ip': True},
|
|
71
|
+
'port': {'type': 'integer', 'min': 1024, 'max': 65536}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
BRHR_LIST = {
|
|
76
|
+
'type': {'type': 'string', 'allowed': ['br', 'hr']},
|
|
77
|
+
}
|
|
78
|
+
H123P_LIST = {
|
|
79
|
+
'type': {
|
|
80
|
+
'type': 'string',
|
|
81
|
+
'allowed': ['h1', 'h2', 'h3', 'p'],
|
|
82
|
+
},
|
|
83
|
+
'desc': {'type': 'string'}
|
|
84
|
+
}
|
|
85
|
+
VALUESETFILES_LIST = {
|
|
86
|
+
'type': {
|
|
87
|
+
'type': 'string',
|
|
88
|
+
'allowed': ['value', 'setpoint', 'files'],
|
|
89
|
+
},
|
|
90
|
+
# tagname must have been found in parsing tags.yaml
|
|
91
|
+
'tagname': {'type': 'string', 'ms_tagname': False}
|
|
92
|
+
}
|
|
93
|
+
SELECTDICT_LIST = {
|
|
94
|
+
'type': {'type': 'string', 'allowed': ['selectdict']},
|
|
95
|
+
# tagname must have been found in parsing tags.yaml
|
|
96
|
+
'tagname': {'type': 'string', 'ms_tagname': False},
|
|
97
|
+
'opts': {
|
|
98
|
+
'type': 'dict',
|
|
99
|
+
# 'schema': {
|
|
100
|
+
# 'type': 'dict',
|
|
101
|
+
# # 'schema': {
|
|
102
|
+
# # 'type': {'type': 'string', 'required': False},
|
|
103
|
+
# # 'multi': {'type': 'list', 'required': False},
|
|
104
|
+
# # }
|
|
105
|
+
# },
|
|
106
|
+
'required': False
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
UPLOT_LIST = {
|
|
110
|
+
'type': {'type': 'string', 'allowed': ['uplot']},
|
|
111
|
+
'ms': {
|
|
112
|
+
'type': 'dict',
|
|
113
|
+
# 'schema': {
|
|
114
|
+
# 'type': 'dict',
|
|
115
|
+
# # 'schema': {
|
|
116
|
+
# # 'type': {'type': 'string', 'required': False},
|
|
117
|
+
# # 'multi': {'type': 'list', 'required': False},
|
|
118
|
+
# # }
|
|
119
|
+
# },
|
|
120
|
+
'required': False
|
|
121
|
+
},
|
|
122
|
+
'axes': {
|
|
123
|
+
'type': 'list',
|
|
124
|
+
'schema': {
|
|
125
|
+
'type': 'dict',
|
|
126
|
+
'schema': {
|
|
127
|
+
'scale': {'type': 'string'},
|
|
128
|
+
'range': {'type': 'list', 'items': [
|
|
129
|
+
{'type': 'float'}, {'type': 'float'}
|
|
130
|
+
]},
|
|
131
|
+
'dp': {'type': 'integer', 'required': False}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
'series': {
|
|
136
|
+
'type': 'list',
|
|
137
|
+
'schema': {
|
|
138
|
+
'type': 'dict',
|
|
139
|
+
'schema': {
|
|
140
|
+
# tagname must have been found in parsing tags.yaml
|
|
141
|
+
'tagname': {'type': 'string', 'ms_tagname': False},
|
|
142
|
+
'label': {'type': 'string', 'required': False},
|
|
143
|
+
'scale': {'type': 'string', 'required': False},
|
|
144
|
+
'color': {'type': 'string', 'required': False},
|
|
145
|
+
'width': {'type': 'float', 'required': False},
|
|
146
|
+
'dp': {'type': 'integer', 'required': False},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
LIST_WWWSERVER = {
|
|
153
|
+
'type': 'dict',
|
|
154
|
+
'schema': {
|
|
155
|
+
'name': {'type': 'string'},
|
|
156
|
+
'parent': {'type': 'string', 'nullable': True},
|
|
157
|
+
'items': {
|
|
158
|
+
'type': 'list',
|
|
159
|
+
'schema': {
|
|
160
|
+
'type': 'dict',
|
|
161
|
+
'oneof_schema': [
|
|
162
|
+
BRHR_LIST, H123P_LIST, VALUESETFILES_LIST,
|
|
163
|
+
SELECTDICT_LIST, UPLOT_LIST
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
WWWSERVER_SCHEMA = {
|
|
171
|
+
'type': 'dict',
|
|
172
|
+
'schema': {
|
|
173
|
+
'bus_ip': {'type': 'string', 'ms_ip': True},
|
|
174
|
+
'bus_port': {'type': 'integer', 'min': 1024, 'max': 65536},
|
|
175
|
+
'ip': {'type': 'string', 'ms_ip': True},
|
|
176
|
+
'port': {'type': 'integer', 'min': 1024, 'max': 65536},
|
|
177
|
+
'get_path': {'nullable': True},
|
|
178
|
+
'paths': {'type': 'list', 'allowed': ['history', 'config', 'pdf']},
|
|
179
|
+
'pages': {
|
|
180
|
+
'type': 'list',
|
|
181
|
+
'schema': LIST_WWWSERVER
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
HISTORY_SCHEMA = {
|
|
187
|
+
'type': 'dict',
|
|
188
|
+
'schema': {
|
|
189
|
+
'bus_ip': {'type': 'string', 'ms_ip': True},
|
|
190
|
+
'bus_port': {'type': 'integer', 'min': 1024, 'max': 65536},
|
|
191
|
+
'path': {'type': 'string'},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
MODBUSSERVER_SCHEMA = {
|
|
196
|
+
'type': 'dict',
|
|
197
|
+
'schema': {
|
|
198
|
+
'bus_ip': {'type': 'string', 'ms_ip': True, 'nullable': True},
|
|
199
|
+
'bus_port': {'type': 'integer', 'min': 1024, 'max': 65536,
|
|
200
|
+
'nullable': True},
|
|
201
|
+
'path': {'type': 'string'},
|
|
202
|
+
'rtus': {
|
|
203
|
+
'type': 'list',
|
|
204
|
+
'schema': {
|
|
205
|
+
'type': 'dict',
|
|
206
|
+
'schema': {
|
|
207
|
+
'name': {},
|
|
208
|
+
'ip': {},
|
|
209
|
+
'port': {},
|
|
210
|
+
'tcp_udp': {'type': 'string', 'allowed': ['tcp', 'udp']},
|
|
211
|
+
'serve': {
|
|
212
|
+
'type': 'list',
|
|
213
|
+
'schema': {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
'tags': {
|
|
219
|
+
'type': 'dict',
|
|
220
|
+
'keysrules': {
|
|
221
|
+
'type': 'string',
|
|
222
|
+
'ms_tagname': False
|
|
223
|
+
},
|
|
224
|
+
'valuesrules': {
|
|
225
|
+
'type': 'dict',
|
|
226
|
+
'schema': {
|
|
227
|
+
'type': {},
|
|
228
|
+
'addr': {}
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
MODBUSCLIENT_SCHEMA = {
|
|
236
|
+
'type': 'dict',
|
|
237
|
+
'schema': {
|
|
238
|
+
'bus_ip': {'type': 'string', 'ms_ip': True, 'nullable': True},
|
|
239
|
+
'bus_port': {'type': 'integer', 'min': 1024, 'max': 65536,
|
|
240
|
+
'nullable': True},
|
|
241
|
+
'path': {'type': 'string'},
|
|
242
|
+
'rtus': {
|
|
243
|
+
'type': 'list',
|
|
244
|
+
'schema': {
|
|
245
|
+
'type': 'dict',
|
|
246
|
+
'schema': {
|
|
247
|
+
'name': {},
|
|
248
|
+
'ip': {},
|
|
249
|
+
'port': {},
|
|
250
|
+
'tcp_udp': {'type': 'string', 'allowed': ['tcp', 'udp']},
|
|
251
|
+
'rate': {},
|
|
252
|
+
'read': {
|
|
253
|
+
'type': 'list',
|
|
254
|
+
'schema': {}
|
|
255
|
+
},
|
|
256
|
+
'writeok': {
|
|
257
|
+
'type': 'list',
|
|
258
|
+
'schema': {}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
'tags': {
|
|
264
|
+
'type': 'dict',
|
|
265
|
+
'keysrules': {
|
|
266
|
+
'type': 'string',
|
|
267
|
+
'ms_tagname': False
|
|
268
|
+
},
|
|
269
|
+
'valuesrules': {
|
|
270
|
+
'type': 'dict',
|
|
271
|
+
'schema': {
|
|
272
|
+
'type': {},
|
|
273
|
+
'addr': {}
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class MsValidator(Validator):
|
|
281
|
+
"""Add additional application checks"""
|
|
282
|
+
ms_tagnames = {}
|
|
283
|
+
|
|
284
|
+
def _validate_ms_tagname(self, constraint, field, value):
|
|
285
|
+
""" Test tagname exists, capture when true.
|
|
286
|
+
|
|
287
|
+
The rule's arguments are validated against this schema:
|
|
288
|
+
{'type': 'boolean'}
|
|
289
|
+
"""
|
|
290
|
+
if '.' in field:
|
|
291
|
+
self._error(field, "'.' invalid in tag definition.")
|
|
292
|
+
if constraint:
|
|
293
|
+
if field in self.ms_tagnames:
|
|
294
|
+
self._error(field, 'attempt to redefine')
|
|
295
|
+
else:
|
|
296
|
+
self.ms_tagnames[field] = {'type': None}
|
|
297
|
+
else:
|
|
298
|
+
if value not in self.ms_tagnames:
|
|
299
|
+
self._error(field, 'tag was not defined in tags.yaml')
|
|
300
|
+
else:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
def _validate_ms_tagtype(self, constraint, field, value):
|
|
304
|
+
""" Test tagname type, capture when true.
|
|
305
|
+
|
|
306
|
+
The rule's arguments are validated against this schema:
|
|
307
|
+
{'type': 'boolean'}
|
|
308
|
+
"""
|
|
309
|
+
if constraint and field in self.ms_tagnames:
|
|
310
|
+
if self.ms_tagnames[field]['type'] is None:
|
|
311
|
+
if 'multi' in value:
|
|
312
|
+
self.ms_tagnames[field]['type'] = 'int'
|
|
313
|
+
else:
|
|
314
|
+
self.ms_tagnames[field]['type'] = value['type']
|
|
315
|
+
else:
|
|
316
|
+
self._error(field, 'attempt to redefine type')
|
|
317
|
+
else:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
def _validate_ms_ip(self, constraint, field, value):
|
|
321
|
+
""" Test session.inet_aton works for the address.
|
|
322
|
+
|
|
323
|
+
The rule's arguments are validated against this schema:
|
|
324
|
+
{'type': 'boolean'}
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
inet_aton(value)
|
|
328
|
+
except (OSError, TypeError):
|
|
329
|
+
self._error(field, 'ip address fails socket.inet_aton')
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def validate(path: str=None):
|
|
333
|
+
"""Validate."""
|
|
334
|
+
s = {
|
|
335
|
+
'tags': TAG_SCHEMA,
|
|
336
|
+
'bus': BUS_SCHEMA,
|
|
337
|
+
'wwwserver': WWWSERVER_SCHEMA,
|
|
338
|
+
'history': HISTORY_SCHEMA,
|
|
339
|
+
'modbusserver': MODBUSSERVER_SCHEMA,
|
|
340
|
+
'modbusclient': MODBUSCLIENT_SCHEMA,
|
|
341
|
+
}
|
|
342
|
+
prefix = ''
|
|
343
|
+
if path is not None:
|
|
344
|
+
prefix = path + '/'
|
|
345
|
+
c = {
|
|
346
|
+
'tags': dict(Config(f'{prefix}tags.yaml')),
|
|
347
|
+
'bus': dict(Config(f'{prefix}bus.yaml')),
|
|
348
|
+
'wwwserver': dict(Config(f'{prefix}wwwserver.yaml')),
|
|
349
|
+
'history': dict(Config(f'{prefix}history.yaml')),
|
|
350
|
+
'modbusserver': dict(Config(f'{prefix}modbusserver.yaml')),
|
|
351
|
+
'modbusclient': dict(Config(f'{prefix}modbusclient.yaml')),
|
|
352
|
+
}
|
|
353
|
+
v = MsValidator(s)
|
|
354
|
+
res = v.validate(c)
|
|
355
|
+
wdy = dump(v.errors) # , default_flow_style=False)
|
|
356
|
+
return res, wdy
|