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.

Files changed (105) hide show
  1. {pymscada-0.2.5/src/pymscada.egg-info → pymscada-0.2.6b0}/PKG-INFO +1 -1
  2. {pymscada-0.2.5 → pymscada-0.2.6b0}/pyproject.toml +1 -1
  3. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/__init__.py +2 -0
  4. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/alarms.py +101 -37
  5. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/console.py +19 -6
  6. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/files.yaml +3 -2
  7. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/openweather.yaml +2 -10
  8. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/piapi.yaml +2 -2
  9. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/witsapi.yaml +4 -6
  10. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/opnotes.py +15 -8
  11. {pymscada-0.2.5 → pymscada-0.2.6b0/src/pymscada.egg-info}/PKG-INFO +1 -1
  12. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_callout.py +1 -1
  13. {pymscada-0.2.5 → pymscada-0.2.6b0}/LICENSE +0 -0
  14. {pymscada-0.2.5 → pymscada-0.2.6b0}/MANIFEST.in +0 -0
  15. {pymscada-0.2.5 → pymscada-0.2.6b0}/README.md +0 -0
  16. {pymscada-0.2.5 → pymscada-0.2.6b0}/setup.cfg +0 -0
  17. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/__main__.py +0 -0
  18. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/bus_client.py +0 -0
  19. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/bus_server.py +0 -0
  20. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/callout.py +0 -0
  21. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/checkout.py +0 -0
  22. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/config.py +0 -0
  23. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/README.md +0 -0
  24. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/__init__.py +0 -0
  25. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
  26. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/accuweather.yaml +0 -0
  27. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/alarms.yaml +0 -0
  28. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/bus.yaml +0 -0
  29. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/callout.yaml +0 -0
  30. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/history.yaml +0 -0
  31. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/logixclient.yaml +0 -0
  32. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/modbus_plc.py +0 -0
  33. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/modbusclient.yaml +0 -0
  34. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/modbusserver.yaml +0 -0
  35. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/opnotes.yaml +0 -0
  36. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/ping.yaml +0 -0
  37. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-alarms.service +0 -0
  38. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-bus.service +0 -0
  39. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-callout.service +0 -0
  40. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  41. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-files.service +0 -0
  42. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-history.service +0 -0
  43. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
  44. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  45. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  46. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
  47. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
  48. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-ping.service +0 -0
  49. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-sms.service +0 -0
  50. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
  51. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
  52. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-opnotes.service +0 -0
  53. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  54. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/sms.yaml +0 -0
  55. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/snmpclient.yaml +0 -0
  56. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/tags.yaml +0 -0
  57. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/demo/wwwserver.yaml +0 -0
  58. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/files.py +0 -0
  59. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/history.py +0 -0
  60. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/__init__.py +0 -0
  61. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/accuweather.py +0 -0
  62. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/logix_client.py +0 -0
  63. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/logix_map.py +0 -0
  64. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/modbus_client.py +0 -0
  65. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/modbus_map.py +0 -0
  66. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/modbus_server.py +0 -0
  67. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/openweather.py +0 -0
  68. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/piapi.py +0 -0
  69. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/ping_client.py +0 -0
  70. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/ping_map.py +0 -0
  71. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/sms.py +0 -0
  72. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/snmp_client.py +0 -0
  73. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/snmp_map.py +0 -0
  74. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/witsapi.py +0 -0
  75. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
  76. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/main.py +0 -0
  77. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/misc.py +0 -0
  78. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/module_config.py +0 -0
  79. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/__init__.py +0 -0
  80. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
  81. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/one.pdf +0 -0
  82. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/pdf/two.pdf +0 -0
  83. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/periodic.py +0 -0
  84. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/protocol_constants.py +0 -0
  85. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/samplers.py +0 -0
  86. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tag.py +0 -0
  87. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tools/get_history.py +0 -0
  88. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tools/snmp_client2.py +0 -0
  89. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/tools/walk.py +0 -0
  90. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada/www_server.py +0 -0
  91. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/SOURCES.txt +0 -0
  92. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/dependency_links.txt +0 -0
  93. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/entry_points.txt +0 -0
  94. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/requires.txt +0 -0
  95. {pymscada-0.2.5 → pymscada-0.2.6b0}/src/pymscada.egg-info/top_level.txt +0 -0
  96. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_alarms.py +0 -0
  97. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_bus_server.py +0 -0
  98. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_config.py +0 -0
  99. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_history.py +0 -0
  100. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_misc.py +0 -0
  101. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_opnotes.py +0 -0
  102. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_periodic.py +0 -0
  103. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_samplers.py +0 -0
  104. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_sms.py +0 -0
  105. {pymscada-0.2.5 → pymscada-0.2.6b0}/tests/test_tag.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymscada
3
- Version: 0.2.5
3
+ Version: 0.2.6b0
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pymscada"
3
- version = "0.2.5"
3
+ version = "0.2.6b0"
4
4
  description = "Shared tag value SCADA with python backup and Angular UI"
5
5
  authors = [
6
6
  {name = "Jamie Walton", email = "jamie@walton.net.nz"},
@@ -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['operator'] = tokens[0]
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
- Generates the ALM and RTN messages for Alarms to publish via rta_tag.
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
- rta_cb, alarms) -> None:
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.rta_cb = rta_cb
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.checking = False
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
- self.in_alarm = new_in_alarm
146
- if self.in_alarm:
143
+ if new_in_alarm:
147
144
  if self.alarm['for'] > 0:
148
- if not self.checking:
149
- self.checking = True
150
- self.alarms.checking_alarms.append(self)
145
+ self.state_cb(self, TIMING)
151
146
  else:
152
- self.generate_alarm(ALM, time_us, value)
147
+ self.state_cb(self, ALM)
153
148
  else:
154
- if self.checking:
155
- self.checking = False
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.generate_alarm(ALM, current_time_us, self.tag.value)
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.rta_cb,
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
- sys.stdout.buffer.write(ln)
136
- sys.stdout.flush()
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
- sys.stdout.buffer.write(b'\r\n')
143
- sys.stdout.flush()
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
- sys.stdout.buffer.write(ln)
150
- sys.stdout.flush()
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:
@@ -3,8 +3,9 @@ bus_port: 1324
3
3
  path: __HOME__
4
4
  files:
5
5
  - path: pdf/one.pdf
6
- desc: PDF
6
+ desc: One PDF
7
7
  - path: pdf/two.pdf
8
- desc: PDF
8
+ desc: Two PDF
9
+ - path: pdf/*.pdf
9
10
  - path: config/*.yaml
10
11
  - path: history/*.dat
@@ -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: 'https://192.168.15.1/'
6
- webid: F1Em9DA80Xftdkec1gdWFtX7NAm1eiSAyV8BG1mAAMKQIjRQUEdQSVxQSU9ORUVSXFdFQkFQSVxNT0JJTEVTQ0FEQQ
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
- url: 'https://api.electricityinfo.co.nz'
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'add {request}')
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
- self.rta.value = {
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'modify {request}')
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'delete {request}')
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'history {request}')
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'bulk history {request}')
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
- self.rta.value = {'__rta_id__': request['__rta_id__'],
184
- 'data': results}
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymscada
3
- Version: 0.2.5
3
+ Version: 0.2.6b0
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  """Test Callout."""
2
2
  import pytest
3
- from pymscada.callout import Callout, alarm_in_callee_group, ALM
3
+ from pymscada.callout import Callout, alarm_in_group, ALM
4
4
 
5
5
 
6
6
  BUS_ID = 123
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes