pymscada 0.2.5__tar.gz → 0.2.6b0__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.6b0}/PKG-INFO +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b0}/pyproject.toml +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/__init__.py +2 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/alarms.py +101 -37
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/console.py +19 -6
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/files.yaml +3 -2
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/openweather.yaml +2 -10
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/piapi.yaml +2 -2
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/witsapi.yaml +4 -6
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/opnotes.py +15 -8
- {pymscada-0.2.5 → pymscada-0.2.6b0/src/pymscada.egg-info}/PKG-INFO +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_callout.py +1 -1
- {pymscada-0.2.5 → pymscada-0.2.6b0}/LICENSE +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/MANIFEST.in +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/README.md +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/setup.cfg +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/__main__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/bus_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/bus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/callout.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/checkout.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/accuweather.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/alarms.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/callout.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/opnotes.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/ping.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-alarms.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-callout.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-history.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-ping.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-sms.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-opnotes.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/sms.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/tags.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/wwwserver.yaml +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/files.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/accuweather.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/modbus_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/modbus_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/openweather.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/piapi.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/ping_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/ping_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/sms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/snmp_client.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/witsapi.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/main.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/misc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/module_config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/periodic.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/samplers.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tag.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tools/get_history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tools/snmp_client2.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/www_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/SOURCES.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/dependency_links.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/entry_points.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/requires.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/top_level.txt +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_alarms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_bus_server.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_config.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_history.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_misc.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_opnotes.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_periodic.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_samplers.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_sms.py +0 -0
- {pymscada-0.2.5 → pymscada-0.2.6b0}/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,14 +198,14 @@ 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)
|
|
@@ -280,6 +258,37 @@ class Alarms:
|
|
|
280
258
|
for alarm in self.checking_alarms[:]:
|
|
281
259
|
alarm.check_duration(current_time_us)
|
|
282
260
|
|
|
261
|
+
def state_cb(self, alarm: Alarm, state: int):
|
|
262
|
+
"""Handle alarm state changes."""
|
|
263
|
+
if state == TIMING:
|
|
264
|
+
self.checking_alarms.append(alarm)
|
|
265
|
+
elif state == ALM:
|
|
266
|
+
if alarm in self.checking_alarms:
|
|
267
|
+
self.checking_alarms.remove(alarm)
|
|
268
|
+
self.in_alarm.append(alarm)
|
|
269
|
+
self.generate_alarm(alarm, ALM)
|
|
270
|
+
elif state == RTN:
|
|
271
|
+
if alarm in self.checking_alarms:
|
|
272
|
+
self.checking_alarms.remove(alarm)
|
|
273
|
+
if alarm in self.in_alarm:
|
|
274
|
+
self.in_alarm.remove(alarm)
|
|
275
|
+
self.generate_alarm(alarm, RTN)
|
|
276
|
+
|
|
277
|
+
def generate_alarm(self, alarm: Alarm, kind: int):
|
|
278
|
+
"""Generate alarm message."""
|
|
279
|
+
value = alarm.tag.value
|
|
280
|
+
time_us = alarm.tag.time_us
|
|
281
|
+
logging.warning(f'Alarm {alarm.alarm_id} {value} {KIND[kind]}')
|
|
282
|
+
self.rta_cb({
|
|
283
|
+
'action': 'ADD',
|
|
284
|
+
'date_ms': int(time_us / 1000),
|
|
285
|
+
'alarm_string': alarm.alarm_id,
|
|
286
|
+
'kind': kind,
|
|
287
|
+
'desc': f'{alarm.tag.desc} {value:.{alarm.tag.dp}f}'
|
|
288
|
+
f' {alarm.tag.units}',
|
|
289
|
+
'group': alarm.group
|
|
290
|
+
})
|
|
291
|
+
|
|
283
292
|
def rta_cb(self, request):
|
|
284
293
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
285
294
|
if 'action' not in request:
|
|
@@ -394,6 +403,61 @@ class Alarms:
|
|
|
394
403
|
elif request['action'] == 'IN ALARM':
|
|
395
404
|
self.rta.value = {'__rta_id__': request['__rta_id__'],
|
|
396
405
|
'data': {'in_alarm': list(self.in_alarm)}}
|
|
406
|
+
elif request['action'] == 'ENABLE':
|
|
407
|
+
time_us = int(time.time() * 1000000)
|
|
408
|
+
local_time = time.localtime(time_us / 1000000)
|
|
409
|
+
for alarm in self.alarms:
|
|
410
|
+
if alarm.alarm_id == request['alarm id']:
|
|
411
|
+
enable = request['enable']
|
|
412
|
+
if enable == 'Enable':
|
|
413
|
+
disabled_until_us = 0
|
|
414
|
+
else:
|
|
415
|
+
if enable == 'Disable until 8am':
|
|
416
|
+
target_hour = 8
|
|
417
|
+
target_day_offset = 0
|
|
418
|
+
next_offset = 1
|
|
419
|
+
elif enable == 'Disable until 4pm':
|
|
420
|
+
target_hour = 16
|
|
421
|
+
target_day_offset = 0
|
|
422
|
+
next_offset = 1
|
|
423
|
+
elif enable == 'Disable until Monday 8am':
|
|
424
|
+
target_hour = 8
|
|
425
|
+
target_day_offset = (0 - local_time.tm_wday) % 7
|
|
426
|
+
next_offset = 7
|
|
427
|
+
else:
|
|
428
|
+
disabled_until_us = 0
|
|
429
|
+
break
|
|
430
|
+
target_s = time.mktime((
|
|
431
|
+
local_time.tm_year, local_time.tm_mon,
|
|
432
|
+
local_time.tm_mday + target_day_offset,
|
|
433
|
+
target_hour, 0, 0, 0, 0, -1
|
|
434
|
+
))
|
|
435
|
+
if target_s * 1000000 <= time_us:
|
|
436
|
+
target_s = time.mktime((
|
|
437
|
+
local_time.tm_year, local_time.tm_mon,
|
|
438
|
+
local_time.tm_mday + next_offset,
|
|
439
|
+
target_hour, 0, 0, 0, 0, -1
|
|
440
|
+
))
|
|
441
|
+
disabled_until_us = int(target_s * 1000000)
|
|
442
|
+
alarm.disabled_until = disabled_until_us
|
|
443
|
+
ts = time.strftime(
|
|
444
|
+
"%Y-%m-%d %H:%M:%S",
|
|
445
|
+
time.localtime(disabled_until_us / 1000000)
|
|
446
|
+
)
|
|
447
|
+
if disabled_until_us == 0:
|
|
448
|
+
desc = 'Enable'
|
|
449
|
+
else:
|
|
450
|
+
desc = f'Disable until {ts}'
|
|
451
|
+
self.rta_cb({
|
|
452
|
+
'action': 'ADD',
|
|
453
|
+
'date_ms': int(time_us / 1000),
|
|
454
|
+
'alarm_string': alarm.alarm_id,
|
|
455
|
+
'kind': ACT,
|
|
456
|
+
'desc': desc,
|
|
457
|
+
'group': alarm.group
|
|
458
|
+
})
|
|
459
|
+
break
|
|
460
|
+
|
|
397
461
|
|
|
398
462
|
async def start(self):
|
|
399
463
|
"""Async startup."""
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -101,11 +102,14 @@ class OpNotes:
|
|
|
101
102
|
|
|
102
103
|
def rta_cb(self, request):
|
|
103
104
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
105
|
+
local_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
106
|
+
logging.info(f'[{local_time}] RTA callback received: {request}')
|
|
107
|
+
|
|
104
108
|
if 'action' not in request:
|
|
105
109
|
logging.warning(f'rta_cb malformed {request}')
|
|
106
110
|
elif request['action'] == 'ADD':
|
|
107
111
|
try:
|
|
108
|
-
logging.info(f'
|
|
112
|
+
logging.info(f'[{local_time}] ADD request: {request}')
|
|
109
113
|
with self.connection:
|
|
110
114
|
self.cursor.execute(
|
|
111
115
|
f'INSERT INTO {self.table} '
|
|
@@ -114,7 +118,7 @@ class OpNotes:
|
|
|
114
118
|
'RETURNING *;',
|
|
115
119
|
request)
|
|
116
120
|
res = self.cursor.fetchone()
|
|
117
|
-
|
|
121
|
+
response = {
|
|
118
122
|
'__rta_id__': 0,
|
|
119
123
|
'id': res[0],
|
|
120
124
|
'date_ms': res[1],
|
|
@@ -123,11 +127,13 @@ class OpNotes:
|
|
|
123
127
|
'note': res[4],
|
|
124
128
|
'abnormal': res[5]
|
|
125
129
|
}
|
|
130
|
+
self.rta.value = response
|
|
131
|
+
logging.info(f'[{local_time}] ADD response sent: {response}')
|
|
126
132
|
except sqlite3.IntegrityError as error:
|
|
127
133
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
128
134
|
elif request['action'] == 'MODIFY':
|
|
129
135
|
try:
|
|
130
|
-
logging.info(f'
|
|
136
|
+
logging.info(f'[{local_time}] MODIFY request: {request}')
|
|
131
137
|
with self.connection:
|
|
132
138
|
self.cursor.execute(
|
|
133
139
|
f'REPLACE INTO {self.table} VALUES(:id, :date_ms, '
|
|
@@ -146,7 +152,7 @@ class OpNotes:
|
|
|
146
152
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
147
153
|
elif request['action'] == 'DELETE':
|
|
148
154
|
try:
|
|
149
|
-
logging.info(f'
|
|
155
|
+
logging.info(f'[{local_time}] DELETE request: {request}')
|
|
150
156
|
with self.connection:
|
|
151
157
|
self.cursor.execute(
|
|
152
158
|
f'DELETE FROM {self.table} WHERE id = :id;', request)
|
|
@@ -155,7 +161,7 @@ class OpNotes:
|
|
|
155
161
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
156
162
|
elif request['action'] == 'HISTORY':
|
|
157
163
|
try:
|
|
158
|
-
logging.info(f'
|
|
164
|
+
logging.info(f'[{local_time}] HISTORY request: {request}')
|
|
159
165
|
with self.connection:
|
|
160
166
|
self.cursor.execute(
|
|
161
167
|
f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
|
|
@@ -174,14 +180,15 @@ class OpNotes:
|
|
|
174
180
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
175
181
|
elif request['action'] == 'BULK HISTORY':
|
|
176
182
|
try:
|
|
177
|
-
logging.info(f'
|
|
183
|
+
logging.info(f'[{local_time}] BULK HISTORY request: {request}')
|
|
178
184
|
with self.connection:
|
|
179
185
|
self.cursor.execute(
|
|
180
186
|
f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
|
|
181
187
|
'ORDER BY -date_ms;', request)
|
|
182
188
|
results = list(self.cursor.fetchall())
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
response = {'__rta_id__': request['__rta_id__'], 'data': results}
|
|
190
|
+
self.rta.value = response
|
|
191
|
+
logging.info(f'[{local_time}] BULK HISTORY response sent: {len(results)} records for rta_id {request["__rta_id__"]}')
|
|
185
192
|
except sqlite3.IntegrityError as error:
|
|
186
193
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
187
194
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|