pymscada 0.2.5__tar.gz → 0.2.6b4__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.
- {pymscada-0.2.5/src/pymscada.egg-info → pymscada-0.2.6b4}/PKG-INFO +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b4}/pyproject.toml +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/__init__.py +2 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/alarms.py +102 -37
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/bus_client.py +17 -2
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/bus_server.py +13 -3
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/callout.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/console.py +19 -6
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/files.yaml +3 -2
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/openweather.yaml +2 -10
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/piapi.yaml +2 -2
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/witsapi.yaml +4 -6
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/files.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/history.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_client.py +189 -21
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_map.py +17 -2
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/opnotes.py +16 -8
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/www_server.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4/src/pymscada.egg-info}/PKG-INFO +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_callout.py +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b4}/LICENSE +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/MANIFEST.in +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/README.md +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/setup.cfg +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/__main__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/checkout.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/accuweather.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/alarms.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/callout.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/opnotes.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/ping.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-alarms.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-callout.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-history.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-ping.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-sms.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-opnotes.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/sms.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/tags.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/wwwserver.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/accuweather.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/openweather.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/piapi.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/sms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/main.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/misc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/module_config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/periodic.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/samplers.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tag.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tools/get_history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tools/snmp_client2.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/SOURCES.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/dependency_links.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/entry_points.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/requires.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/top_level.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_alarms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_bus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_misc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_opnotes.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_periodic.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_samplers.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_sms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_tag.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
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, alarm_in_group, 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
|
|
@@ -17,6 +18,7 @@ __all__ = [
|
|
|
17
18
|
'BusClient',
|
|
18
19
|
'BusServer',
|
|
19
20
|
'Config',
|
|
21
|
+
'Callout', 'alarm_in_group', ALM,
|
|
20
22
|
'AccuWeatherClient',
|
|
21
23
|
'LogixClient',
|
|
22
24
|
'ModbusClient',
|
|
@@ -11,6 +11,7 @@ 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',
|
|
@@ -75,12 +76,11 @@ def standardise_tag_info(tagname: str, tag: dict):
|
|
|
75
76
|
def split_operator(alarm: str) -> dict:
|
|
76
77
|
"""Split alarm string into operator and value."""
|
|
77
78
|
tokens = alarm.split(' ')
|
|
78
|
-
alm_dict = {'for': 0}
|
|
79
79
|
if len(tokens) not in (2, 4):
|
|
80
80
|
raise ValueError(f"Invalid alarm {alarm}")
|
|
81
81
|
if tokens[0] not in ['>', '<', '==', '>=', '<=']:
|
|
82
82
|
raise ValueError(f"Invalid alarm {alarm}")
|
|
83
|
-
alm_dict
|
|
83
|
+
alm_dict = {'for': 0, 'operator': tokens[0], 'value': None}
|
|
84
84
|
try:
|
|
85
85
|
alm_dict['value'] = float(tokens[1])
|
|
86
86
|
except ValueError:
|
|
@@ -103,11 +103,11 @@ class Alarm():
|
|
|
103
103
|
conditions, each combination of tag and condition is a separate Alarm.
|
|
104
104
|
|
|
105
105
|
Monitors tag value through the Tag callback. Tracks in alarm state.
|
|
106
|
-
|
|
106
|
+
Notifies Alarms of state changes via state callback.
|
|
107
107
|
"""
|
|
108
108
|
|
|
109
109
|
def __init__(self, tagname: str, tag: dict, alarm: str, group: str,
|
|
110
|
-
|
|
110
|
+
state_cb) -> None:
|
|
111
111
|
"""Initialize alarm with tag and condition(s)."""
|
|
112
112
|
self.alarm_id = f'{tagname} {alarm}'
|
|
113
113
|
self.tag = Tag(tagname, tag['type'])
|
|
@@ -116,18 +116,16 @@ class Alarm():
|
|
|
116
116
|
self.tag.units = tag['units']
|
|
117
117
|
self.tag.add_callback(self.callback)
|
|
118
118
|
self.group = group
|
|
119
|
-
self.
|
|
120
|
-
self.alarms = alarms
|
|
119
|
+
self.state_cb = state_cb
|
|
121
120
|
self.alarm = split_operator(alarm)
|
|
122
121
|
self.in_alarm = False
|
|
123
|
-
self.
|
|
122
|
+
self.disabled_until = 0
|
|
124
123
|
|
|
125
124
|
def callback(self, tag: Tag):
|
|
126
125
|
"""Handle tag value changes and generate ALM/RTN messages."""
|
|
127
|
-
if tag.value is None:
|
|
126
|
+
if tag.value is None or tag.time_us < self.disabled_until:
|
|
128
127
|
return
|
|
129
128
|
value = float(tag.value)
|
|
130
|
-
time_us = tag.time_us
|
|
131
129
|
new_in_alarm = False
|
|
132
130
|
op = self.alarm['operator']
|
|
133
131
|
if op == '>':
|
|
@@ -142,39 +140,19 @@ class Alarm():
|
|
|
142
140
|
new_in_alarm = value <= self.alarm['value']
|
|
143
141
|
if new_in_alarm == self.in_alarm:
|
|
144
142
|
return
|
|
145
|
-
|
|
146
|
-
if self.in_alarm:
|
|
143
|
+
if new_in_alarm:
|
|
147
144
|
if self.alarm['for'] > 0:
|
|
148
|
-
|
|
149
|
-
self.checking = True
|
|
150
|
-
self.alarms.checking_alarms.append(self)
|
|
145
|
+
self.state_cb(self, TIMING)
|
|
151
146
|
else:
|
|
152
|
-
self.
|
|
147
|
+
self.state_cb(self, ALM)
|
|
153
148
|
else:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
self.alarms.checking_alarms.remove(self)
|
|
157
|
-
self.generate_alarm(RTN, time_us, value)
|
|
158
|
-
|
|
159
|
-
def generate_alarm(self, kind: int, time_us: int, value: float):
|
|
160
|
-
"""Generate alarm message."""
|
|
161
|
-
logging.warning(f'Alarm {self.alarm_id} {value} {KIND[kind]}')
|
|
162
|
-
self.rta_cb({
|
|
163
|
-
'action': 'ADD',
|
|
164
|
-
'date_ms': int(time_us / 1000),
|
|
165
|
-
'alarm_string': self.alarm_id,
|
|
166
|
-
'kind': kind,
|
|
167
|
-
'desc': f'{self.tag.desc} {value:.{self.tag.dp}f}'
|
|
168
|
-
f' {self.tag.units}',
|
|
169
|
-
'group': self.group
|
|
170
|
-
})
|
|
149
|
+
self.state_cb(self, RTN)
|
|
150
|
+
self.in_alarm = new_in_alarm
|
|
171
151
|
|
|
172
152
|
def check_duration(self, current_time_us: int):
|
|
173
153
|
"""Check if alarm condition has been met for required duration."""
|
|
174
154
|
if current_time_us - self.tag.time_us >= self.alarm['for'] * 1000000:
|
|
175
|
-
self.
|
|
176
|
-
self.checking = False
|
|
177
|
-
self.alarms.checking_alarms.remove(self)
|
|
155
|
+
self.state_cb(self, ALM)
|
|
178
156
|
|
|
179
157
|
|
|
180
158
|
class Alarms:
|
|
@@ -220,19 +198,20 @@ class Alarms:
|
|
|
220
198
|
logging.warning(f'Alarms {bus_ip} {bus_port} {db} {rta_tag}')
|
|
221
199
|
self.alarms: list[Alarm] = []
|
|
222
200
|
self.checking_alarms: list[Alarm] = []
|
|
201
|
+
self.in_alarm: list[Alarm] = []
|
|
223
202
|
for tagname, tag in tag_info.items():
|
|
224
203
|
standardise_tag_info(tagname, tag)
|
|
225
204
|
if 'alarm' not in tag or tag['type'] not in (int, float):
|
|
226
205
|
continue
|
|
227
206
|
group = tag['group']
|
|
228
207
|
for alarm in tag['alarm']:
|
|
229
|
-
new_alarm = Alarm(tagname, tag, alarm, group, self.
|
|
230
|
-
self)
|
|
208
|
+
new_alarm = Alarm(tagname, tag, alarm, group, self.state_cb)
|
|
231
209
|
self.alarms.append(new_alarm)
|
|
232
210
|
self.busclient = BusClient(bus_ip, bus_port, module='Alarms')
|
|
233
211
|
self.rta = Tag(rta_tag, dict)
|
|
234
212
|
self.rta.value = {'__rta_id__': 0}
|
|
235
213
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
214
|
+
self.busclient.add_tag(self.rta)
|
|
236
215
|
self._init_db(db, table)
|
|
237
216
|
self.periodic = Periodic(self.periodic_cb, 1.0)
|
|
238
217
|
|
|
@@ -280,6 +259,37 @@ class Alarms:
|
|
|
280
259
|
for alarm in self.checking_alarms[:]:
|
|
281
260
|
alarm.check_duration(current_time_us)
|
|
282
261
|
|
|
262
|
+
def state_cb(self, alarm: Alarm, state: int):
|
|
263
|
+
"""Handle alarm state changes."""
|
|
264
|
+
if state == TIMING:
|
|
265
|
+
self.checking_alarms.append(alarm)
|
|
266
|
+
elif state == ALM:
|
|
267
|
+
if alarm in self.checking_alarms:
|
|
268
|
+
self.checking_alarms.remove(alarm)
|
|
269
|
+
self.in_alarm.append(alarm)
|
|
270
|
+
self.generate_alarm(alarm, ALM)
|
|
271
|
+
elif state == RTN:
|
|
272
|
+
if alarm in self.checking_alarms:
|
|
273
|
+
self.checking_alarms.remove(alarm)
|
|
274
|
+
if alarm in self.in_alarm:
|
|
275
|
+
self.in_alarm.remove(alarm)
|
|
276
|
+
self.generate_alarm(alarm, RTN)
|
|
277
|
+
|
|
278
|
+
def generate_alarm(self, alarm: Alarm, kind: int):
|
|
279
|
+
"""Generate alarm message."""
|
|
280
|
+
value = alarm.tag.value
|
|
281
|
+
time_us = alarm.tag.time_us
|
|
282
|
+
logging.warning(f'Alarm {alarm.alarm_id} {value} {KIND[kind]}')
|
|
283
|
+
self.rta_cb({
|
|
284
|
+
'action': 'ADD',
|
|
285
|
+
'date_ms': int(time_us / 1000),
|
|
286
|
+
'alarm_string': alarm.alarm_id,
|
|
287
|
+
'kind': kind,
|
|
288
|
+
'desc': f'{alarm.tag.desc} {value:.{alarm.tag.dp}f}'
|
|
289
|
+
f' {alarm.tag.units}',
|
|
290
|
+
'group': alarm.group
|
|
291
|
+
})
|
|
292
|
+
|
|
283
293
|
def rta_cb(self, request):
|
|
284
294
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
285
295
|
if 'action' not in request:
|
|
@@ -394,6 +404,61 @@ class Alarms:
|
|
|
394
404
|
elif request['action'] == 'IN ALARM':
|
|
395
405
|
self.rta.value = {'__rta_id__': request['__rta_id__'],
|
|
396
406
|
'data': {'in_alarm': list(self.in_alarm)}}
|
|
407
|
+
elif request['action'] == 'ENABLE':
|
|
408
|
+
time_us = int(time.time() * 1000000)
|
|
409
|
+
local_time = time.localtime(time_us / 1000000)
|
|
410
|
+
for alarm in self.alarms:
|
|
411
|
+
if alarm.alarm_id == request['alarm id']:
|
|
412
|
+
enable = request['enable']
|
|
413
|
+
if enable == 'Enable':
|
|
414
|
+
disabled_until_us = 0
|
|
415
|
+
else:
|
|
416
|
+
if enable == 'Disable until 8am':
|
|
417
|
+
target_hour = 8
|
|
418
|
+
target_day_offset = 0
|
|
419
|
+
next_offset = 1
|
|
420
|
+
elif enable == 'Disable until 4pm':
|
|
421
|
+
target_hour = 16
|
|
422
|
+
target_day_offset = 0
|
|
423
|
+
next_offset = 1
|
|
424
|
+
elif enable == 'Disable until Monday 8am':
|
|
425
|
+
target_hour = 8
|
|
426
|
+
target_day_offset = (0 - local_time.tm_wday) % 7
|
|
427
|
+
next_offset = 7
|
|
428
|
+
else:
|
|
429
|
+
disabled_until_us = 0
|
|
430
|
+
break
|
|
431
|
+
target_s = time.mktime((
|
|
432
|
+
local_time.tm_year, local_time.tm_mon,
|
|
433
|
+
local_time.tm_mday + target_day_offset,
|
|
434
|
+
target_hour, 0, 0, 0, 0, -1
|
|
435
|
+
))
|
|
436
|
+
if target_s * 1000000 <= time_us:
|
|
437
|
+
target_s = time.mktime((
|
|
438
|
+
local_time.tm_year, local_time.tm_mon,
|
|
439
|
+
local_time.tm_mday + next_offset,
|
|
440
|
+
target_hour, 0, 0, 0, 0, -1
|
|
441
|
+
))
|
|
442
|
+
disabled_until_us = int(target_s * 1000000)
|
|
443
|
+
alarm.disabled_until = disabled_until_us
|
|
444
|
+
ts = time.strftime(
|
|
445
|
+
"%Y-%m-%d %H:%M:%S",
|
|
446
|
+
time.localtime(disabled_until_us / 1000000)
|
|
447
|
+
)
|
|
448
|
+
if disabled_until_us == 0:
|
|
449
|
+
desc = 'Enable'
|
|
450
|
+
else:
|
|
451
|
+
desc = f'Disable until {ts}'
|
|
452
|
+
self.rta_cb({
|
|
453
|
+
'action': 'ADD',
|
|
454
|
+
'date_ms': int(time_us / 1000),
|
|
455
|
+
'alarm_string': alarm.alarm_id,
|
|
456
|
+
'kind': ACT,
|
|
457
|
+
'desc': desc,
|
|
458
|
+
'group': alarm.group
|
|
459
|
+
})
|
|
460
|
+
break
|
|
461
|
+
|
|
397
462
|
|
|
398
463
|
async def start(self):
|
|
399
464
|
"""Async startup."""
|
|
@@ -77,6 +77,10 @@ class BusClient:
|
|
|
77
77
|
jsonstr = json.dumps(request).encode()
|
|
78
78
|
size = len(jsonstr)
|
|
79
79
|
data = struct.pack(f'>B{size}s', pc.TYPE.JSON, jsonstr)
|
|
80
|
+
action = request.get("action", "unknown")
|
|
81
|
+
tag_id = self.tag_by_name[tagname].id
|
|
82
|
+
logging.info(f'{self.module}: RTA sending {tagname} {action} to'
|
|
83
|
+
f' tag_id {tag_id}')
|
|
80
84
|
self.write(pc.COMMAND.RTA, self.tag_by_name[tagname].id, time_us, data)
|
|
81
85
|
|
|
82
86
|
def write(self, command: pc.COMMAND, tag_id: int, time_us: int,
|
|
@@ -84,6 +88,12 @@ class BusClient:
|
|
|
84
88
|
"""Write a message."""
|
|
85
89
|
if data is None:
|
|
86
90
|
data = b''
|
|
91
|
+
try:
|
|
92
|
+
size_total = len(data)
|
|
93
|
+
except Exception:
|
|
94
|
+
size_total = 0
|
|
95
|
+
logging.info(f'{self.module}: write cmd={command} tag_id={tag_id} '
|
|
96
|
+
f'size_total={size_total}')
|
|
87
97
|
for i in range(0, len(data) + 1, pc.MAX_LEN):
|
|
88
98
|
snip = data[i:i+pc.MAX_LEN]
|
|
89
99
|
size = len(snip)
|
|
@@ -94,7 +104,9 @@ class BusClient:
|
|
|
94
104
|
except (asyncio.IncompleteReadError, ConnectionResetError):
|
|
95
105
|
self.read_task.cancel()
|
|
96
106
|
except AttributeError:
|
|
97
|
-
logging.warning('
|
|
107
|
+
logging.warning(f'{self.module}: write AttributeError '
|
|
108
|
+
f'cmd={command} '
|
|
109
|
+
f'tag_id={tag_id} size={size}')
|
|
98
110
|
|
|
99
111
|
def add_tag(self, tag: Tag):
|
|
100
112
|
"""Add the new tag and get the tag's bus ID."""
|
|
@@ -209,10 +221,13 @@ class BusClient:
|
|
|
209
221
|
data = struct.unpack_from(f'!{len(value) - 1}s', value, offset=1
|
|
210
222
|
)[0].decode()
|
|
211
223
|
data = json.loads(data)
|
|
224
|
+
action = data.get("action", "unknown")
|
|
225
|
+
logging.info(f'{self.module}: RTA received {tag.name} {action} '
|
|
226
|
+
f'from tag_id {tag_id}')
|
|
212
227
|
try:
|
|
213
228
|
self.rta_handlers[tag.name](data)
|
|
214
229
|
except KeyError:
|
|
215
|
-
logging.warning(f'unhandled RTA for {tag.name} {data}')
|
|
230
|
+
logging.warning(f'{self.module}: unhandled RTA for {tag.name} {data}')
|
|
216
231
|
else:
|
|
217
232
|
raise SystemExit(f'Invalid message {cmd}')
|
|
218
233
|
|
|
@@ -111,7 +111,8 @@ class BusConnection():
|
|
|
111
111
|
head = await self.reader.readexactly(14)
|
|
112
112
|
_, cmd, tag_id, size, time_us = unpack('!BBHHQ', head)
|
|
113
113
|
except (ConnectionResetError, asyncio.IncompleteReadError,
|
|
114
|
-
asyncio.CancelledError):
|
|
114
|
+
asyncio.CancelledError) as e:
|
|
115
|
+
logging.warning(f'{self.addr} read error: {e}')
|
|
115
116
|
break
|
|
116
117
|
# if the command packet indicates data, get that too
|
|
117
118
|
if size == 0:
|
|
@@ -121,7 +122,8 @@ class BusConnection():
|
|
|
121
122
|
payload = await self.reader.readexactly(size)
|
|
122
123
|
data = unpack(f'!{size}s', payload)[0]
|
|
123
124
|
except (ConnectionResetError, asyncio.IncompleteReadError,
|
|
124
|
-
asyncio.CancelledError):
|
|
125
|
+
asyncio.CancelledError) as e:
|
|
126
|
+
logging.warning(f'{self.addr} read payload error: {e}')
|
|
125
127
|
break
|
|
126
128
|
# if MAX_LEN then a continuation packet is required
|
|
127
129
|
if size == pc.MAX_LEN:
|
|
@@ -201,15 +203,21 @@ class BusServer:
|
|
|
201
203
|
try:
|
|
202
204
|
tag = BusTags._tag_by_id[tag_id]
|
|
203
205
|
except KeyError:
|
|
206
|
+
logging.warning(f'RTA KeyError {tag_id}')
|
|
204
207
|
self.connections[bus_id].write(
|
|
205
208
|
pc.COMMAND.ERR, tag_id, time_us,
|
|
206
209
|
f"RTA KeyError {tag_id}".encode())
|
|
210
|
+
return
|
|
207
211
|
try:
|
|
212
|
+
logging.info(f'RTA forwarding {tag.name} from_bus={tag.from_bus} '
|
|
213
|
+
f'to bus_id={tag.from_bus}')
|
|
208
214
|
self.connections[tag.from_bus].write(
|
|
209
215
|
pc.COMMAND.RTA, tag_id, tag.time_us, data)
|
|
210
216
|
except KeyError:
|
|
211
|
-
logging.warning(f'
|
|
217
|
+
logging.warning(f'RTA forwarding failed: busclient for '
|
|
218
|
+
f'{tag.name} (from_bus={tag.from_bus}) is gone')
|
|
212
219
|
except Exception as e:
|
|
220
|
+
logging.warning(f'RTA forwarding error {tag.name}: {e}')
|
|
213
221
|
self.connections[bus_id].write(
|
|
214
222
|
pc.COMMAND.ERR, tag_id, time_us,
|
|
215
223
|
f"RTA {tag_id} {e}".encode())
|
|
@@ -288,6 +296,8 @@ class BusServer:
|
|
|
288
296
|
def read_callback(self, command):
|
|
289
297
|
"""Process read messages, delete broken connections."""
|
|
290
298
|
bus_id, cmd, tag_id, time_us, data = command
|
|
299
|
+
logging.info(f'recv cmd={cmd} tag_id={tag_id} bus_id={bus_id} '
|
|
300
|
+
f'size={(0 if data is None else len(data))}')
|
|
291
301
|
if cmd is None:
|
|
292
302
|
# Clean up tag subscriptions before deleting it
|
|
293
303
|
for tag in BusTags._tag_by_id.values():
|
|
@@ -145,6 +145,7 @@ class Callout:
|
|
|
145
145
|
'groups': self.groups,
|
|
146
146
|
'escalation': self.escalation}
|
|
147
147
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
148
|
+
self.busclient.add_tag(self.rta)
|
|
148
149
|
self.periodic = Periodic(self.periodic_cb, 1.0)
|
|
149
150
|
|
|
150
151
|
def alarms_cb(self, alm_tag):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Interactive console."""
|
|
2
2
|
import asyncio
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
from pymscada.bus_client import BusClient
|
|
6
7
|
from pymscada.tag import Tag
|
|
@@ -123,6 +124,10 @@ class ConsoleWriter:
|
|
|
123
124
|
"""Init."""
|
|
124
125
|
self.edit = None
|
|
125
126
|
self.cursor = 0
|
|
127
|
+
# Make stdout non-blocking to avoid BlockingIOError in async context
|
|
128
|
+
import fcntl
|
|
129
|
+
flags = fcntl.fcntl(sys.stdout.fileno(), fcntl.F_GETFL)
|
|
130
|
+
fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
126
131
|
|
|
127
132
|
def write(self, data: bytes):
|
|
128
133
|
"""Stream writer, primarily for logging."""
|
|
@@ -132,22 +137,30 @@ class ConsoleWriter:
|
|
|
132
137
|
ln = EC.cr_clr + data + b'\r\n'
|
|
133
138
|
if self.edit is not None:
|
|
134
139
|
ln += EC.cr_clr + self.edit + EC.mv_left + cursor_str
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
try:
|
|
141
|
+
os.write(sys.stdout.fileno(), ln)
|
|
142
|
+
except BlockingIOError:
|
|
143
|
+
# If stdout buffer is full, skip this write to avoid blocking
|
|
144
|
+
pass
|
|
137
145
|
|
|
138
146
|
def edit_line(self, edit: bytes, cursor: int):
|
|
139
147
|
"""Update the edit line and cursor position."""
|
|
140
148
|
self.edit = edit
|
|
141
149
|
if self.edit is None:
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
try:
|
|
151
|
+
os.write(sys.stdout.fileno(), b'\r\n')
|
|
152
|
+
except BlockingIOError:
|
|
153
|
+
pass
|
|
144
154
|
return
|
|
145
155
|
self.cursor = cursor
|
|
146
156
|
ln = EC.cr_clr + self.edit + EC.mv_left
|
|
147
157
|
if self.cursor > 0:
|
|
148
158
|
ln += b'\x1b[' + str(self.cursor).encode() + b'C'
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
try:
|
|
160
|
+
os.write(sys.stdout.fileno(), ln)
|
|
161
|
+
except BlockingIOError:
|
|
162
|
+
# If stdout buffer is full, skip this write to avoid blocking
|
|
163
|
+
pass
|
|
151
164
|
|
|
152
165
|
|
|
153
166
|
class Console:
|
|
@@ -2,6 +2,7 @@ bus_ip: 127.0.0.1
|
|
|
2
2
|
bus_port: 1324
|
|
3
3
|
proxy:
|
|
4
4
|
api:
|
|
5
|
+
# tagnames are assigned by location_parameter, i.e. Murupara_Temp
|
|
5
6
|
api_key: ${MSCADA_OPENWEATHERMAP_API_KEY}
|
|
6
7
|
units: metric
|
|
7
8
|
locations:
|
|
@@ -13,13 +14,4 @@ api:
|
|
|
13
14
|
- Temp
|
|
14
15
|
- WindSpeed
|
|
15
16
|
- WindDir
|
|
16
|
-
- Rain
|
|
17
|
-
tags:
|
|
18
|
-
- Murupara_Temp
|
|
19
|
-
- Murupara_WindSpeed
|
|
20
|
-
- Murupara_WindDir
|
|
21
|
-
- Murupara_Rain
|
|
22
|
-
- Murupara_Temp_03
|
|
23
|
-
- Murupara_WindSpeed_03
|
|
24
|
-
- Murupara_WindDir_03
|
|
25
|
-
- Murupara_Rain_03
|
|
17
|
+
- Rain
|
|
@@ -2,8 +2,8 @@ bus_ip: 127.0.0.1
|
|
|
2
2
|
bus_port: 1324
|
|
3
3
|
proxy:
|
|
4
4
|
api:
|
|
5
|
-
url:
|
|
6
|
-
webid:
|
|
5
|
+
url: ${MSCADA_PI_URL}
|
|
6
|
+
webid: ${MSCADA_PI_WEBID}
|
|
7
7
|
averaging: 300
|
|
8
8
|
tags:
|
|
9
9
|
I_An_G1_MW:
|
|
@@ -2,7 +2,9 @@ bus_ip: 127.0.0.1
|
|
|
2
2
|
bus_port: 1324
|
|
3
3
|
proxy:
|
|
4
4
|
api:
|
|
5
|
-
|
|
5
|
+
# tagnames are assigned by gxp_list
|
|
6
|
+
# i.e. MAT1101, MAT1101_Realtime, MAT1101_Forecast
|
|
7
|
+
url: ${MSCADA_WITS_URL}
|
|
6
8
|
client_id: ${MSCADA_WITS_CLIENT_ID}
|
|
7
9
|
client_secret: ${MSCADA_WITS_CLIENT_SECRET}
|
|
8
10
|
gxp_list:
|
|
@@ -10,8 +12,4 @@ api:
|
|
|
10
12
|
- CYD2201
|
|
11
13
|
- BEN2201
|
|
12
14
|
back: 1
|
|
13
|
-
forward: 12
|
|
14
|
-
tags:
|
|
15
|
-
- MAT1101_RTD
|
|
16
|
-
- CYD2201_RTD
|
|
17
|
-
- BEN2201_RTD
|
|
15
|
+
forward: 12
|
|
@@ -33,6 +33,7 @@ class Files():
|
|
|
33
33
|
self.rta = Tag(rta_tag, dict)
|
|
34
34
|
self.rta.value = {}
|
|
35
35
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
36
|
+
self.busclient.add_tag(self.rta)
|
|
36
37
|
|
|
37
38
|
def rta_cb(self, request):
|
|
38
39
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
@@ -272,6 +272,7 @@ class History():
|
|
|
272
272
|
self.rta = Tag(rta_tag, bytes)
|
|
273
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
|
+
self.busclient.add_tag(self.rta)
|
|
275
276
|
|
|
276
277
|
def rta_cb(self, request: Request):
|
|
277
278
|
"""Respond to bus requests for data to publish on rta."""
|