pymscada 0.1.0a3__tar.gz → 0.1.0a4__tar.gz

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 (78) hide show
  1. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/PKG-INFO +1 -1
  2. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/pyproject.toml +1 -1
  3. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/checkout.py +38 -5
  4. pymscada-0.1.0a4/src/pymscada/demo/ping.yaml +12 -0
  5. pymscada-0.1.0a4/src/pymscada/demo/pymscada-io-ping.service +15 -0
  6. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/tags.yaml +10 -1
  7. pymscada-0.1.0a4/src/pymscada/demo/wwwserver.yaml +533 -0
  8. pymscada-0.1.0a4/src/pymscada/iodrivers/ping_client.py +120 -0
  9. pymscada-0.1.0a4/src/pymscada/iodrivers/ping_map.py +43 -0
  10. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/main.py +44 -19
  11. pymscada-0.1.0a3/src/pymscada/demo/wwwserver.yaml +0 -87
  12. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/LICENSE +0 -0
  13. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/README.md +0 -0
  14. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/__init__.py +0 -0
  15. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/__main__.py +0 -0
  16. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/bus_client.py +0 -0
  17. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/bus_server.py +0 -0
  18. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/config.py +0 -0
  19. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/console.py +0 -0
  20. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/README.md +0 -0
  21. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/__init__.py +0 -0
  22. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/bus.yaml +0 -0
  23. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/files.yaml +0 -0
  24. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/history.yaml +0 -0
  25. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/logixclient.yaml +0 -0
  26. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/modbus_plc.py +0 -0
  27. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/modbusclient.yaml +0 -0
  28. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/modbusserver.yaml +0 -0
  29. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-bus.service +0 -0
  30. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  31. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-files.service +0 -0
  32. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-history.service +0 -0
  33. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
  34. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  35. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  36. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
  37. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  38. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/snmpclient.yaml +0 -0
  39. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/files.py +0 -0
  40. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/history.py +0 -0
  41. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/__init__.py +0 -0
  42. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/logix_client.py +0 -0
  43. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/logix_map.py +0 -0
  44. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/modbus_client.py +0 -0
  45. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/modbus_map.py +0 -0
  46. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/modbus_server.py +0 -0
  47. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/snmp_client.py +0 -0
  48. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/snmp_map.py +0 -0
  49. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/misc.py +0 -0
  50. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/pdf/__init__.py +0 -0
  51. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/pdf/one.pdf +0 -0
  52. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/pdf/two.pdf +0 -0
  53. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/periodic.py +0 -0
  54. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/protocol_constants.py +0 -0
  55. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/samplers.py +0 -0
  56. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/tag.py +0 -0
  57. {pymscada-0.1.0a3/src/pymscada/iodrivers → pymscada-0.1.0a4/src/pymscada/tools}/snmp_client2.py +0 -0
  58. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/tools/walk.py +0 -0
  59. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/validate.py +0 -0
  60. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/www_server.py +0 -0
  61. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/__init__.py +0 -0
  62. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/bus_echo.py +0 -0
  63. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/iodrivers/test_logix.py +0 -0
  64. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/iodrivers/test_modbus.py +0 -0
  65. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/busserver.yaml +0 -0
  66. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_0.dat +0 -0
  67. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_10_2.dat +0 -0
  68. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_15.dat +0 -0
  69. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_26.dat +0 -0
  70. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_50.dat +0 -0
  71. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_bus_server.py +0 -0
  72. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_config.py +0 -0
  73. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_misc.py +0 -0
  74. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_periodic.py +0 -0
  75. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_samplers.py +0 -0
  76. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_tag.py +0 -0
  77. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_tag_history.py +0 -0
  78. {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymscada
3
- Version: 0.1.0a3
3
+ Version: 0.1.0a4
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-Email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pymscada"
3
- version = "0.1.0a3"
3
+ version = "0.1.0a4"
4
4
  description = "Shared tag value SCADA with python backup and Angular UI"
5
5
  authors = [
6
6
  { name = "Jamie Walton", email = "jamie@walton.net.nz" },
@@ -1,4 +1,5 @@
1
1
  """Create base config folder and check out demo files."""
2
+ import difflib
2
3
  from pathlib import Path
3
4
  import sys
4
5
  from pymscada.config import get_demo_files, get_pdf_files
@@ -36,7 +37,7 @@ def make_pdf():
36
37
  target.write_bytes(pdf_file.read_bytes())
37
38
 
38
39
 
39
- def make_config(overwrite):
40
+ def make_config(overwrite: bool):
40
41
  """Make the config folder, if missing, and copy files in."""
41
42
  config_dir = PATH['__DIR__'].joinpath('config')
42
43
  if not config_dir.exists():
@@ -62,11 +63,43 @@ def make_config(overwrite):
62
63
  target.write_bytes(config_file.read_bytes())
63
64
 
64
65
 
65
- def checkout(overwrite=False):
66
+ def read_with_subst(file: Path):
67
+ """Read the file and replace DIR markers."""
68
+ rd = file.read_bytes().decode()
69
+ if str(file).endswith('service'):
70
+ for k, v in PATH.items():
71
+ rd = rd.replace(k, str(v.absolute()))
72
+ lines = rd.splitlines()
73
+ return lines
74
+
75
+
76
+ def compare_config():
77
+ """Compare old and new config."""
78
+ config_dir = PATH['__DIR__'].joinpath('config')
79
+ if not config_dir.exists():
80
+ print('No config dir, are you in the right directory')
81
+ return
82
+ for config_file in get_demo_files():
83
+ target = config_dir.joinpath(config_file.name)
84
+ if target.exists():
85
+ new_lines = read_with_subst(config_file)
86
+ old_lines = read_with_subst(target)
87
+ diff = list(difflib.unified_diff(old_lines, new_lines,
88
+ fromfile=str(target), tofile=str(config_file)))
89
+ if len(diff):
90
+ print('\n'.join(diff), '\n')
91
+ else:
92
+ print(f'\n--- MISSING FILE\n\n+++ {config_file}')
93
+
94
+
95
+ def checkout(overwrite=False, diff=False):
66
96
  """Do it."""
67
97
  for name in PATH:
68
98
  if not PATH[name].exists():
69
99
  raise SystemExit(f'{PATH[name]} is missing')
70
- make_history()
71
- make_pdf()
72
- make_config(overwrite)
100
+ if diff:
101
+ compare_config()
102
+ else:
103
+ make_history()
104
+ make_pdf(diff)
105
+ make_config(overwrite, diff)
@@ -0,0 +1,12 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1324
3
+ tags:
4
+ localhost_ping:
5
+ type: ping
6
+ addr: '127.0.0.1'
7
+ electronet_ping:
8
+ type: ping
9
+ addr: electronet.co.nz
10
+ google_ping:
11
+ type: ping
12
+ addr: www.google.com
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=pymscada - Ping client
3
+ BindsTo=pymscada-bus.service
4
+ After=pymscada-bus.service
5
+
6
+ [Service]
7
+ WorkingDirectory=__DIR__
8
+ ExecStart=__PYMSCADA__ ping --config __DIR__/config/ping.yaml
9
+ Restart=always
10
+ RestartSec=5
11
+ User=mscada
12
+ Group=mscada
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -253,4 +253,13 @@ Router_eth8_bytes_in:
253
253
  type: int
254
254
  Router_eth8_bytes_out:
255
255
  desc: eth8 bytes out
256
- type: int
256
+ type: int
257
+ localhost_ping:
258
+ desc: Ping time to localhost
259
+ units: ms
260
+ google_ping:
261
+ desc: Ping time to google
262
+ units: ms
263
+ electronet_ping:
264
+ desc: Ping time to electronet
265
+ units: ms
@@ -0,0 +1,533 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1324
3
+ ip: 0.0.0.0
4
+ port: 8324
5
+ get_path:
6
+ paths:
7
+ - history
8
+ - config
9
+ - pdf
10
+ pages:
11
+ - name: Default Main
12
+ parent:
13
+ items:
14
+ - {desc: Default tags, type: h1}
15
+ - {tagname: IntSet, type: setpoint}
16
+ - {tagname: IntVal, type: value}
17
+ - {tagname: FloatSet, type: setpoint}
18
+ - {tagname: FloatVal, type: value}
19
+ - {tagname: MultiSet, type: setpoint}
20
+ - {tagname: MultiVal, type: value}
21
+ - {tagname: StringSet, type: setpoint}
22
+ - {tagname: StringVal, type: value}
23
+ - {tagname: TimeSet, type: setpoint}
24
+ - {tagname: TimeVal, type: value}
25
+ - {tagname: DateSet, type: setpoint}
26
+ - {tagname: DateVal, type: value}
27
+ - {tagname: DateTimeSet, type: setpoint}
28
+ - {tagname: DateTimeVal, type: value}
29
+ - type: selectdict
30
+ tagname: MultiSelect
31
+ opts:
32
+ type: multi
33
+ multi:
34
+ - Zero
35
+ - One
36
+ - Two
37
+ - Three
38
+ - Four
39
+ - Five
40
+ - {tagname: FloatSelect, type: selectdict, opts: {type: float, dp: 2}}
41
+ - name: Trends
42
+ parent: Dropdown
43
+ items:
44
+ - type: uplot # Do all times in seconds, which uplot uses.
45
+ ms:
46
+ desc: Sample Trend
47
+ age: 172800
48
+ legend_pos: left
49
+ time_pos: left
50
+ time_res: m
51
+ axes:
52
+ - scale: x
53
+ range: [-86400, 0] # 86400 172800 1209600
54
+ - scale: '°C'
55
+ range: [0.0, 100.0]
56
+ dp: 1
57
+ - scale: '%'
58
+ range: [0.0, 100.0]
59
+ dp: 1
60
+ # side: 1
61
+ # bands: # TODO
62
+ # - series: [I_Transpower_Limit_Hi, I_Transpower_Limit_Lo]
63
+ # fill: [red, 0.2]
64
+ # dir: -1
65
+ series:
66
+ - tagname: cpu_load
67
+ label: CPU Load
68
+ scale: '%'
69
+ color: black
70
+ width: 1.5
71
+ dp: 1
72
+ - tagname: disk_use
73
+ label: Disk Use
74
+ scale: '%'
75
+ color: blue
76
+ width: 1
77
+ dp: 1
78
+ - tagname: cpu_temp
79
+ label: CPU Temp
80
+ scale: '°C'
81
+ color: red
82
+ width: 1
83
+ dp: 1
84
+ - name: Files
85
+ parent: Dropdown
86
+ items:
87
+ - {tagname: __files__, type: files}
88
+ - name: Temperature
89
+ parent: Weather
90
+ items:
91
+ - type: uplot # Do all times in seconds, which uplot uses.
92
+ ms:
93
+ desc: Temperature
94
+ age: 172800
95
+ legend_pos: left
96
+ time_pos: left
97
+ time_res: m
98
+ axes:
99
+ - scale: x
100
+ range: [-604800, 86400] # 86400 172800 1209600
101
+ - scale: 'C'
102
+ range: [0.0, 35.0]
103
+ dp: 1
104
+ series:
105
+ - tagname: temperature
106
+ label: Current Temperature
107
+ scale: 'C'
108
+ color: black
109
+ width: 2
110
+ dp: 1
111
+ - tagname: temperature_01
112
+ label: 1h Temperature
113
+ scale: 'C'
114
+ color: darkgray
115
+ width: 1.5
116
+ dp: 1
117
+ - tagname: temperature_04
118
+ label: 4h Temperature
119
+ scale: 'C'
120
+ color: green
121
+ width: 1
122
+ dp: 1
123
+ - tagname: temperature_12
124
+ label: 12h Temperature
125
+ scale: 'C'
126
+ color: orange
127
+ width: 0.75
128
+ dp: 1
129
+ - tagname: temperature_24
130
+ label: 24h Temperature
131
+ scale: 'C'
132
+ color: red
133
+ width: 0.5
134
+ dp: 1
135
+ - name: Wind Speed
136
+ parent: Weather
137
+ items:
138
+ - type: uplot # Do all times in seconds, which uplot uses.
139
+ ms:
140
+ desc: Wind Speed
141
+ age: 172800
142
+ legend_pos: left
143
+ time_pos: left
144
+ time_res: m
145
+ axes:
146
+ - scale: x
147
+ range: [-604800, 86400] # 86400 172800 1209600
148
+ - scale: 'm/s'
149
+ range: [0.0, 20.0]
150
+ dp: 1
151
+ series:
152
+ - tagname: windSpeed
153
+ label: Current Wind Speed
154
+ scale: 'm/s'
155
+ color: black
156
+ width: 2
157
+ dp: 1
158
+ - tagname: windSpeed_01
159
+ label: 1h Wind Speed
160
+ scale: 'm/s'
161
+ color: darkgray
162
+ width: 1.5
163
+ dp: 1
164
+ - tagname: windSpeed_04
165
+ label: 4h Wind Speed
166
+ scale: 'm/s'
167
+ color: green
168
+ width: 1
169
+ dp: 1
170
+ - tagname: windSpeed_12
171
+ label: 12h Wind Speed
172
+ scale: 'm/s'
173
+ color: orange
174
+ width: 0.75
175
+ dp: 1
176
+ - tagname: windSpeed_24
177
+ label: 24h Wind Speed
178
+ scale: 'm/s'
179
+ color: red
180
+ width: 0.5
181
+ dp: 1
182
+ - name: Wind Direction
183
+ parent: Weather
184
+ items:
185
+ - type: uplot # Do all times in seconds, which uplot uses.
186
+ ms:
187
+ desc: Wind Direction
188
+ age: 172800
189
+ legend_pos: left
190
+ time_pos: left
191
+ time_res: m
192
+ axes:
193
+ - scale: x
194
+ range: [-604800, 86400] # 86400 172800 1209600
195
+ - scale: 'deg'
196
+ range: [0.0, 360.0]
197
+ dp: 1
198
+ series:
199
+ - tagname: windDirection
200
+ label: Current Wind Direction
201
+ scale: 'deg'
202
+ color: black
203
+ width: 2
204
+ dp: 1
205
+ - tagname: windDirection_01
206
+ label: 1h Wind Direction
207
+ scale: 'deg'
208
+ color: darkgray
209
+ width: 1.5
210
+ dp: 1
211
+ - tagname: windDirection_04
212
+ label: 4h Wind Direction
213
+ scale: 'deg'
214
+ color: green
215
+ width: 1
216
+ dp: 1
217
+ - tagname: windDirection_12
218
+ label: 12h Wind Direction
219
+ scale: 'deg'
220
+ color: orange
221
+ width: 0.75
222
+ dp: 1
223
+ - tagname: windDirection_24
224
+ label: 24h Wind Direction
225
+ scale: 'deg'
226
+ color: red
227
+ width: 0.5
228
+ dp: 1
229
+ - name: Rain Accumulation
230
+ parent: Weather
231
+ items:
232
+ - type: uplot # Do all times in seconds, which uplot uses.
233
+ ms:
234
+ desc: Rain Accumulation
235
+ age: 172800
236
+ legend_pos: left
237
+ time_pos: left
238
+ time_res: m
239
+ axes:
240
+ - scale: x
241
+ range: [-604800, 86400] # 86400 172800 1209600
242
+ - scale: 'mm'
243
+ range: [0.0, 10.0]
244
+ dp: 1
245
+ series:
246
+ - tagname: rainAccumulation
247
+ label: Current Rain Accumulation
248
+ scale: 'mm'
249
+ color: black
250
+ width: 2
251
+ dp: 1
252
+ - tagname: rainAccumulation_01
253
+ label: 1h Rain Accumulation
254
+ scale: 'mm'
255
+ color: darkgray
256
+ width: 1.5
257
+ dp: 1
258
+ - tagname: rainAccumulation_04
259
+ label: 4h Rain Accumulation
260
+ scale: 'mm'
261
+ color: green
262
+ width: 1
263
+ dp: 1
264
+ - tagname: rainAccumulation_12
265
+ label: 12h Rain Accumulation
266
+ scale: 'mm'
267
+ color: orange
268
+ width: 0.75
269
+ dp: 1
270
+ - tagname: rainAccumulation_24
271
+ label: 24h Rain Accumulation
272
+ scale: 'mm'
273
+ color: red
274
+ width: 0.5
275
+ dp: 1
276
+ - name: Humidity
277
+ parent: Weather
278
+ items:
279
+ - type: uplot # Do all times in seconds, which uplot uses.
280
+ ms:
281
+ desc: Humidity
282
+ age: 172800
283
+ legend_pos: left
284
+ time_pos: left
285
+ time_res: m
286
+ axes:
287
+ - scale: x
288
+ range: [-604800, 86400] # 86400 172800 1209600
289
+ - scale: '%'
290
+ range: [0.0, 100.0]
291
+ dp: 1
292
+ series:
293
+ - tagname: humidity
294
+ label: Current Humidity
295
+ scale: '%'
296
+ color: black
297
+ width: 2
298
+ dp: 1
299
+ - tagname: humidity_01
300
+ label: 1h Humidity
301
+ scale: '%'
302
+ color: darkgray
303
+ width: 1.5
304
+ dp: 1
305
+ - tagname: humidity_04
306
+ label: 4h Humidity
307
+ scale: '%'
308
+ color: green
309
+ width: 1
310
+ dp: 1
311
+ - tagname: humidity_12
312
+ label: 12h Humidity
313
+ scale: '%'
314
+ color: orange
315
+ width: 0.75
316
+ dp: 1
317
+ - tagname: humidity_24
318
+ label: 24h Humidity
319
+ scale: '%'
320
+ color: red
321
+ width: 0.5
322
+ dp: 1
323
+ - name: Values
324
+ parent: Weather
325
+ items:
326
+ - {tagname: temperature, type: value}
327
+ - {tagname: temperature_01, type: value}
328
+ - {tagname: temperature_04, type: value}
329
+ - {tagname: temperature_12, type: value}
330
+ - {tagname: temperature_24, type: value}
331
+ - {tagname: windSpeed, type: value}
332
+ - {tagname: windSpeed_01, type: value}
333
+ - {tagname: windSpeed_04, type: value}
334
+ - {tagname: windSpeed_12, type: value}
335
+ - {tagname: windSpeed_24, type: value}
336
+ - {tagname: windDirection, type: value}
337
+ - {tagname: windDirection_01, type: value}
338
+ - {tagname: windDirection_04, type: value}
339
+ - {tagname: windDirection_12, type: value}
340
+ - {tagname: windDirection_24, type: value}
341
+ - {tagname: rainAccumulation, type: value}
342
+ - {tagname: rainAccumulation_01, type: value}
343
+ - {tagname: rainAccumulation_04, type: value}
344
+ - {tagname: rainAccumulation_12, type: value}
345
+ - {tagname: rainAccumulation_24, type: value}
346
+ - {tagname: humidity, type: value}
347
+ - {tagname: humidity_01, type: value}
348
+ - {tagname: humidity_04, type: value}
349
+ - {tagname: humidity_12, type: value}
350
+ - {tagname: humidity_24, type: value}
351
+ - name: Logix
352
+ items:
353
+ - {tagname: Ani_Fin_20, type: setpoint}
354
+ - {tagname: Ani_Fout_20, type: value}
355
+ - {tagname: Ani_Iin_20, type: setpoint}
356
+ - {tagname: Ani_Iout_20, type: value}
357
+ - {tagname: InVar, type: setpoint}
358
+ - {tagname: OutVar, type: value}
359
+ - {tagname: Ani_Iin_21_0, type: setpoint}
360
+ - {tagname: Ani_Iout_21_0, type: value}
361
+ - {tagname: Ani_Iin_21_1, type: setpoint}
362
+ - {tagname: Ani_Iout_21_1, type: value}
363
+ - name: Values
364
+ parent: SNMP
365
+ items:
366
+ - {tagname: Router_eth1_bytes_in, type: value}
367
+ - {tagname: Router_eth1_bytes_out, type: value}
368
+ - {tagname: Router_eth2_bytes_in, type: value}
369
+ - {tagname: Router_eth2_bytes_out, type: value}
370
+ - {tagname: Router_eth3_bytes_in, type: value}
371
+ - {tagname: Router_eth3_bytes_out, type: value}
372
+ - {tagname: Router_eth4_bytes_in, type: value}
373
+ - {tagname: Router_eth4_bytes_out, type: value}
374
+ - {tagname: Router_eth5_bytes_in, type: value}
375
+ - {tagname: Router_eth5_bytes_out, type: value}
376
+ - {tagname: Router_eth6_bytes_in, type: value}
377
+ - {tagname: Router_eth6_bytes_out, type: value}
378
+ - {tagname: Router_eth7_bytes_in, type: value}
379
+ - {tagname: Router_eth7_bytes_out, type: value}
380
+ - {tagname: Router_eth8_bytes_in, type: value}
381
+ - {tagname: Router_eth8_bytes_out, type: value}
382
+ - name: Trend
383
+ parent: SNMP
384
+ items:
385
+ - type: uplot # Do all times in seconds, which uplot uses.
386
+ ms:
387
+ desc: Bytes
388
+ age: 172800
389
+ legend_pos: left
390
+ time_pos: left
391
+ time_res: m
392
+ axes:
393
+ - scale: x
394
+ range: [-86400, 0]
395
+ - scale: 'bytes'
396
+ range: [0, 100000]
397
+ dp: 0
398
+ series:
399
+ - tagname: Router_eth1_bytes_in
400
+ label: eth1 in
401
+ scale: 'bytes'
402
+ color: violet
403
+ width: 1
404
+ dp: 0
405
+ - tagname: Router_eth1_bytes_out
406
+ label: eth1 out
407
+ scale: 'bytes'
408
+ color: violet
409
+ width: 0.5
410
+ dp: 0
411
+ - tagname: Router_eth2_bytes_in
412
+ label: eth2 in
413
+ scale: 'bytes'
414
+ color: blue
415
+ width: 1
416
+ dp: 0
417
+ - tagname: Router_eth2_bytes_out
418
+ label: eth2 out
419
+ scale: 'bytes'
420
+ color: blue
421
+ width: 0.5
422
+ dp: 0
423
+ - tagname: Router_eth3_bytes_in
424
+ label: eth3 in
425
+ scale: 'bytes'
426
+ color: green
427
+ width: 1
428
+ dp: 0
429
+ - tagname: Router_eth3_bytes_out
430
+ label: eth3 out
431
+ scale: 'bytes'
432
+ color: green
433
+ width: 0.5
434
+ dp: 0
435
+ - tagname: Router_eth4_bytes_in
436
+ label: eth4 in
437
+ scale: 'bytes'
438
+ color: gray
439
+ width: 1
440
+ dp: 0
441
+ - tagname: Router_eth4_bytes_out
442
+ label: eth4 out
443
+ scale: 'bytes'
444
+ color: gray
445
+ width: 0.5
446
+ dp: 0
447
+ - tagname: Router_eth5_bytes_in
448
+ label: eth5 in
449
+ scale: 'bytes'
450
+ color: goldenrod
451
+ width: 1
452
+ dp: 0
453
+ - tagname: Router_eth5_bytes_out
454
+ label: eth5 out
455
+ scale: 'bytes'
456
+ color: goldenrod
457
+ width: 0.5
458
+ dp: 0
459
+ - tagname: Router_eth6_bytes_in
460
+ label: eth6 in
461
+ scale: 'bytes'
462
+ color: brown
463
+ width: 1
464
+ dp: 0
465
+ - tagname: Router_eth6_bytes_out
466
+ label: eth6 out
467
+ scale: 'bytes'
468
+ color: brown
469
+ width: 0.5
470
+ dp: 0
471
+ - tagname: Router_eth7_bytes_in
472
+ label: eth7 in
473
+ scale: 'bytes'
474
+ color: orange
475
+ width: 1
476
+ dp: 0
477
+ - tagname: Router_eth7_bytes_out
478
+ label: eth7 out
479
+ scale: 'bytes'
480
+ color: orange
481
+ width: 0.5
482
+ dp: 0
483
+ - tagname: Router_eth8_bytes_in
484
+ label: eth8 in
485
+ scale: 'bytes'
486
+ color: aqua
487
+ width: 1
488
+ dp: 0
489
+ - tagname: Router_eth8_bytes_out
490
+ label: eth8 out
491
+ scale: 'bytes'
492
+ color: aqua
493
+ width: 0.5
494
+ dp: 0
495
+ - name: Ping Values
496
+ parent: Ping
497
+ items:
498
+ - {desc: Default tags, type: h1}
499
+ - {tagname: localhost_ping, type: value}
500
+ - {tagname: google_ping, type: value}
501
+ - {tagname: electronet_ping, type: value}
502
+ - name: Ping Trend
503
+ parent: Ping
504
+ items:
505
+ - type: uplot # Do all times in seconds, which uplot uses.
506
+ ms:
507
+ desc: Ping Trend
508
+ age: 172800
509
+ legend_pos: left
510
+ time_pos: left
511
+ time_res: m
512
+ axes:
513
+ - scale: x
514
+ range: [-86400, 0] # 86400 172800 1209600
515
+ - scale: mS
516
+ range: [0.0, 1.0]
517
+ dp: 1
518
+ series:
519
+ - tagname: localhost_ping
520
+ label: localhost
521
+ scale: mS
522
+ color: black
523
+ dp: 1
524
+ - tagname: electronet_ping
525
+ label: electronet
526
+ scale: mS
527
+ color: blue
528
+ dp: 1
529
+ - tagname: google_ping
530
+ label: google
531
+ scale: mS
532
+ color: red
533
+ dp: 1
@@ -0,0 +1,120 @@
1
+ """Network monitoring / scanning."""
2
+ import asyncio
3
+ import logging
4
+ import socket
5
+ import struct
6
+ import time
7
+ from pymscada.bus_client import BusClient
8
+ from pymscada.periodic import Periodic
9
+ from pymscada.iodrivers.ping_map import PingMaps
10
+
11
+
12
+ ICMP_REQUEST = 8
13
+ ICMP_REPLY = 0
14
+ RATE = 60
15
+
16
+
17
+ def checksum_chat(data):
18
+ """Calculate ICMP Checksum."""
19
+ if len(data) & 1: # If the packet length is odd
20
+ data += b'\0'
21
+ res = 0
22
+ # Process two bytes at a time
23
+ for i in range(0, len(data), 2):
24
+ res += (data[i] << 8) + data[i + 1]
25
+ res = (res >> 16) + (res & 0xffff)
26
+ res += res >> 16
27
+ return (~res) & 0xffff # Return ones' complement of the result
28
+
29
+
30
+ class PingClientConnector:
31
+ """Ping a list of addresses."""
32
+
33
+ def __init__(self, mapping: PingMaps):
34
+ """Accept list of addresses, ip or name."""
35
+ self.mapping = mapping
36
+ self.dns = {}
37
+ self.addr_info = {}
38
+ self.socket = None
39
+ self.ping_id = 0
40
+ self.ping_dict = {}
41
+
42
+ async def poll(self):
43
+ """Do pings."""
44
+ self.reply_dict = {}
45
+ if self.socket is None:
46
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_RAW,
47
+ socket.IPPROTO_ICMP)
48
+ asyncio.get_event_loop().add_reader(self.socket,
49
+ self.read_response)
50
+ for ping_id, address in list(self.ping_dict.items()):
51
+ logging.info(f'failed {self.dns[address]} {ping_id}')
52
+ self.mapping.polled_data(self.dns[address], float('NAN'))
53
+ del self.ping_dict[ping_id]
54
+ self.send_ping()
55
+
56
+ def send_ping(self):
57
+ """Build and send ping messages."""
58
+ for address in self.dns.keys():
59
+ self.ping_id = (self.ping_id + 1) & 0xffff
60
+ self.ping_dict[self.ping_id] = address
61
+ logging.info(f'ping {address} id {self.ping_id}')
62
+ header = struct.pack("!BbHHh", ICMP_REQUEST, 0, 0, self.ping_id, 1)
63
+ data = struct.pack("!d", time.perf_counter()) + (60 * b'\0')
64
+ checksum = checksum_chat(header + data)
65
+ packet = struct.pack("!BbHHh", ICMP_REQUEST, 0, checksum,
66
+ self.ping_id, 1) + data
67
+ self.socket.sendto(packet, (address, 0))
68
+
69
+ def read_response(self):
70
+ """Match ping response."""
71
+ data, address = self.socket.recvfrom(1024)
72
+ msgtype, _, _, ping_id, _ = struct.unpack('!BBHHH', data[20:28])
73
+ if msgtype != ICMP_REPLY:
74
+ return
75
+ if ping_id in self.ping_dict:
76
+ latency = 1000 * (time.perf_counter() -
77
+ struct.unpack('!d', data[28:36])[0])
78
+ name = self.dns[address[0]]
79
+ logging.info(f'success {name} {ping_id} {latency}ms')
80
+ self.mapping.polled_data(name, latency)
81
+ del self.ping_dict[ping_id]
82
+
83
+ async def start(self):
84
+ """Start pinging."""
85
+ loop = asyncio.get_event_loop()
86
+ for address in self.mapping.var_map.keys():
87
+ info = await loop.getaddrinfo(address, None, family=socket.AF_INET,
88
+ type=socket.SOCK_STREAM)
89
+ ip = info[0][4][0]
90
+ self.dns[ip] = address
91
+ self.periodic = Periodic(self.poll, RATE)
92
+ await self.periodic.start()
93
+
94
+
95
+ class PingClient:
96
+ """Ping client."""
97
+
98
+ def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
99
+ tags: dict = {}) -> None:
100
+ """
101
+ Connect to bus on bus_ip:bus_port, ping a list.
102
+
103
+ Event loop must be running.
104
+ """
105
+ self.busclient = None
106
+ if bus_ip is not None:
107
+ self.busclient = BusClient(bus_ip, bus_port)
108
+ self.mapping = PingMaps(tags)
109
+ self.pinger = PingClientConnector(mapping=self.mapping)
110
+
111
+ async def _poll(self):
112
+ """For testing."""
113
+ for connection in self.connections:
114
+ await connection.poll()
115
+
116
+ async def start(self):
117
+ """Start bus connection and PLC polling."""
118
+ if self.busclient is not None:
119
+ await self.busclient.start()
120
+ await self.pinger.start()
@@ -0,0 +1,43 @@
1
+ """Map between snmp MIB and Tag."""
2
+ from time import time
3
+ from pymscada.tag import Tag
4
+
5
+
6
+ class PingMap:
7
+ """Do value updates for each tag."""
8
+
9
+ def __init__(self, tagname: str, addr: str):
10
+ """Initialise MIB map and Tag."""
11
+ self.last_value = None
12
+ self.tag = Tag(tagname, float)
13
+ self.addr = addr
14
+ self.map_bus = id(self)
15
+
16
+ def set_tag_value(self, value, time_us):
17
+ """Pass update from IO driver to tag value."""
18
+ if self.last_value is None:
19
+ self.last_value = value
20
+ return
21
+ if self.last_value != value:
22
+ self.tag.value = value, time_us, self.map_bus
23
+
24
+
25
+ class PingMaps:
26
+ """Link tags with protocol connector."""
27
+
28
+ def __init__(self, tags: dict):
29
+ """Collect maps based on a tag dictionary."""
30
+ # use the tagname to access the map.
31
+ self.tag_map: dict[str, PingMap] = {}
32
+ # use the plc_name then variable name to access a list of maps.
33
+ self.var_map: dict[str, PingMap] = {}
34
+ for tagname, v in tags.items():
35
+ addr = v['addr']
36
+ map = PingMap(tagname, addr)
37
+ self.var_map[addr] = map
38
+ self.tag_map[tagname] = map
39
+
40
+ def polled_data(self, address, latency):
41
+ """Pass updates read from the PLC to the tags."""
42
+ time_us = int(time() * 1e6)
43
+ self.var_map[address].set_tag_value(latency, time_us)
@@ -3,6 +3,7 @@ import argparse
3
3
  import asyncio
4
4
  from importlib.metadata import version
5
5
  import logging
6
+ import sys
6
7
  from pymscada.bus_server import BusServer
7
8
  from pymscada.checkout import checkout
8
9
  from pymscada.config import Config
@@ -12,11 +13,15 @@ from pymscada.history import History
12
13
  from pymscada.iodrivers.logix_client import LogixClient
13
14
  from pymscada.iodrivers.modbus_client import ModbusClient
14
15
  from pymscada.iodrivers.modbus_server import ModbusServer
16
+ from pymscada.iodrivers.ping_client import PingClient
15
17
  from pymscada.iodrivers.snmp_client import SnmpClient
16
18
  from pymscada.www_server import WwwServer
17
19
  from pymscada.validate import validate
18
20
 
19
21
 
22
+ MODULES = {}
23
+
24
+
20
25
  async def bus(options):
21
26
  """Return bus module."""
22
27
  config = Config(options.config)
@@ -50,7 +55,7 @@ async def console(_options):
50
55
 
51
56
  async def _checkout(options):
52
57
  """Checkout files in current working directory."""
53
- checkout(overwrite=options.overwrite)
58
+ checkout(overwrite=options.overwrite, diff=options.diff)
54
59
  return
55
60
 
56
61
 
@@ -82,6 +87,14 @@ async def logixclient(options):
82
87
  return LogixClient(**config)
83
88
 
84
89
 
90
+ async def ping(options):
91
+ """Return logixclient module."""
92
+ if sys.platform.startswith("win"):
93
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
94
+ config = Config(options.config)
95
+ return PingClient(**config)
96
+
97
+
85
98
  async def snmpclient(options):
86
99
  """Return snmpclient module."""
87
100
  config = Config(options.config)
@@ -90,16 +103,16 @@ async def snmpclient(options):
90
103
 
91
104
  def add_subparser_defaults(
92
105
  parser: argparse._SubParsersAction,
93
- name: str, call, help: str):
106
+ name: str, call, help: str, epilog: str):
94
107
  """Add arguments common to all subparsers."""
95
- s = parser.add_parser(name, help=help)
108
+ s = parser.add_parser(name, help=help, epilog=epilog)
96
109
  s.set_defaults(get_module=call, module=name)
97
110
  s.add_argument('--config', metavar='file', default=None,
98
- help=f'Config file, default is "{name}.yaml".')
111
+ help=f"Config file, default is '{name}.yaml'")
99
112
  s.add_argument('--tags', metavar='file', default=None,
100
- help='Tags file, default is "tags.yaml".')
113
+ help="Tags file, default is 'tags.yaml'")
101
114
  s.add_argument('--verbose', action='store_true',
102
- help="Set level to logging.INFO.")
115
+ help="Set level to logging.INFO")
103
116
  return s
104
117
 
105
118
 
@@ -111,24 +124,36 @@ def args(_version: str):
111
124
  epilog=f'Python Mobile SCADA {_version}'
112
125
  )
113
126
  subparsers = parser.add_subparsers(title='module')
114
- for module, func, help in [
115
- ['bus', bus, 'run the message bus'],
116
- ['wwwserver', wwwserver, 'serve web pages'],
117
- ['history', history, 'collect and serve history'],
118
- ['files', files, 'receive and send files'],
119
- ['console', console, 'interactive bus console'],
120
- ['checkout', _checkout, 'create example config files'],
121
- ['validate', _validate, 'validate config files'],
122
- ['modbusserver', modbusserver, 'receive modbus messages'],
123
- ['modbusclient', modbusclient, 'poll/write to modbus devices'],
124
- ['logixclient', logixclient, 'poll/write to logix devices'],
125
- ['snmpclient', snmpclient, 'poll snmp oids'],
127
+ for module, func, help, epilog in [
128
+ ['bus', bus, 'run the message bus', None],
129
+ ['wwwserver', wwwserver, 'serve web pages', None],
130
+ ['history', history, 'collect and serve history', None],
131
+ ['files', files, 'receive and send files', None],
132
+ ['console', console, 'interactive bus console', None],
133
+ ['checkout', _checkout, 'create example config files', """
134
+ To add to systemd `f="pymscada-bus" && cp config/$f.service
135
+ /lib/systemd/system && systemctl enable $f && systemctl start
136
+ $f`"""],
137
+ ['validate', _validate, 'validate config files', None],
138
+ ['modbusserver', modbusserver, 'receive modbus messages', """
139
+ Needs `setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/python3.nn` to
140
+ bind to port 502"""],
141
+ ['modbusclient', modbusclient, 'poll/write to modbus devices', None],
142
+ ['ping', ping, 'ping a list of addresses, return time', """
143
+ Needs `setcap CAP_NET_RAW+ep /usr/bin/python3.nn` to open SOCK_RAW
144
+ """],
145
+ ['logixclient', logixclient, 'poll/write to logix devices', None],
146
+ ['snmpclient', snmpclient, 'poll snmp oids', None],
126
147
  ]:
127
- modparser = add_subparser_defaults(subparsers, module, func, help)
148
+ modparser = add_subparser_defaults(subparsers, module, func, help,
149
+ epilog)
128
150
  if module == 'checkout':
129
151
  modparser.add_argument(
130
152
  '--overwrite', action='store_true', default=False,
131
153
  help='checkout may overwrite files, CARE!')
154
+ modparser.add_argument(
155
+ '--diff', action='store_true', default=False,
156
+ help='compare default with existing')
132
157
  elif module == 'validate':
133
158
  modparser.add_argument(
134
159
  '--path', metavar='file',
@@ -1,87 +0,0 @@
1
- bus_ip: 127.0.0.1
2
- bus_port: 1324
3
- ip: 0.0.0.0
4
- port: 8324
5
- get_path:
6
- paths:
7
- - history
8
- - config
9
- - pdf
10
- pages:
11
- - name: Default Main
12
- parent:
13
- items:
14
- - {desc: Default tags, type: h1}
15
- - {tagname: IntSet, type: setpoint}
16
- - {tagname: IntVal, type: value}
17
- - {tagname: FloatSet, type: setpoint}
18
- - {tagname: FloatVal, type: value}
19
- - {tagname: MultiSet, type: setpoint}
20
- - {tagname: MultiVal, type: value}
21
- - {tagname: StringSet, type: setpoint}
22
- - {tagname: StringVal, type: value}
23
- - {tagname: TimeSet, type: setpoint}
24
- - {tagname: TimeVal, type: value}
25
- - {tagname: DateSet, type: setpoint}
26
- - {tagname: DateVal, type: value}
27
- - {tagname: DateTimeSet, type: setpoint}
28
- - {tagname: DateTimeVal, type: value}
29
- - type: selectdict
30
- tagname: MultiSelect
31
- opts:
32
- type: multi
33
- multi:
34
- - Zero
35
- - One
36
- - Two
37
- - Three
38
- - Four
39
- - Five
40
- - {tagname: FloatSelect, type: selectdict, opts: {type: float, dp: 2}}
41
- - name: Trends
42
- parent: Dropdown
43
- items:
44
- - type: uplot # Do all times in seconds, which uplot uses.
45
- ms:
46
- desc: Sample Trend
47
- age: 172800
48
- legend_pos: left
49
- time_pos: left
50
- time_res: m
51
- axes:
52
- - scale: x
53
- range: [-86400, 0] # 86400 172800 1209600
54
- - scale: '°C'
55
- range: [0.0, 100.0]
56
- dp: 1
57
- - scale: '%'
58
- range: [0.0, 100.0]
59
- dp: 1
60
- # side: 1
61
- # bands: # TODO
62
- # - series: [I_Transpower_Limit_Hi, I_Transpower_Limit_Lo]
63
- # fill: [red, 0.2]
64
- # dir: -1
65
- series:
66
- - tagname: cpu_load
67
- label: CPU Load
68
- scale: '%'
69
- color: black
70
- width: 1.5
71
- dp: 1
72
- - tagname: disk_use
73
- label: Disk Use
74
- scale: '%'
75
- color: blue
76
- width: 1
77
- dp: 1
78
- - tagname: cpu_temp
79
- label: CPU Temp
80
- scale: '°C'
81
- color: red
82
- width: 1
83
- dp: 1
84
- - name: Files
85
- parent: Dropdown
86
- items:
87
- - {tagname: __files__, type: files}
File without changes
File without changes
File without changes
File without changes
File without changes