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.
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/PKG-INFO +1 -1
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/pyproject.toml +1 -1
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/checkout.py +38 -5
- pymscada-0.1.0a4/src/pymscada/demo/ping.yaml +12 -0
- pymscada-0.1.0a4/src/pymscada/demo/pymscada-io-ping.service +15 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/tags.yaml +10 -1
- pymscada-0.1.0a4/src/pymscada/demo/wwwserver.yaml +533 -0
- pymscada-0.1.0a4/src/pymscada/iodrivers/ping_client.py +120 -0
- pymscada-0.1.0a4/src/pymscada/iodrivers/ping_map.py +43 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/main.py +44 -19
- pymscada-0.1.0a3/src/pymscada/demo/wwwserver.yaml +0 -87
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/LICENSE +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/README.md +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/__init__.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/__main__.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/bus_client.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/bus_server.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/config.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/console.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/files.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-history.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/files.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/history.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/modbus_client.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/modbus_map.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/snmp_client.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/misc.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/periodic.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/samplers.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/tag.py +0 -0
- {pymscada-0.1.0a3/src/pymscada/iodrivers → pymscada-0.1.0a4/src/pymscada/tools}/snmp_client2.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/validate.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/src/pymscada/www_server.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/__init__.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/bus_echo.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/iodrivers/test_logix.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/iodrivers/test_modbus.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/busserver.yaml +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_0.dat +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_10_2.dat +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_15.dat +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_26.dat +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_assets/hist_tag_0_50.dat +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_bus_server.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_config.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_misc.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_periodic.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_samplers.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_tag.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_tag_history.py +0 -0
- {pymscada-0.1.0a3 → pymscada-0.1.0a4}/tests/test_validate.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
100
|
+
if diff:
|
|
101
|
+
compare_config()
|
|
102
|
+
else:
|
|
103
|
+
make_history()
|
|
104
|
+
make_pdf(diff)
|
|
105
|
+
make_config(overwrite, diff)
|
|
@@ -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
|
|
111
|
+
help=f"Config file, default is '{name}.yaml'")
|
|
99
112
|
s.add_argument('--tags', metavar='file', default=None,
|
|
100
|
-
help=
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
['
|
|
125
|
-
['
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymscada-0.1.0a3/src/pymscada/iodrivers → pymscada-0.1.0a4/src/pymscada/tools}/snmp_client2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|