pymscada 0.2.5__tar.gz → 0.2.6b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pymscada might be problematic. Click here for more details.
- {pymscada-0.2.5/src/pymscada.egg-info → pymscada-0.2.6b1}/PKG-INFO +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b1}/pyproject.toml +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/__init__.py +2 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/alarms.py +102 -37
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/bus_client.py +7 -1
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/bus_server.py +7 -1
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/callout.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/console.py +19 -6
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/files.yaml +3 -2
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/openweather.yaml +2 -10
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/piapi.yaml +2 -2
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/witsapi.yaml +4 -6
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/files.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/history.py +1 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/opnotes.py +16 -8
- {pymscada-0.2.5 → pymscada-0.2.6b1/src/pymscada.egg-info}/PKG-INFO +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_callout.py +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b1}/LICENSE +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/MANIFEST.in +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/README.md +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/setup.cfg +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/__main__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/checkout.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/accuweather.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/alarms.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/callout.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/opnotes.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/ping.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-alarms.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-callout.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-history.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-ping.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-sms.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-opnotes.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/sms.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/tags.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/demo/wwwserver.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/accuweather.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/modbus_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/modbus_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/openweather.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/piapi.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/ping_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/ping_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/sms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/snmp_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/witsapi.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/main.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/misc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/module_config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/periodic.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/samplers.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/tag.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/tools/get_history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/tools/snmp_client2.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada/www_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada.egg-info/SOURCES.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada.egg-info/dependency_links.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada.egg-info/entry_points.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada.egg-info/requires.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/src/pymscada.egg-info/top_level.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_alarms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_bus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_misc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_opnotes.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_periodic.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_samplers.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/tests/test_sms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b1}/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,9 @@ 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 tag_id {tag_id}')
|
|
80
83
|
self.write(pc.COMMAND.RTA, self.tag_by_name[tagname].id, time_us, data)
|
|
81
84
|
|
|
82
85
|
def write(self, command: pc.COMMAND, tag_id: int, time_us: int,
|
|
@@ -209,10 +212,13 @@ class BusClient:
|
|
|
209
212
|
data = struct.unpack_from(f'!{len(value) - 1}s', value, offset=1
|
|
210
213
|
)[0].decode()
|
|
211
214
|
data = json.loads(data)
|
|
215
|
+
action = data.get("action", "unknown")
|
|
216
|
+
logging.info(f'{self.module}: RTA received {tag.name} {action} '
|
|
217
|
+
f'from tag_id {tag_id}')
|
|
212
218
|
try:
|
|
213
219
|
self.rta_handlers[tag.name](data)
|
|
214
220
|
except KeyError:
|
|
215
|
-
logging.warning(f'unhandled RTA for {tag.name} {data}')
|
|
221
|
+
logging.warning(f'{self.module}: unhandled RTA for {tag.name} {data}')
|
|
216
222
|
else:
|
|
217
223
|
raise SystemExit(f'Invalid message {cmd}')
|
|
218
224
|
|
|
@@ -201,15 +201,21 @@ class BusServer:
|
|
|
201
201
|
try:
|
|
202
202
|
tag = BusTags._tag_by_id[tag_id]
|
|
203
203
|
except KeyError:
|
|
204
|
+
logging.warning(f'RTA KeyError {tag_id}')
|
|
204
205
|
self.connections[bus_id].write(
|
|
205
206
|
pc.COMMAND.ERR, tag_id, time_us,
|
|
206
207
|
f"RTA KeyError {tag_id}".encode())
|
|
208
|
+
return
|
|
207
209
|
try:
|
|
210
|
+
logging.info(f'RTA forwarding {tag.name} from_bus={tag.from_bus} '
|
|
211
|
+
f'to bus_id={tag.from_bus}')
|
|
208
212
|
self.connections[tag.from_bus].write(
|
|
209
213
|
pc.COMMAND.RTA, tag_id, tag.time_us, data)
|
|
210
214
|
except KeyError:
|
|
211
|
-
logging.warning(f'
|
|
215
|
+
logging.warning(f'RTA forwarding failed: busclient for '
|
|
216
|
+
f'{tag.name} (from_bus={tag.from_bus}) is gone')
|
|
212
217
|
except Exception as e:
|
|
218
|
+
logging.warning(f'RTA forwarding error {tag.name}: {e}')
|
|
213
219
|
self.connections[bus_id].write(
|
|
214
220
|
pc.COMMAND.ERR, tag_id, time_us,
|
|
215
221
|
f"RTA {tag_id} {e}".encode())
|
|
@@ -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."""
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import logging
|
|
3
3
|
import sqlite3 # note that sqlite3 has blocking calls
|
|
4
4
|
import socket
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from pymscada.bus_client import BusClient
|
|
6
7
|
from pymscada.tag import Tag
|
|
7
8
|
|
|
@@ -54,6 +55,7 @@ class OpNotes:
|
|
|
54
55
|
self.rta = Tag(rta_tag, dict)
|
|
55
56
|
self.rta.value = {'__rta_id__': 0}
|
|
56
57
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
58
|
+
self.busclient.add_tag(self.rta)
|
|
57
59
|
|
|
58
60
|
def _init_table(self):
|
|
59
61
|
"""Initialize or upgrade the database table schema."""
|
|
@@ -101,11 +103,14 @@ class OpNotes:
|
|
|
101
103
|
|
|
102
104
|
def rta_cb(self, request):
|
|
103
105
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
106
|
+
local_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
107
|
+
logging.info(f'[{local_time}] RTA callback received: {request}')
|
|
108
|
+
|
|
104
109
|
if 'action' not in request:
|
|
105
110
|
logging.warning(f'rta_cb malformed {request}')
|
|
106
111
|
elif request['action'] == 'ADD':
|
|
107
112
|
try:
|
|
108
|
-
logging.info(f'
|
|
113
|
+
logging.info(f'[{local_time}] ADD request: {request}')
|
|
109
114
|
with self.connection:
|
|
110
115
|
self.cursor.execute(
|
|
111
116
|
f'INSERT INTO {self.table} '
|
|
@@ -114,7 +119,7 @@ class OpNotes:
|
|
|
114
119
|
'RETURNING *;',
|
|
115
120
|
request)
|
|
116
121
|
res = self.cursor.fetchone()
|
|
117
|
-
|
|
122
|
+
response = {
|
|
118
123
|
'__rta_id__': 0,
|
|
119
124
|
'id': res[0],
|
|
120
125
|
'date_ms': res[1],
|
|
@@ -123,11 +128,13 @@ class OpNotes:
|
|
|
123
128
|
'note': res[4],
|
|
124
129
|
'abnormal': res[5]
|
|
125
130
|
}
|
|
131
|
+
self.rta.value = response
|
|
132
|
+
logging.info(f'[{local_time}] ADD response sent: {response}')
|
|
126
133
|
except sqlite3.IntegrityError as error:
|
|
127
134
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
128
135
|
elif request['action'] == 'MODIFY':
|
|
129
136
|
try:
|
|
130
|
-
logging.info(f'
|
|
137
|
+
logging.info(f'[{local_time}] MODIFY request: {request}')
|
|
131
138
|
with self.connection:
|
|
132
139
|
self.cursor.execute(
|
|
133
140
|
f'REPLACE INTO {self.table} VALUES(:id, :date_ms, '
|
|
@@ -146,7 +153,7 @@ class OpNotes:
|
|
|
146
153
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
147
154
|
elif request['action'] == 'DELETE':
|
|
148
155
|
try:
|
|
149
|
-
logging.info(f'
|
|
156
|
+
logging.info(f'[{local_time}] DELETE request: {request}')
|
|
150
157
|
with self.connection:
|
|
151
158
|
self.cursor.execute(
|
|
152
159
|
f'DELETE FROM {self.table} WHERE id = :id;', request)
|
|
@@ -155,7 +162,7 @@ class OpNotes:
|
|
|
155
162
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
156
163
|
elif request['action'] == 'HISTORY':
|
|
157
164
|
try:
|
|
158
|
-
logging.info(f'
|
|
165
|
+
logging.info(f'[{local_time}] HISTORY request: {request}')
|
|
159
166
|
with self.connection:
|
|
160
167
|
self.cursor.execute(
|
|
161
168
|
f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
|
|
@@ -174,14 +181,15 @@ class OpNotes:
|
|
|
174
181
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
175
182
|
elif request['action'] == 'BULK HISTORY':
|
|
176
183
|
try:
|
|
177
|
-
logging.info(f'
|
|
184
|
+
logging.info(f'[{local_time}] BULK HISTORY request: {request}')
|
|
178
185
|
with self.connection:
|
|
179
186
|
self.cursor.execute(
|
|
180
187
|
f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
|
|
181
188
|
'ORDER BY -date_ms;', request)
|
|
182
189
|
results = list(self.cursor.fetchall())
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
response = {'__rta_id__': request['__rta_id__'], 'data': results}
|
|
191
|
+
self.rta.value = response
|
|
192
|
+
logging.info(f'[{local_time}] BULK HISTORY response sent: {len(results)} records for rta_id {request["__rta_id__"]}')
|
|
185
193
|
except sqlite3.IntegrityError as error:
|
|
186
194
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
187
195
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|