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.

Files changed (39) hide show
  1. pymscada/__init__.py +7 -3
  2. pymscada/demo/logixclient.yaml +50 -0
  3. pymscada/demo/modbus_plc.py +46 -29
  4. pymscada/demo/modbusclient.yaml +10 -1
  5. pymscada/demo/modbusserver.yaml +12 -3
  6. pymscada/demo/pymscada-bus.service +1 -1
  7. pymscada/demo/pymscada-files.service +1 -1
  8. pymscada/demo/pymscada-history.service +1 -1
  9. pymscada/demo/pymscada-io-logixclient.service +15 -0
  10. pymscada/demo/{pymscada-modbusclient.service → pymscada-io-modbusclient.service} +1 -1
  11. pymscada/demo/{pymscada-modbusserver.service → pymscada-io-modbusserver.service} +1 -1
  12. pymscada/demo/pymscada-io-snmpclient.service +15 -0
  13. pymscada/demo/pymscada-wwwserver.service +1 -1
  14. pymscada/demo/snmpclient.yaml +73 -0
  15. pymscada/demo/tags.yaml +110 -5
  16. pymscada/demo/wwwserver.yaml +16 -10
  17. pymscada/history.py +46 -97
  18. pymscada/iodrivers/__init__.py +0 -0
  19. pymscada/iodrivers/logix_client.py +80 -0
  20. pymscada/iodrivers/logix_map.py +147 -0
  21. pymscada/{modbus_client.py → iodrivers/modbus_client.py} +3 -3
  22. pymscada/{modbus_server.py → iodrivers/modbus_server.py} +1 -1
  23. pymscada/iodrivers/snmp_client.py +75 -0
  24. pymscada/iodrivers/snmp_client2.py +53 -0
  25. pymscada/iodrivers/snmp_map.py +71 -0
  26. pymscada/main.py +24 -4
  27. pymscada/periodic.py +1 -1
  28. pymscada/samplers.py +93 -0
  29. pymscada/tag.py +0 -27
  30. pymscada/tools/walk.py +55 -0
  31. pymscada/validate.py +356 -0
  32. pymscada-0.0.15.dist-info/METADATA +270 -0
  33. pymscada-0.0.15.dist-info/RECORD +58 -0
  34. {pymscada-0.0.6.dist-info → pymscada-0.0.15.dist-info}/WHEEL +1 -1
  35. pymscada-0.0.6.dist-info/METADATA +0 -58
  36. pymscada-0.0.6.dist-info/RECORD +0 -45
  37. /pymscada/{modbus_map.py → iodrivers/modbus_map.py} +0 -0
  38. {pymscada-0.0.6.dist-info → pymscada-0.0.15.dist-info}/entry_points.txt +0 -0
  39. {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.modbus_client import ModbusClient
12
- from pymscada.modbus_server import ModbusServer
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
- 'modbusserver', 'modbusclient', 'simulate', 'checkout']
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
@@ -36,6 +36,6 @@ class Periodic:
36
36
  if sleep_for < 0.0:
37
37
  sleep_for = self.period
38
38
  self._time = time.time()
39
- logging.warn(f'{self._func} skipped at {self._time}')
39
+ logging.warning(f'{self._func} skipped at {self._time}')
40
40
  else:
41
41
  await asyncio.sleep(sleep_for)
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