pymscada 0.2.0__py3-none-any.whl → 0.2.6b9__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.
- pymscada/__init__.py +8 -2
- pymscada/alarms.py +179 -60
- pymscada/bus_client.py +12 -2
- pymscada/bus_server.py +18 -9
- pymscada/callout.py +198 -101
- pymscada/config.py +20 -1
- pymscada/console.py +19 -6
- pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/demo/callout.yaml +13 -4
- pymscada/demo/files.yaml +3 -2
- pymscada/demo/openweather.yaml +3 -11
- 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 +6 -8
- pymscada/demo/wwwserver.yaml +15 -0
- pymscada/files.py +1 -0
- pymscada/history.py +4 -5
- pymscada/iodrivers/logix_map.py +1 -1
- pymscada/iodrivers/modbus_client.py +189 -21
- pymscada/iodrivers/modbus_map.py +17 -2
- 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 +38 -16
- pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/tag.py +6 -7
- pymscada/tools/get_history.py +147 -0
- pymscada/www_server.py +2 -1
- {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/METADATA +2 -2
- {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/RECORD +38 -32
- pymscada/validate.py +0 -451
- {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/WHEEL +0 -0
- {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/entry_points.txt +0 -0
- {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/licenses/LICENSE +0 -0
- {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/top_level.txt +0 -0
pymscada/__init__.py
CHANGED
|
@@ -2,25 +2,31 @@
|
|
|
2
2
|
from pymscada.bus_client import BusClient
|
|
3
3
|
from pymscada.bus_server import BusServer
|
|
4
4
|
from pymscada.config import Config
|
|
5
|
+
from pymscada.callout import Callout, ALM
|
|
5
6
|
from pymscada.iodrivers.accuweather import AccuWeatherClient
|
|
6
7
|
from pymscada.iodrivers.logix_client import LogixClient
|
|
7
8
|
from pymscada.iodrivers.modbus_client import ModbusClient
|
|
8
9
|
from pymscada.iodrivers.modbus_server import ModbusServer
|
|
10
|
+
from pymscada.iodrivers.piapi import PIWebAPIClient
|
|
11
|
+
from pymscada.iodrivers.sms import SMS
|
|
12
|
+
from pymscada.iodrivers.witsapi import WitsAPIClient
|
|
9
13
|
from pymscada.misc import find_nodes, ramp
|
|
10
14
|
from pymscada.periodic import Periodic
|
|
11
15
|
from pymscada.tag import Tag
|
|
12
|
-
from pymscada.validate import validate
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
15
18
|
'BusClient',
|
|
16
19
|
'BusServer',
|
|
17
20
|
'Config',
|
|
21
|
+
'Callout', 'ALM',
|
|
18
22
|
'AccuWeatherClient',
|
|
19
23
|
'LogixClient',
|
|
20
24
|
'ModbusClient',
|
|
21
25
|
'ModbusServer',
|
|
26
|
+
'PIWebAPIClient',
|
|
27
|
+
'SMS',
|
|
28
|
+
'WitsAPIClient',
|
|
22
29
|
'find_nodes', 'ramp',
|
|
23
30
|
'Periodic',
|
|
24
31
|
'Tag',
|
|
25
|
-
'validate',
|
|
26
32
|
]
|
pymscada/alarms.py
CHANGED
|
@@ -11,12 +11,14 @@ ALM = 0
|
|
|
11
11
|
RTN = 1
|
|
12
12
|
ACT = 2
|
|
13
13
|
INF = 3
|
|
14
|
+
TIMING = 4
|
|
14
15
|
KIND = {
|
|
15
16
|
ALM: 'ALM',
|
|
16
17
|
RTN: 'RTN',
|
|
17
18
|
ACT: 'ACT',
|
|
18
19
|
INF: 'INF'
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
NORMAL = 0
|
|
21
23
|
ALARM = 1
|
|
22
24
|
|
|
@@ -74,12 +76,11 @@ def standardise_tag_info(tagname: str, tag: dict):
|
|
|
74
76
|
def split_operator(alarm: str) -> dict:
|
|
75
77
|
"""Split alarm string into operator and value."""
|
|
76
78
|
tokens = alarm.split(' ')
|
|
77
|
-
alm_dict = {'for': 0}
|
|
78
79
|
if len(tokens) not in (2, 4):
|
|
79
80
|
raise ValueError(f"Invalid alarm {alarm}")
|
|
80
81
|
if tokens[0] not in ['>', '<', '==', '>=', '<=']:
|
|
81
82
|
raise ValueError(f"Invalid alarm {alarm}")
|
|
82
|
-
alm_dict
|
|
83
|
+
alm_dict = {'for': 0, 'operator': tokens[0], 'value': None}
|
|
83
84
|
try:
|
|
84
85
|
alm_dict['value'] = float(tokens[1])
|
|
85
86
|
except ValueError:
|
|
@@ -102,10 +103,11 @@ class Alarm():
|
|
|
102
103
|
conditions, each combination of tag and condition is a separate Alarm.
|
|
103
104
|
|
|
104
105
|
Monitors tag value through the Tag callback. Tracks in alarm state.
|
|
105
|
-
|
|
106
|
+
Notifies Alarms of state changes via state callback.
|
|
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
|
+
state_cb) -> 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'])
|
|
@@ -114,18 +116,18 @@ class Alarm():
|
|
|
114
116
|
self.tag.units = tag['units']
|
|
115
117
|
self.tag.add_callback(self.callback)
|
|
116
118
|
self.group = group
|
|
117
|
-
self.
|
|
118
|
-
self.
|
|
119
|
+
self.state_cb = state_cb
|
|
120
|
+
self.timing_value = None
|
|
121
|
+
self.timing_us = None
|
|
119
122
|
self.alarm = split_operator(alarm)
|
|
120
123
|
self.in_alarm = False
|
|
121
|
-
self.
|
|
124
|
+
self.disabled_until = 0
|
|
122
125
|
|
|
123
126
|
def callback(self, tag: Tag):
|
|
124
127
|
"""Handle tag value changes and generate ALM/RTN messages."""
|
|
125
|
-
if tag.value is None:
|
|
128
|
+
if tag.value is None or tag.time_us < self.disabled_until:
|
|
126
129
|
return
|
|
127
130
|
value = float(tag.value)
|
|
128
|
-
time_us = tag.time_us
|
|
129
131
|
new_in_alarm = False
|
|
130
132
|
op = self.alarm['operator']
|
|
131
133
|
if op == '>':
|
|
@@ -140,39 +142,25 @@ class Alarm():
|
|
|
140
142
|
new_in_alarm = value <= self.alarm['value']
|
|
141
143
|
if new_in_alarm == self.in_alarm:
|
|
142
144
|
return
|
|
143
|
-
|
|
144
|
-
if self.in_alarm:
|
|
145
|
+
if new_in_alarm:
|
|
145
146
|
if self.alarm['for'] > 0:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
self.state_cb(self, TIMING)
|
|
148
|
+
self.timing_us = tag.time_us
|
|
149
|
+
self.timing_value = value
|
|
149
150
|
else:
|
|
150
|
-
self.
|
|
151
|
+
self.state_cb(self, ALM)
|
|
151
152
|
else:
|
|
152
|
-
if self.
|
|
153
|
-
self.
|
|
154
|
-
|
|
155
|
-
self.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"""Generate alarm message."""
|
|
159
|
-
logging.warning(f'Alarm {self.alarm_id} {value} {KIND[kind]}')
|
|
160
|
-
self.rta_cb({
|
|
161
|
-
'action': 'ADD',
|
|
162
|
-
'date_ms': int(time_us / 1000),
|
|
163
|
-
'alarm_string': self.alarm_id,
|
|
164
|
-
'kind': kind,
|
|
165
|
-
'desc': f'{self.tag.desc} {value:.{self.tag.dp}f}'
|
|
166
|
-
f' {self.tag.units}',
|
|
167
|
-
'group': self.group
|
|
168
|
-
})
|
|
153
|
+
if self.timing_us is not None:
|
|
154
|
+
if tag.time_us - self.timing_us >= self.alarm['for'] * 1000000:
|
|
155
|
+
self.state_cb(self, ALM)
|
|
156
|
+
self.timing_us = None
|
|
157
|
+
self.state_cb(self, RTN)
|
|
158
|
+
self.in_alarm = new_in_alarm
|
|
169
159
|
|
|
170
160
|
def check_duration(self, current_time_us: int):
|
|
171
161
|
"""Check if alarm condition has been met for required duration."""
|
|
172
162
|
if current_time_us - self.tag.time_us >= self.alarm['for'] * 1000000:
|
|
173
|
-
self.
|
|
174
|
-
self.checking = False
|
|
175
|
-
self.alarms.checking_alarms.remove(self)
|
|
163
|
+
self.state_cb(self, ALM)
|
|
176
164
|
|
|
177
165
|
|
|
178
166
|
class Alarms:
|
|
@@ -218,19 +206,20 @@ class Alarms:
|
|
|
218
206
|
logging.warning(f'Alarms {bus_ip} {bus_port} {db} {rta_tag}')
|
|
219
207
|
self.alarms: list[Alarm] = []
|
|
220
208
|
self.checking_alarms: list[Alarm] = []
|
|
209
|
+
self.in_alarm: list[Alarm] = []
|
|
221
210
|
for tagname, tag in tag_info.items():
|
|
222
211
|
standardise_tag_info(tagname, tag)
|
|
223
212
|
if 'alarm' not in tag or tag['type'] not in (int, float):
|
|
224
213
|
continue
|
|
225
214
|
group = tag['group']
|
|
226
215
|
for alarm in tag['alarm']:
|
|
227
|
-
new_alarm = Alarm(tagname, tag, alarm, group, self.
|
|
228
|
-
self)
|
|
216
|
+
new_alarm = Alarm(tagname, tag, alarm, group, self.state_cb)
|
|
229
217
|
self.alarms.append(new_alarm)
|
|
230
218
|
self.busclient = BusClient(bus_ip, bus_port, module='Alarms')
|
|
231
219
|
self.rta = Tag(rta_tag, dict)
|
|
232
|
-
self.rta.value = {}
|
|
220
|
+
self.rta.value = {'__rta_id__': 0}
|
|
233
221
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
222
|
+
self.busclient.add_tag(self.rta)
|
|
234
223
|
self._init_db(db, table)
|
|
235
224
|
self.periodic = Periodic(self.periodic_cb, 1.0)
|
|
236
225
|
|
|
@@ -239,6 +228,14 @@ class Alarms:
|
|
|
239
228
|
self.connection = sqlite3.connect(db)
|
|
240
229
|
self.table = table
|
|
241
230
|
self.cursor = self.connection.cursor()
|
|
231
|
+
sqlite_version = sqlite3.sqlite_version_info
|
|
232
|
+
self.has_returning = sqlite_version >= (3, 35, 0)
|
|
233
|
+
if not self.has_returning:
|
|
234
|
+
logging.warning(
|
|
235
|
+
f'SQLite {sqlite3.sqlite_version} is older than 3.35.0. '
|
|
236
|
+
f'RETURNING clause not supported, using fallback method. '
|
|
237
|
+
f'Consider upgrading SQLite for better performance.'
|
|
238
|
+
)
|
|
242
239
|
query = (
|
|
243
240
|
'CREATE TABLE IF NOT EXISTS ' + self.table + ' '
|
|
244
241
|
'(id INTEGER PRIMARY KEY ASC, '
|
|
@@ -250,7 +247,6 @@ class Alarms:
|
|
|
250
247
|
)
|
|
251
248
|
self.cursor.execute(query)
|
|
252
249
|
self.connection.commit()
|
|
253
|
-
|
|
254
250
|
startup_record = {
|
|
255
251
|
'action': 'ADD',
|
|
256
252
|
'date_ms': int(time.time() * 1000),
|
|
@@ -267,6 +263,39 @@ class Alarms:
|
|
|
267
263
|
for alarm in self.checking_alarms[:]:
|
|
268
264
|
alarm.check_duration(current_time_us)
|
|
269
265
|
|
|
266
|
+
def state_cb(self, alarm: Alarm, state: int):
|
|
267
|
+
"""Handle alarm state changes."""
|
|
268
|
+
if state == TIMING:
|
|
269
|
+
self.checking_alarms.append(alarm)
|
|
270
|
+
elif state == ALM:
|
|
271
|
+
if alarm in self.checking_alarms:
|
|
272
|
+
self.checking_alarms.remove(alarm)
|
|
273
|
+
self.in_alarm.append(alarm)
|
|
274
|
+
self.generate_alarm(alarm, ALM)
|
|
275
|
+
elif state == RTN:
|
|
276
|
+
if alarm in self.checking_alarms:
|
|
277
|
+
self.checking_alarms.remove(alarm)
|
|
278
|
+
if alarm in self.in_alarm:
|
|
279
|
+
self.in_alarm.remove(alarm)
|
|
280
|
+
self.generate_alarm(alarm, RTN)
|
|
281
|
+
|
|
282
|
+
def generate_alarm(self, alarm: Alarm, kind: int):
|
|
283
|
+
"""Generate alarm message."""
|
|
284
|
+
value = alarm.tag.value
|
|
285
|
+
if alarm.timing_us is not None:
|
|
286
|
+
value = alarm.timing_value
|
|
287
|
+
time_us = alarm.tag.time_us
|
|
288
|
+
logging.warning(f'Alarm {alarm.alarm_id} {value} {KIND[kind]}')
|
|
289
|
+
self.rta_cb({
|
|
290
|
+
'action': 'ADD',
|
|
291
|
+
'date_ms': int(time_us / 1000),
|
|
292
|
+
'alarm_string': alarm.alarm_id,
|
|
293
|
+
'kind': kind,
|
|
294
|
+
'desc': f'{alarm.tag.desc} {value:.{alarm.tag.dp}f}'
|
|
295
|
+
f' {alarm.tag.units}',
|
|
296
|
+
'group': alarm.group
|
|
297
|
+
})
|
|
298
|
+
|
|
270
299
|
def rta_cb(self, request):
|
|
271
300
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
272
301
|
if 'action' not in request:
|
|
@@ -275,14 +304,27 @@ class Alarms:
|
|
|
275
304
|
try:
|
|
276
305
|
logging.info(f'add {request}')
|
|
277
306
|
with self.connection:
|
|
278
|
-
self.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
307
|
+
if self.has_returning:
|
|
308
|
+
self.cursor.execute(
|
|
309
|
+
f'INSERT INTO {self.table} '
|
|
310
|
+
'(date_ms, alarm_string, kind, desc, "group") '
|
|
311
|
+
'VALUES(:date_ms, :alarm_string, :kind, :desc, :group) '
|
|
312
|
+
'RETURNING *;',
|
|
313
|
+
request)
|
|
314
|
+
res = self.cursor.fetchone()
|
|
315
|
+
else:
|
|
316
|
+
self.cursor.execute(
|
|
317
|
+
f'INSERT INTO {self.table} '
|
|
318
|
+
'(date_ms, alarm_string, kind, desc, "group") '
|
|
319
|
+
'VALUES(:date_ms, :alarm_string, :kind, :desc, :group);',
|
|
320
|
+
request)
|
|
321
|
+
row_id = self.cursor.lastrowid
|
|
322
|
+
self.cursor.execute(
|
|
323
|
+
f'SELECT * FROM {self.table} WHERE id = ?;',
|
|
324
|
+
(row_id,))
|
|
325
|
+
res = self.cursor.fetchone()
|
|
285
326
|
self.rta.value = {
|
|
327
|
+
'__rta_id__': 0,
|
|
286
328
|
'id': res[0],
|
|
287
329
|
'date_ms': res[1],
|
|
288
330
|
'alarm_string': res[2],
|
|
@@ -296,20 +338,42 @@ class Alarms:
|
|
|
296
338
|
try:
|
|
297
339
|
logging.info(f'update {request}')
|
|
298
340
|
with self.connection:
|
|
299
|
-
self.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
341
|
+
if self.has_returning:
|
|
342
|
+
self.cursor.execute(
|
|
343
|
+
f'UPDATE {self.table} SET in_alm = :in_alm '
|
|
344
|
+
'WHERE id = :id RETURNING *;',
|
|
345
|
+
request)
|
|
346
|
+
res = self.cursor.fetchone()
|
|
347
|
+
if res:
|
|
348
|
+
self.rta.value = {
|
|
349
|
+
'__rta_id__': 0,
|
|
350
|
+
'id': res[0],
|
|
351
|
+
'date_ms': res[1],
|
|
352
|
+
'alarm_string': res[2],
|
|
353
|
+
'kind': res[3],
|
|
354
|
+
'desc': res[4],
|
|
355
|
+
'group': res[5]
|
|
356
|
+
}
|
|
357
|
+
else:
|
|
358
|
+
self.cursor.execute(
|
|
359
|
+
f'UPDATE {self.table} SET in_alm = :in_alm '
|
|
360
|
+
'WHERE id = :id;',
|
|
361
|
+
request)
|
|
362
|
+
if self.cursor.rowcount > 0:
|
|
363
|
+
self.cursor.execute(
|
|
364
|
+
f'SELECT * FROM {self.table} WHERE id = :id;',
|
|
365
|
+
request)
|
|
366
|
+
res = self.cursor.fetchone()
|
|
367
|
+
if res:
|
|
368
|
+
self.rta.value = {
|
|
369
|
+
'__rta_id__': 0,
|
|
370
|
+
'id': res[0],
|
|
371
|
+
'date_ms': res[1],
|
|
372
|
+
'alarm_string': res[2],
|
|
373
|
+
'kind': res[3],
|
|
374
|
+
'desc': res[4],
|
|
375
|
+
'group': res[5]
|
|
376
|
+
}
|
|
313
377
|
except sqlite3.IntegrityError as error:
|
|
314
378
|
logging.warning(f'Alarms rta_cb update {error}')
|
|
315
379
|
elif request['action'] == 'HISTORY':
|
|
@@ -346,6 +410,61 @@ class Alarms:
|
|
|
346
410
|
elif request['action'] == 'IN ALARM':
|
|
347
411
|
self.rta.value = {'__rta_id__': request['__rta_id__'],
|
|
348
412
|
'data': {'in_alarm': list(self.in_alarm)}}
|
|
413
|
+
elif request['action'] == 'ENABLE':
|
|
414
|
+
time_us = int(time.time() * 1000000)
|
|
415
|
+
local_time = time.localtime(time_us / 1000000)
|
|
416
|
+
for alarm in self.alarms:
|
|
417
|
+
if alarm.alarm_id == request['alarm id']:
|
|
418
|
+
enable = request['enable']
|
|
419
|
+
if enable == 'Enable':
|
|
420
|
+
disabled_until_us = 0
|
|
421
|
+
else:
|
|
422
|
+
if enable == 'Disable until 8am':
|
|
423
|
+
target_hour = 8
|
|
424
|
+
target_day_offset = 0
|
|
425
|
+
next_offset = 1
|
|
426
|
+
elif enable == 'Disable until 4pm':
|
|
427
|
+
target_hour = 16
|
|
428
|
+
target_day_offset = 0
|
|
429
|
+
next_offset = 1
|
|
430
|
+
elif enable == 'Disable until Monday 8am':
|
|
431
|
+
target_hour = 8
|
|
432
|
+
target_day_offset = (0 - local_time.tm_wday) % 7
|
|
433
|
+
next_offset = 7
|
|
434
|
+
else:
|
|
435
|
+
disabled_until_us = 0
|
|
436
|
+
break
|
|
437
|
+
target_s = time.mktime((
|
|
438
|
+
local_time.tm_year, local_time.tm_mon,
|
|
439
|
+
local_time.tm_mday + target_day_offset,
|
|
440
|
+
target_hour, 0, 0, 0, 0, -1
|
|
441
|
+
))
|
|
442
|
+
if target_s * 1000000 <= time_us:
|
|
443
|
+
target_s = time.mktime((
|
|
444
|
+
local_time.tm_year, local_time.tm_mon,
|
|
445
|
+
local_time.tm_mday + next_offset,
|
|
446
|
+
target_hour, 0, 0, 0, 0, -1
|
|
447
|
+
))
|
|
448
|
+
disabled_until_us = int(target_s * 1000000)
|
|
449
|
+
alarm.disabled_until = disabled_until_us
|
|
450
|
+
ts = time.strftime(
|
|
451
|
+
"%Y-%m-%d %H:%M:%S",
|
|
452
|
+
time.localtime(disabled_until_us / 1000000)
|
|
453
|
+
)
|
|
454
|
+
if disabled_until_us == 0:
|
|
455
|
+
desc = 'Enable'
|
|
456
|
+
else:
|
|
457
|
+
desc = f'Disable until {ts}'
|
|
458
|
+
self.rta_cb({
|
|
459
|
+
'action': 'ADD',
|
|
460
|
+
'date_ms': int(time_us / 1000),
|
|
461
|
+
'alarm_string': alarm.alarm_id,
|
|
462
|
+
'kind': ACT,
|
|
463
|
+
'desc': desc,
|
|
464
|
+
'group': alarm.group
|
|
465
|
+
})
|
|
466
|
+
break
|
|
467
|
+
|
|
349
468
|
|
|
350
469
|
async def start(self):
|
|
351
470
|
"""Async startup."""
|
pymscada/bus_client.py
CHANGED
|
@@ -84,6 +84,12 @@ class BusClient:
|
|
|
84
84
|
"""Write a message."""
|
|
85
85
|
if data is None:
|
|
86
86
|
data = b''
|
|
87
|
+
try:
|
|
88
|
+
size_total = len(data)
|
|
89
|
+
except Exception:
|
|
90
|
+
size_total = 0
|
|
91
|
+
logging.info(f'{self.module}: write cmd={command} tag_id={tag_id} '
|
|
92
|
+
f'size_total={size_total}')
|
|
87
93
|
for i in range(0, len(data) + 1, pc.MAX_LEN):
|
|
88
94
|
snip = data[i:i+pc.MAX_LEN]
|
|
89
95
|
size = len(snip)
|
|
@@ -94,7 +100,9 @@ class BusClient:
|
|
|
94
100
|
except (asyncio.IncompleteReadError, ConnectionResetError):
|
|
95
101
|
self.read_task.cancel()
|
|
96
102
|
except AttributeError:
|
|
97
|
-
logging.warning('
|
|
103
|
+
logging.warning(f'{self.module}: write AttributeError '
|
|
104
|
+
f'cmd={command} '
|
|
105
|
+
f'tag_id={tag_id} size={size}')
|
|
98
106
|
|
|
99
107
|
def add_tag(self, tag: Tag):
|
|
100
108
|
"""Add the new tag and get the tag's bus ID."""
|
|
@@ -209,10 +217,12 @@ class BusClient:
|
|
|
209
217
|
data = struct.unpack_from(f'!{len(value) - 1}s', value, offset=1
|
|
210
218
|
)[0].decode()
|
|
211
219
|
data = json.loads(data)
|
|
220
|
+
logging.info(f'{self.module}: RTA received {tag.name} {data} '
|
|
221
|
+
f'from tag_id {tag_id}')
|
|
212
222
|
try:
|
|
213
223
|
self.rta_handlers[tag.name](data)
|
|
214
224
|
except KeyError:
|
|
215
|
-
logging.warning(f'unhandled RTA for {tag.name} {data}')
|
|
225
|
+
logging.warning(f'{self.module}: unhandled RTA for {tag.name} {data}')
|
|
216
226
|
else:
|
|
217
227
|
raise SystemExit(f'Invalid message {cmd}')
|
|
218
228
|
|
pymscada/bus_server.py
CHANGED
|
@@ -18,8 +18,7 @@ class BusTags(type):
|
|
|
18
18
|
"""Return existing tag if tagname already exists."""
|
|
19
19
|
if tagname in cls._tag_by_name:
|
|
20
20
|
return cls._tag_by_name[tagname]
|
|
21
|
-
tag: 'BusTag' =
|
|
22
|
-
tag.__init__(tagname)
|
|
21
|
+
tag: 'BusTag' = super().__call__(tagname)
|
|
23
22
|
tag.id = cls._next_id
|
|
24
23
|
cls._next_id += 1
|
|
25
24
|
cls._tag_by_name[tagname] = tag
|
|
@@ -35,10 +34,10 @@ class BusTag(metaclass=BusTags):
|
|
|
35
34
|
def __init__(self, name: bytes):
|
|
36
35
|
"""Name and id must be unique."""
|
|
37
36
|
self.name = name
|
|
38
|
-
self.id =
|
|
37
|
+
self.id = 0
|
|
39
38
|
self.time_us: int = 0
|
|
40
39
|
self.value: bytes = b''
|
|
41
|
-
self.from_bus:
|
|
40
|
+
self.from_bus: int = 0
|
|
42
41
|
self.pub = []
|
|
43
42
|
|
|
44
43
|
def add_callback(self, callback, bus_id):
|
|
@@ -51,7 +50,7 @@ class BusTag(metaclass=BusTags):
|
|
|
51
50
|
if (callback, bus_id) in self.pub:
|
|
52
51
|
self.pub.remove((callback, bus_id))
|
|
53
52
|
|
|
54
|
-
def update(self, data: bytes, time_us: int, from_bus:
|
|
53
|
+
def update(self, data: bytes, time_us: int, from_bus: int):
|
|
55
54
|
"""Assign value and update subscribers."""
|
|
56
55
|
self.value = data
|
|
57
56
|
self.time_us = time_us
|
|
@@ -111,7 +110,8 @@ class BusConnection():
|
|
|
111
110
|
head = await self.reader.readexactly(14)
|
|
112
111
|
_, cmd, tag_id, size, time_us = unpack('!BBHHQ', head)
|
|
113
112
|
except (ConnectionResetError, asyncio.IncompleteReadError,
|
|
114
|
-
asyncio.CancelledError):
|
|
113
|
+
asyncio.CancelledError) as e:
|
|
114
|
+
logging.warning(f'{self.addr} read error: {e}')
|
|
115
115
|
break
|
|
116
116
|
# if the command packet indicates data, get that too
|
|
117
117
|
if size == 0:
|
|
@@ -121,7 +121,8 @@ class BusConnection():
|
|
|
121
121
|
payload = await self.reader.readexactly(size)
|
|
122
122
|
data = unpack(f'!{size}s', payload)[0]
|
|
123
123
|
except (ConnectionResetError, asyncio.IncompleteReadError,
|
|
124
|
-
asyncio.CancelledError):
|
|
124
|
+
asyncio.CancelledError) as e:
|
|
125
|
+
logging.warning(f'{self.addr} read payload error: {e}')
|
|
125
126
|
break
|
|
126
127
|
# if MAX_LEN then a continuation packet is required
|
|
127
128
|
if size == pc.MAX_LEN:
|
|
@@ -201,15 +202,21 @@ class BusServer:
|
|
|
201
202
|
try:
|
|
202
203
|
tag = BusTags._tag_by_id[tag_id]
|
|
203
204
|
except KeyError:
|
|
205
|
+
logging.warning(f'RTA KeyError {tag_id}')
|
|
204
206
|
self.connections[bus_id].write(
|
|
205
207
|
pc.COMMAND.ERR, tag_id, time_us,
|
|
206
208
|
f"RTA KeyError {tag_id}".encode())
|
|
209
|
+
return
|
|
207
210
|
try:
|
|
211
|
+
logging.info(f'RTA forwarding {tag.name} from_bus={tag.from_bus} '
|
|
212
|
+
f'to bus_id={tag.from_bus}')
|
|
208
213
|
self.connections[tag.from_bus].write(
|
|
209
214
|
pc.COMMAND.RTA, tag_id, tag.time_us, data)
|
|
210
215
|
except KeyError:
|
|
211
|
-
logging.warning(f'
|
|
216
|
+
logging.warning(f'RTA forwarding failed: busclient for '
|
|
217
|
+
f'{tag.name} (from_bus={tag.from_bus}) is gone')
|
|
212
218
|
except Exception as e:
|
|
219
|
+
logging.warning(f'RTA forwarding error {tag.name}: {e}')
|
|
213
220
|
self.connections[bus_id].write(
|
|
214
221
|
pc.COMMAND.ERR, tag_id, time_us,
|
|
215
222
|
f"RTA {tag_id} {e}".encode())
|
|
@@ -283,11 +290,13 @@ class BusServer:
|
|
|
283
290
|
self.bus_tag.value = f'\x03{client_addr}: {log_msg}'.encode()
|
|
284
291
|
self.bus_tag.time_us = int(time.time() * 1e6)
|
|
285
292
|
else: # consider disconnecting
|
|
286
|
-
logging.
|
|
293
|
+
logging.warning(f'invalid message {cmd}')
|
|
287
294
|
|
|
288
295
|
def read_callback(self, command):
|
|
289
296
|
"""Process read messages, delete broken connections."""
|
|
290
297
|
bus_id, cmd, tag_id, time_us, data = command
|
|
298
|
+
logging.info(f'recv cmd={cmd} tag_id={tag_id} bus_id={bus_id} '
|
|
299
|
+
f'size={(0 if data is None else len(data))}')
|
|
291
300
|
if cmd is None:
|
|
292
301
|
# Clean up tag subscriptions before deleting it
|
|
293
302
|
for tag in BusTags._tag_by_id.values():
|