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.
Files changed (105) hide show
  1. {pymscada-0.2.5/src/pymscada.egg-info → pymscada-0.2.6b4}/PKG-INFO +1 -1
  2. {pymscada-0.2.5 → pymscada-0.2.6b4}/pyproject.toml +1 -1
  3. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/__init__.py +2 -0
  4. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/alarms.py +102 -37
  5. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/bus_client.py +17 -2
  6. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/bus_server.py +13 -3
  7. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/callout.py +1 -0
  8. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/console.py +19 -6
  9. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/files.yaml +3 -2
  10. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/openweather.yaml +2 -10
  11. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/piapi.yaml +2 -2
  12. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/witsapi.yaml +4 -6
  13. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/files.py +1 -0
  14. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/history.py +1 -0
  15. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_client.py +189 -21
  16. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_map.py +17 -2
  17. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/opnotes.py +16 -8
  18. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/www_server.py +1 -0
  19. {pymscada-0.2.5 → pymscada-0.2.6b4/src/pymscada.egg-info}/PKG-INFO +1 -1
  20. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_callout.py +1 -1
  21. {pymscada-0.2.5 → pymscada-0.2.6b4}/LICENSE +0 -0
  22. {pymscada-0.2.5 → pymscada-0.2.6b4}/MANIFEST.in +0 -0
  23. {pymscada-0.2.5 → pymscada-0.2.6b4}/README.md +0 -0
  24. {pymscada-0.2.5 → pymscada-0.2.6b4}/setup.cfg +0 -0
  25. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/__main__.py +0 -0
  26. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/checkout.py +0 -0
  27. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/config.py +0 -0
  28. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/README.md +0 -0
  29. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/__init__.py +0 -0
  30. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
  31. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/accuweather.yaml +0 -0
  32. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/alarms.yaml +0 -0
  33. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/bus.yaml +0 -0
  34. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/callout.yaml +0 -0
  35. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/history.yaml +0 -0
  36. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/logixclient.yaml +0 -0
  37. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/modbus_plc.py +0 -0
  38. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/modbusclient.yaml +0 -0
  39. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/modbusserver.yaml +0 -0
  40. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/opnotes.yaml +0 -0
  41. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/ping.yaml +0 -0
  42. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-alarms.service +0 -0
  43. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-bus.service +0 -0
  44. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-callout.service +0 -0
  45. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  46. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-files.service +0 -0
  47. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-history.service +0 -0
  48. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
  49. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  50. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  51. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
  52. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-piapi.service +0 -0
  53. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-ping.service +0 -0
  54. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-sms.service +0 -0
  55. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
  56. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-io-witsapi.service +0 -0
  57. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-opnotes.service +0 -0
  58. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  59. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/sms.yaml +0 -0
  60. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/snmpclient.yaml +0 -0
  61. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/tags.yaml +0 -0
  62. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/demo/wwwserver.yaml +0 -0
  63. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/__init__.py +0 -0
  64. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/accuweather.py +0 -0
  65. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_client.py +0 -0
  66. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/logix_map.py +0 -0
  67. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/modbus_server.py +0 -0
  68. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/openweather.py +0 -0
  69. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/piapi.py +0 -0
  70. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_client.py +0 -0
  71. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/ping_map.py +0 -0
  72. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/sms.py +0 -0
  73. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_client.py +0 -0
  74. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/snmp_map.py +0 -0
  75. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi.py +0 -0
  76. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/iodrivers/witsapi_POC.py +0 -0
  77. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/main.py +0 -0
  78. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/misc.py +0 -0
  79. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/module_config.py +0 -0
  80. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/__init__.py +0 -0
  81. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
  82. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/one.pdf +0 -0
  83. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/pdf/two.pdf +0 -0
  84. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/periodic.py +0 -0
  85. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/protocol_constants.py +0 -0
  86. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/samplers.py +0 -0
  87. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tag.py +0 -0
  88. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tools/get_history.py +0 -0
  89. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tools/snmp_client2.py +0 -0
  90. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada/tools/walk.py +0 -0
  91. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/SOURCES.txt +0 -0
  92. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/dependency_links.txt +0 -0
  93. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/entry_points.txt +0 -0
  94. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/requires.txt +0 -0
  95. {pymscada-0.2.5 → pymscada-0.2.6b4}/src/pymscada.egg-info/top_level.txt +0 -0
  96. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_alarms.py +0 -0
  97. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_bus_server.py +0 -0
  98. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_config.py +0 -0
  99. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_history.py +0 -0
  100. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_misc.py +0 -0
  101. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_opnotes.py +0 -0
  102. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_periodic.py +0 -0
  103. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_samplers.py +0 -0
  104. {pymscada-0.2.5 → pymscada-0.2.6b4}/tests/test_sms.py +0 -0
  105. {pymscada-0.2.5 → pymscada-0.2.6b4}/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.6b4
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.6b4"
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,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.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)
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('Attribute Error, TO DO, fix in test')
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'likely busclient for {tag.name} is gone')
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
- 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
@@ -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."""