pymscada 0.2.0rc8__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pymscada might be problematic. Click here for more details.
- pymscada/__init__.py +6 -2
- pymscada/alarms.py +6 -2
- pymscada/callout.py +128 -72
- pymscada/config.py +20 -1
- pymscada/demo/callout.yaml +17 -7
- pymscada/demo/openweather.yaml +1 -1
- pymscada/demo/piapi.yaml +15 -0
- pymscada/demo/pymscada-callout.service +16 -0
- pymscada/demo/pymscada-io-piapi.service +15 -0
- pymscada/demo/pymscada-io-sms.service +18 -0
- pymscada/demo/sms.yaml +11 -0
- pymscada/demo/tags.yaml +3 -0
- pymscada/demo/witsapi.yaml +2 -2
- pymscada/demo/wwwserver.yaml +15 -0
- pymscada/history.py +3 -5
- pymscada/iodrivers/piapi.py +133 -0
- pymscada/iodrivers/sms.py +212 -0
- pymscada/iodrivers/witsapi.py +26 -35
- pymscada/module_config.py +24 -18
- pymscada/opnotes.py +4 -2
- pymscada/tools/get_history.py +147 -0
- pymscada/www_server.py +1 -1
- {pymscada-0.2.0rc8.dist-info → pymscada-0.2.2.dist-info}/METADATA +2 -2
- {pymscada-0.2.0rc8.dist-info → pymscada-0.2.2.dist-info}/RECORD +28 -21
- pymscada/validate.py +0 -451
- {pymscada-0.2.0rc8.dist-info → pymscada-0.2.2.dist-info}/WHEEL +0 -0
- {pymscada-0.2.0rc8.dist-info → pymscada-0.2.2.dist-info}/entry_points.txt +0 -0
- {pymscada-0.2.0rc8.dist-info → pymscada-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {pymscada-0.2.0rc8.dist-info → pymscada-0.2.2.dist-info}/top_level.txt +0 -0
pymscada/__init__.py
CHANGED
|
@@ -6,10 +6,12 @@ from pymscada.iodrivers.accuweather import AccuWeatherClient
|
|
|
6
6
|
from pymscada.iodrivers.logix_client import LogixClient
|
|
7
7
|
from pymscada.iodrivers.modbus_client import ModbusClient
|
|
8
8
|
from pymscada.iodrivers.modbus_server import ModbusServer
|
|
9
|
+
from pymscada.iodrivers.piapi import PIWebAPIClient
|
|
10
|
+
from pymscada.iodrivers.sms import SMS
|
|
11
|
+
from pymscada.iodrivers.witsapi import WitsAPIClient
|
|
9
12
|
from pymscada.misc import find_nodes, ramp
|
|
10
13
|
from pymscada.periodic import Periodic
|
|
11
14
|
from pymscada.tag import Tag
|
|
12
|
-
from pymscada.validate import validate
|
|
13
15
|
|
|
14
16
|
__all__ = [
|
|
15
17
|
'BusClient',
|
|
@@ -19,8 +21,10 @@ __all__ = [
|
|
|
19
21
|
'LogixClient',
|
|
20
22
|
'ModbusClient',
|
|
21
23
|
'ModbusServer',
|
|
24
|
+
'PIWebAPIClient',
|
|
25
|
+
'SMS',
|
|
26
|
+
'WitsAPIClient',
|
|
22
27
|
'find_nodes', 'ramp',
|
|
23
28
|
'Periodic',
|
|
24
29
|
'Tag',
|
|
25
|
-
'validate',
|
|
26
30
|
]
|
pymscada/alarms.py
CHANGED
|
@@ -17,6 +17,7 @@ KIND = {
|
|
|
17
17
|
ACT: 'ACT',
|
|
18
18
|
INF: 'INF'
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
NORMAL = 0
|
|
21
22
|
ALARM = 1
|
|
22
23
|
|
|
@@ -105,7 +106,8 @@ class Alarm():
|
|
|
105
106
|
Generates the ALM and RTN messages for Alarms to publish via rta_tag.
|
|
106
107
|
"""
|
|
107
108
|
|
|
108
|
-
def __init__(self, tagname: str, tag: dict, alarm: str, group: str,
|
|
109
|
+
def __init__(self, tagname: str, tag: dict, alarm: str, group: str,
|
|
110
|
+
rta_cb, alarms) -> None:
|
|
109
111
|
"""Initialize alarm with tag and condition(s)."""
|
|
110
112
|
self.alarm_id = f'{tagname} {alarm}'
|
|
111
113
|
self.tag = Tag(tagname, tag['type'])
|
|
@@ -229,7 +231,7 @@ class Alarms:
|
|
|
229
231
|
self.alarms.append(new_alarm)
|
|
230
232
|
self.busclient = BusClient(bus_ip, bus_port, module='Alarms')
|
|
231
233
|
self.rta = Tag(rta_tag, dict)
|
|
232
|
-
self.rta.value = {}
|
|
234
|
+
self.rta.value = {'__rta_id__': 0}
|
|
233
235
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
234
236
|
self._init_db(db, table)
|
|
235
237
|
self.periodic = Periodic(self.periodic_cb, 1.0)
|
|
@@ -283,6 +285,7 @@ class Alarms:
|
|
|
283
285
|
request)
|
|
284
286
|
res = self.cursor.fetchone()
|
|
285
287
|
self.rta.value = {
|
|
288
|
+
'__rta_id__': 0,
|
|
286
289
|
'id': res[0],
|
|
287
290
|
'date_ms': res[1],
|
|
288
291
|
'alarm_string': res[2],
|
|
@@ -303,6 +306,7 @@ class Alarms:
|
|
|
303
306
|
res = self.cursor.fetchone()
|
|
304
307
|
if res:
|
|
305
308
|
self.rta.value = {
|
|
309
|
+
'__rta_id__': 0,
|
|
306
310
|
'id': res[0],
|
|
307
311
|
'date_ms': res[1],
|
|
308
312
|
'alarm_string': res[2],
|
pymscada/callout.py
CHANGED
|
@@ -25,41 +25,52 @@ Operation:
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
ALM = 0
|
|
28
|
+
|
|
28
29
|
IDLE = 0
|
|
29
30
|
NEW_ALM = 1
|
|
30
31
|
CALLOUT = 2
|
|
31
32
|
|
|
33
|
+
SENT = 0
|
|
34
|
+
REMIND = 1
|
|
35
|
+
|
|
32
36
|
|
|
33
|
-
def normalise_callees(callees: list | None):
|
|
34
|
-
"""Normalise callees to include delay_ms and remove
|
|
37
|
+
def normalise_callees(callees: list | None, escalation: dict):
|
|
38
|
+
"""Normalise callees to include delay_ms and remove role."""
|
|
35
39
|
if callees is None:
|
|
36
40
|
return []
|
|
37
41
|
for callee in callees:
|
|
38
42
|
if 'sms' not in callee:
|
|
39
43
|
raise ValueError(f'Callee {callee["name"]} has no sms number')
|
|
40
|
-
if '
|
|
41
|
-
callee['
|
|
42
|
-
|
|
44
|
+
if 'role' in callee:
|
|
45
|
+
if callee['role'] not in escalation:
|
|
46
|
+
raise ValueError(f'Invalid role: {callee["role"]}. Must be'
|
|
47
|
+
f' one of: {list(escalation.keys())}')
|
|
48
|
+
callee['delay_ms'] = escalation[callee['role']]
|
|
43
49
|
else:
|
|
44
50
|
callee['delay_ms'] = 0
|
|
51
|
+
callee['role'] = ''
|
|
45
52
|
if 'group' not in callee:
|
|
46
|
-
callee['group'] =
|
|
53
|
+
callee['group'] = ''
|
|
47
54
|
callees.sort(key=lambda x: x['delay_ms'])
|
|
48
55
|
return callees
|
|
49
56
|
|
|
50
57
|
|
|
51
|
-
def
|
|
52
|
-
"""Check if alarm_group
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
def alarm_in_group(alarm, callee_group, groups):
|
|
59
|
+
"""Check if alarm_group matches callee_group."""
|
|
60
|
+
if callee_group == '':
|
|
61
|
+
return True
|
|
62
|
+
if callee_group not in groups:
|
|
63
|
+
return False
|
|
64
|
+
group = groups[callee_group]
|
|
65
|
+
if 'tagnames' in group:
|
|
66
|
+
for tagname in group['tagnames']:
|
|
67
|
+
if alarm['alarm_string'].startswith(tagname):
|
|
68
|
+
return True
|
|
69
|
+
if 'groups' in group:
|
|
70
|
+
for group in group['groups']:
|
|
71
|
+
if group in alarm['group']:
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
63
74
|
|
|
64
75
|
|
|
65
76
|
class Callout:
|
|
@@ -71,10 +82,13 @@ class Callout:
|
|
|
71
82
|
bus_port: int | None = 1324,
|
|
72
83
|
rta_tag: str = '__callout__',
|
|
73
84
|
alarms_tag: str | None = None,
|
|
85
|
+
sms_send_tag: str | None = None,
|
|
86
|
+
sms_recv_tag: str | None = None,
|
|
74
87
|
ack_tag: str | None = None,
|
|
75
88
|
status_tag: str | None = None,
|
|
76
89
|
callees: list | None = None,
|
|
77
|
-
groups:
|
|
90
|
+
groups: dict = {},
|
|
91
|
+
escalation: list | None = None
|
|
78
92
|
) -> None:
|
|
79
93
|
"""
|
|
80
94
|
Connect to bus_ip:bus_port, monitor alarms and manage callouts.
|
|
@@ -99,97 +113,139 @@ class Callout:
|
|
|
99
113
|
raise ValueError('bus_port must be between 1024 and 65535')
|
|
100
114
|
if not isinstance(rta_tag, str) or not rta_tag:
|
|
101
115
|
raise ValueError('rta_tag must be a non-empty string')
|
|
116
|
+
if alarms_tag is None:
|
|
117
|
+
raise ValueError('alarms_tag must be defined')
|
|
118
|
+
if sms_send_tag is None:
|
|
119
|
+
raise ValueError('sms_send_tag must be defined')
|
|
120
|
+
if sms_recv_tag is None:
|
|
121
|
+
raise ValueError('sms_recv_tag must be defined')
|
|
102
122
|
|
|
103
|
-
|
|
123
|
+
self.escalation = [list(d.keys())[0] for d in escalation]
|
|
124
|
+
self.delay_ms = {list(d.keys())[0]: list(d.values())[0]
|
|
125
|
+
for d in escalation}
|
|
126
|
+
logging.warning(f'Callout {bus_ip} {bus_port} {rta_tag} '
|
|
127
|
+
f'{sms_send_tag} {sms_recv_tag}')
|
|
104
128
|
self.alarms = []
|
|
105
|
-
self.callees = normalise_callees(callees)
|
|
106
|
-
self.groups =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
self.
|
|
129
|
+
self.callees = normalise_callees(callees, self.delay_ms)
|
|
130
|
+
self.groups = groups
|
|
131
|
+
self.alarms_tag = Tag(alarms_tag, dict)
|
|
132
|
+
self.alarms_tag.add_callback(self.alarms_cb)
|
|
133
|
+
self.sms_recv_tag = Tag(sms_recv_tag, dict)
|
|
134
|
+
self.sms_recv_tag.add_callback(self.sms_recv_cb)
|
|
135
|
+
self.sms_send_tag = Tag(sms_send_tag, dict)
|
|
136
|
+
if ack_tag is not None:
|
|
137
|
+
self.ack_tag = Tag(ack_tag, int)
|
|
138
|
+
self.ack_tag.add_callback(self.ack_cb)
|
|
110
139
|
if status_tag is not None:
|
|
111
140
|
self.status = Tag(status_tag, int)
|
|
112
|
-
self.status.value = IDLE
|
|
113
141
|
self.busclient = BusClient(bus_ip, bus_port, module='Callout')
|
|
114
|
-
self.busclient.add_callback_rta(alarms_tag, self.alarms_cb)
|
|
115
|
-
self.busclient.add_callback_rta(ack_tag, self.ack_cb)
|
|
116
142
|
self.rta = Tag(rta_tag, dict)
|
|
117
|
-
self.rta.value = {
|
|
143
|
+
self.rta.value = {'__rta_id__': 0,
|
|
144
|
+
'callees': self.callees,
|
|
145
|
+
'groups': self.groups,
|
|
146
|
+
'escalation': self.escalation}
|
|
118
147
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
119
148
|
self.periodic = Periodic(self.periodic_cb, 1.0)
|
|
120
149
|
|
|
121
|
-
def alarms_cb(self,
|
|
150
|
+
def alarms_cb(self, alm_tag):
|
|
122
151
|
"""Handle alarm messages from alarms.py."""
|
|
123
|
-
|
|
152
|
+
alarm = alm_tag.value
|
|
153
|
+
if 'kind' not in alarm or alarm['kind'] != ALM:
|
|
124
154
|
return
|
|
125
155
|
alarm = {
|
|
126
|
-
'date_ms':
|
|
127
|
-
'alarm_string':
|
|
128
|
-
'desc':
|
|
129
|
-
'group':
|
|
156
|
+
'date_ms': alarm['date_ms'],
|
|
157
|
+
'alarm_string': alarm['alarm_string'],
|
|
158
|
+
'desc': alarm['desc'],
|
|
159
|
+
'group': alarm['group'],
|
|
130
160
|
'sent': {}
|
|
131
161
|
}
|
|
132
162
|
self.alarms.append(alarm)
|
|
133
163
|
logging.info(f'Added alarm to list: {alarm}')
|
|
164
|
+
if self.status is not None and self.status.value == IDLE:
|
|
165
|
+
self.status.value = NEW_ALM
|
|
134
166
|
|
|
135
|
-
def ack_cb(self,
|
|
167
|
+
def ack_cb(self, ack_tag):
|
|
136
168
|
"""Handle ACK requests for alarm acknowledgment."""
|
|
137
|
-
if
|
|
169
|
+
if ack_tag.value == 1:
|
|
138
170
|
self.alarms = []
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
remaining_alarms = []
|
|
154
|
-
for alarm in self.alarms:
|
|
155
|
-
alarm_group = alarm_in_callee_group(alarm['group'], group)
|
|
156
|
-
if alarm_group is None:
|
|
157
|
-
remaining_alarms.append(alarm)
|
|
158
|
-
self.alarms = remaining_alarms
|
|
171
|
+
if self.status is not None:
|
|
172
|
+
self.status.value = IDLE
|
|
173
|
+
logging.info('ACK: all alarms cleared')
|
|
174
|
+
|
|
175
|
+
def sms_recv_cb(self, sms_recv_tag: dict):
|
|
176
|
+
"""Handle SMS messages from the modem."""
|
|
177
|
+
logging.info(f'sms_recv_cb {sms_recv_tag.value}')
|
|
178
|
+
_number = sms_recv_tag.value['number']
|
|
179
|
+
message = sms_recv_tag.value['message'][:2].upper()
|
|
180
|
+
if message in ['OK', 'AC', 'TH']:
|
|
181
|
+
self.alarms = []
|
|
182
|
+
if self.status is not None:
|
|
183
|
+
self.status.value = IDLE
|
|
184
|
+
logging.info('ACK: all alarms cleared')
|
|
159
185
|
|
|
160
186
|
def rta_cb(self, request):
|
|
161
187
|
"""Handle RTA requests for callout configuration."""
|
|
188
|
+
logging.info(f'rta_cb {request}')
|
|
162
189
|
if 'action' not in request:
|
|
163
190
|
logging.warning(f'rta_cb malformed {request}')
|
|
191
|
+
return
|
|
192
|
+
if request['action'] == 'GET CONFIG':
|
|
193
|
+
self.rta.value = {'__rta_id__': request['__rta_id__'],
|
|
194
|
+
'callees': self.callees,
|
|
195
|
+
'groups': self.groups,
|
|
196
|
+
'escalation': self.escalation}
|
|
164
197
|
elif request['action'] == 'MODIFY':
|
|
198
|
+
send_update = False
|
|
165
199
|
for callee in self.callees:
|
|
166
200
|
if callee['name'] == request['name']:
|
|
167
|
-
if '
|
|
168
|
-
callee['
|
|
201
|
+
if 'role' in request:
|
|
202
|
+
callee['role'] = request['role']
|
|
203
|
+
callee['delay_ms'] = self.delay_ms.get(
|
|
204
|
+
request['role'], 30000)
|
|
169
205
|
if 'group' in request:
|
|
170
206
|
callee['group'] = request['group']
|
|
171
207
|
logging.info(f'Modified callee with {request}')
|
|
208
|
+
send_update = True
|
|
209
|
+
if send_update:
|
|
210
|
+
self.rta.value = {'__rta_id__': 0,
|
|
211
|
+
'callees': self.callees,
|
|
212
|
+
'groups': self.groups,
|
|
213
|
+
'escalation': self.escalation}
|
|
172
214
|
|
|
173
215
|
def check_callouts(self):
|
|
174
|
-
"""Check alarms and send callouts.
|
|
216
|
+
"""Check alarms and send callouts."""
|
|
175
217
|
time_ms = int(time.time() * 1000)
|
|
176
218
|
for callee in self.callees:
|
|
177
|
-
|
|
219
|
+
if not callee['role']:
|
|
220
|
+
continue
|
|
178
221
|
count = 0
|
|
179
|
-
|
|
222
|
+
message = ''
|
|
180
223
|
notify_ms = time_ms - callee['delay_ms']
|
|
224
|
+
remind_ms = notify_ms - 60000
|
|
181
225
|
for alarm in self.alarms:
|
|
182
|
-
if alarm['
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
if not alarm_in_group(alarm, callee['group'], self.groups):
|
|
227
|
+
continue
|
|
228
|
+
if notify_ms < alarm['date_ms']:
|
|
229
|
+
continue
|
|
230
|
+
if callee['name'] not in alarm['sent']:
|
|
231
|
+
message += f"{alarm['desc']}\n"
|
|
232
|
+
alarm['sent'][callee['name']] = SENT
|
|
233
|
+
count += 1
|
|
234
|
+
continue
|
|
235
|
+
if remind_ms < alarm['date_ms']:
|
|
236
|
+
continue
|
|
237
|
+
if alarm['sent'][callee['name']] != REMIND:
|
|
238
|
+
message += f"REMIND {alarm['desc']}\n"
|
|
239
|
+
alarm['sent'][callee['name']] = REMIND
|
|
240
|
+
count += 1
|
|
188
241
|
if count > 0:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
242
|
+
if len(message) > 200:
|
|
243
|
+
message = message[:200] + '\n...'
|
|
244
|
+
send_message = f"{count} Alarms\n{message}"
|
|
245
|
+
self.sms_send_tag.value = {
|
|
246
|
+
'number': callee['sms'],
|
|
247
|
+
'message': send_message
|
|
248
|
+
}
|
|
193
249
|
if self.status is not None:
|
|
194
250
|
self.status.value = CALLOUT
|
|
195
251
|
|
pymscada/config.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import importlib.resources
|
|
3
3
|
from importlib.abc import Traversable
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from yaml import safe_load_all, YAMLError
|
|
7
9
|
from pymscada import demo, pdf
|
|
@@ -38,6 +40,21 @@ def get_pdf_files() -> list[Traversable]:
|
|
|
38
40
|
return files
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
def _expand_env_vars(value):
|
|
44
|
+
"""Recursively expand environment variables in config values."""
|
|
45
|
+
if isinstance(value, str):
|
|
46
|
+
pattern = re.compile(r'\$\{([^}]+)\}')
|
|
47
|
+
def replace_env(match):
|
|
48
|
+
env_var = match.group(1)
|
|
49
|
+
return os.environ.get(env_var, match.group(0))
|
|
50
|
+
return pattern.sub(replace_env, value)
|
|
51
|
+
elif isinstance(value, dict):
|
|
52
|
+
return {k: _expand_env_vars(v) for k, v in value.items()}
|
|
53
|
+
elif isinstance(value, list):
|
|
54
|
+
return [_expand_env_vars(item) for item in value]
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
41
58
|
class Config(dict):
|
|
42
59
|
"""Read config from yaml file."""
|
|
43
60
|
|
|
@@ -55,7 +72,9 @@ class Config(dict):
|
|
|
55
72
|
with open(fp, 'r') as fh:
|
|
56
73
|
try:
|
|
57
74
|
for data in safe_load_all(fh):
|
|
75
|
+
if '__vars__' in data:
|
|
76
|
+
del data['__vars__']
|
|
58
77
|
for x in data:
|
|
59
|
-
self[x] = data[x]
|
|
78
|
+
self[x] = _expand_env_vars(data[x])
|
|
60
79
|
except YAMLError as e:
|
|
61
80
|
raise SystemExit(f'failed to load {filename} {e}')
|
pymscada/demo/callout.yaml
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
bus_ip: 127.0.0.1
|
|
2
2
|
bus_port: 1324
|
|
3
3
|
rta_tag: __callout__
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
alarms_tag: __alarms__
|
|
5
|
+
sms_send_tag: __sms_send__
|
|
6
|
+
sms_recv_tag: __sms_recv__
|
|
7
|
+
ack_tag: SI_Alarm_Ack
|
|
8
|
+
status_tag: SO_Alarm_Status
|
|
9
|
+
escalation:
|
|
10
|
+
- OnCall: 60000
|
|
11
|
+
- Backup 1: 180000
|
|
12
|
+
- Backup 2: 300000
|
|
13
|
+
- Escalate: 600000
|
|
7
14
|
callees:
|
|
8
15
|
- name: A name
|
|
9
16
|
sms: A number
|
|
17
|
+
group: System
|
|
10
18
|
- name: B name
|
|
11
19
|
sms: B number
|
|
12
20
|
groups:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
Wind:
|
|
22
|
+
tagnames: [Mt, Fh]
|
|
23
|
+
System:
|
|
24
|
+
groups: [__system__, Comms]
|
|
25
|
+
Test:
|
|
26
|
+
tagnames: [alarm]
|
pymscada/demo/openweather.yaml
CHANGED
pymscada/demo/piapi.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
bus_ip: 127.0.0.1
|
|
2
|
+
bus_port: 1324
|
|
3
|
+
proxy:
|
|
4
|
+
api:
|
|
5
|
+
url: 'https://192.168.15.1/'
|
|
6
|
+
webid: F1Em9DA80Xftdkec1gdWFtX7NAm1eiSAyV8BG1mAAMKQIjRQUEdQSVxQSU9ORUVSXFdFQkFQSVxNT0JJTEVTQ0FEQQ
|
|
7
|
+
averaging: 300
|
|
8
|
+
tags:
|
|
9
|
+
I_An_G1_MW:
|
|
10
|
+
pitag: AnG1.AIMw
|
|
11
|
+
I_An_G2_MW:
|
|
12
|
+
pitag: AnG2.AIMw
|
|
13
|
+
I_Ko_Gn_MW:
|
|
14
|
+
pitag: KoGn.AIkW
|
|
15
|
+
scale: 1000
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - callout
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ callout --config __DIR__/config/callout.yaml --tags __DIR__/config/tags.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=__USER__
|
|
12
|
+
Group=__USER__
|
|
13
|
+
KillSignal=SIGINT
|
|
14
|
+
|
|
15
|
+
[Install]
|
|
16
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - OSI PI client
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ witsapiclient --config __DIR__/config/piapi.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=__USER__
|
|
12
|
+
Group=__USER__
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - SMS client
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Environment="MSCADA_SMS_IP=an_ip_address"
|
|
8
|
+
Environment="MSCADA_SMS_USERNAME=a_user"
|
|
9
|
+
Environment="MSCADA_SMS_PASSWORD=a_password"
|
|
10
|
+
WorkingDirectory=__DIR__
|
|
11
|
+
ExecStart=__PYMSCADA__ sms --config __DIR__/config/sms.yaml
|
|
12
|
+
Restart=always
|
|
13
|
+
RestartSec=5
|
|
14
|
+
User=root
|
|
15
|
+
Group=root
|
|
16
|
+
|
|
17
|
+
[Install]
|
|
18
|
+
WantedBy=multi-user.target
|
pymscada/demo/sms.yaml
ADDED
pymscada/demo/tags.yaml
CHANGED
pymscada/demo/witsapi.yaml
CHANGED
|
@@ -3,8 +3,8 @@ bus_port: 1324
|
|
|
3
3
|
proxy:
|
|
4
4
|
api:
|
|
5
5
|
url: 'https://api.electricityinfo.co.nz'
|
|
6
|
-
client_id: ${
|
|
7
|
-
client_secret: ${
|
|
6
|
+
client_id: ${MSCADA_WITS_CLIENT_ID}
|
|
7
|
+
client_secret: ${MSCADA_WITS_CLIENT_SECRET}
|
|
8
8
|
gxp_list:
|
|
9
9
|
- MAT1101
|
|
10
10
|
- CYD2201
|
pymscada/demo/wwwserver.yaml
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
__vars__:
|
|
2
|
+
- &primary_color darkmagenta
|
|
3
|
+
- &secondary_color green
|
|
1
4
|
bus_ip: 127.0.0.1
|
|
2
5
|
bus_port: 1324
|
|
3
6
|
ip: 0.0.0.0
|
|
4
7
|
port: 8324
|
|
5
8
|
get_path:
|
|
6
9
|
serve_path: __HOME__
|
|
10
|
+
config:
|
|
11
|
+
primary_color: *primary_color
|
|
12
|
+
secondary_color: *secondary_color
|
|
13
|
+
mscada_link: http://127.0.0.1/
|
|
14
|
+
people:
|
|
15
|
+
- name: John Smith
|
|
16
|
+
email: john.smith@example.com
|
|
17
|
+
phone: '+64not_a_number'
|
|
7
18
|
pages:
|
|
8
19
|
- name: Notes
|
|
9
20
|
parent:
|
|
@@ -78,6 +89,10 @@ pages:
|
|
|
78
89
|
scale: mS
|
|
79
90
|
color: red
|
|
80
91
|
dp: 1
|
|
92
|
+
bands:
|
|
93
|
+
- series: [localhost_ping, google_ping]
|
|
94
|
+
fill: [green, 0.3]
|
|
95
|
+
dir: 1
|
|
81
96
|
- name: Weather
|
|
82
97
|
parent:
|
|
83
98
|
items:
|
pymscada/history.py
CHANGED
|
@@ -270,7 +270,7 @@ class History():
|
|
|
270
270
|
self.tags[tagname] = Tag(tagname, tag['type'])
|
|
271
271
|
self.tags[tagname].add_callback(self.hist_tags[tagname].callback)
|
|
272
272
|
self.rta = Tag(rta_tag, bytes)
|
|
273
|
-
self.rta.value = b'\x00\x00\x00\x00\x00\x00'
|
|
273
|
+
self.rta.value = b'\x00\x00\x00\x00\x00\x00' # rta_id is 0
|
|
274
274
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
275
275
|
|
|
276
276
|
def rta_cb(self, request: Request):
|
|
@@ -278,9 +278,7 @@ class History():
|
|
|
278
278
|
if 'start_ms' in request:
|
|
279
279
|
request['start_us'] = request['start_ms'] * 1000
|
|
280
280
|
request['end_us'] = request['end_ms'] * 1000
|
|
281
|
-
rta_id =
|
|
282
|
-
if '__rta_id__' in request:
|
|
283
|
-
rta_id = request['__rta_id__']
|
|
281
|
+
rta_id = request['__rta_id__']
|
|
284
282
|
tagname = request['tagname']
|
|
285
283
|
start_time = time.asctime(time.localtime(
|
|
286
284
|
request['start_us'] / 1000000))
|
|
@@ -299,7 +297,7 @@ class History():
|
|
|
299
297
|
packtype = 2
|
|
300
298
|
self.rta.value = pack('>HHH', rta_id, tagid, packtype) + data
|
|
301
299
|
logging.info(f'sent {len(data)} bytes for {request["tagname"]}')
|
|
302
|
-
self.rta.value = b'\x00\x00\x00\x00\x00\x00'
|
|
300
|
+
self.rta.value = b'\x00\x00\x00\x00\x00\x00' # rta_id is 0
|
|
303
301
|
except Exception as e:
|
|
304
302
|
logging.error(f'history rta_cb {e}')
|
|
305
303
|
|