pymscada 0.2.1__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 +127 -74
- pymscada/config.py +20 -1
- pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/demo/callout.yaml +13 -4
- pymscada/demo/openweather.yaml +1 -1
- pymscada/demo/piapi.yaml +15 -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/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/tools/get_history.py +147 -0
- pymscada/www_server.py +1 -1
- {pymscada-0.2.1.dist-info → pymscada-0.2.2.dist-info}/METADATA +1 -1
- {pymscada-0.2.1.dist-info → pymscada-0.2.2.dist-info}/RECORD +29 -23
- pymscada/validate.py +0 -451
- {pymscada-0.2.1.dist-info → pymscada-0.2.2.dist-info}/WHEEL +0 -0
- {pymscada-0.2.1.dist-info → pymscada-0.2.2.dist-info}/entry_points.txt +0 -0
- {pymscada-0.2.1.dist-info → pymscada-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {pymscada-0.2.1.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,100 +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."""
|
|
162
188
|
logging.info(f'rta_cb {request}')
|
|
163
189
|
if 'action' not in request:
|
|
164
190
|
logging.warning(f'rta_cb malformed {request}')
|
|
165
|
-
|
|
166
|
-
|
|
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}
|
|
167
197
|
elif request['action'] == 'MODIFY':
|
|
198
|
+
send_update = False
|
|
168
199
|
for callee in self.callees:
|
|
169
200
|
if callee['name'] == request['name']:
|
|
170
|
-
if '
|
|
171
|
-
callee['
|
|
201
|
+
if 'role' in request:
|
|
202
|
+
callee['role'] = request['role']
|
|
203
|
+
callee['delay_ms'] = self.delay_ms.get(
|
|
204
|
+
request['role'], 30000)
|
|
172
205
|
if 'group' in request:
|
|
173
206
|
callee['group'] = request['group']
|
|
174
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}
|
|
175
214
|
|
|
176
215
|
def check_callouts(self):
|
|
177
|
-
"""Check alarms and send callouts.
|
|
216
|
+
"""Check alarms and send callouts."""
|
|
178
217
|
time_ms = int(time.time() * 1000)
|
|
179
218
|
for callee in self.callees:
|
|
180
|
-
|
|
219
|
+
if not callee['role']:
|
|
220
|
+
continue
|
|
181
221
|
count = 0
|
|
182
|
-
|
|
222
|
+
message = ''
|
|
183
223
|
notify_ms = time_ms - callee['delay_ms']
|
|
224
|
+
remind_ms = notify_ms - 60000
|
|
184
225
|
for alarm in self.alarms:
|
|
185
|
-
if alarm['
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
191
241
|
if count > 0:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
}
|
|
196
249
|
if self.status is not None:
|
|
197
250
|
self.status.value = CALLOUT
|
|
198
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}')
|
|
Binary file
|
pymscada/demo/callout.yaml
CHANGED
|
@@ -2,8 +2,15 @@ bus_ip: 127.0.0.1
|
|
|
2
2
|
bus_port: 1324
|
|
3
3
|
rta_tag: __callout__
|
|
4
4
|
alarms_tag: __alarms__
|
|
5
|
+
sms_send_tag: __sms_send__
|
|
6
|
+
sms_recv_tag: __sms_recv__
|
|
5
7
|
ack_tag: SI_Alarm_Ack
|
|
6
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
|
|
@@ -11,7 +18,9 @@ callees:
|
|
|
11
18
|
- name: B name
|
|
12
19
|
sms: B number
|
|
13
20
|
groups:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,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
|
|