pymscada 0.2.0rc4__tar.gz → 0.2.0rc6__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.0rc4/src/pymscada.egg-info → pymscada-0.2.0rc6}/PKG-INFO +3 -2
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/pyproject.toml +1 -1
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/alarms.py +166 -170
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/console.py +4 -3
- pymscada-0.2.0rc6/src/pymscada/demo/wits.yaml +17 -0
- pymscada-0.2.0rc6/src/pymscada/iodrivers/witsapi.py +217 -0
- pymscada-0.2.0rc6/src/pymscada/iodrivers/witsapi_POC.py +246 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/module_config.py +12 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6/src/pymscada.egg-info}/PKG-INFO +3 -2
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada.egg-info/SOURCES.txt +3 -1
- pymscada-0.2.0rc6/tests/test_alarms.py +198 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_history.py +55 -0
- pymscada-0.2.0rc4/tests/test_alarms.py +0 -128
- pymscada-0.2.0rc4/tests/test_openweather.py +0 -46
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/LICENSE +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/MANIFEST.in +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/README.md +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/setup.cfg +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/__init__.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/__main__.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/bus_client.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/bus_server.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/checkout.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/config.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/accuweather.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/alarms.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/files.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/openweather.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/opnotes.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/ping.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-alarms.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-history.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-io-ping.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-opnotes.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/tags.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/demo/wwwserver.yaml +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/files.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/history.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/accuweather.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/modbus_client.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/modbus_map.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/openweather.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/ping_client.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/ping_map.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/snmp_client.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/main.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/misc.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/opnotes.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/periodic.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/samplers.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/tag.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/tools/snmp_client2.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/validate.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada/www_server.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada.egg-info/dependency_links.txt +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada.egg-info/entry_points.txt +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada.egg-info/requires.txt +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/src/pymscada.egg-info/top_level.txt +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_bus_server.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_config.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_misc.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_opnotes.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_periodic.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_samplers.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_tag.py +0 -0
- {pymscada-0.2.0rc4 → pymscada-0.2.0rc6}/tests/test_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pymscada
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0rc6
|
|
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
|
|
@@ -21,6 +21,7 @@ Requires-Dist: pymscada-html==0.2.0rc4
|
|
|
21
21
|
Requires-Dist: cerberus>=1.3.5
|
|
22
22
|
Requires-Dist: pycomm3>=1.2.14
|
|
23
23
|
Requires-Dist: pysnmplib>=5.0.24
|
|
24
|
+
Dynamic: license-file
|
|
24
25
|
|
|
25
26
|
# pymscada
|
|
26
27
|
#### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
|
|
@@ -3,8 +3,8 @@ import logging
|
|
|
3
3
|
import sqlite3 # note that sqlite3 has blocking calls
|
|
4
4
|
import socket
|
|
5
5
|
import time
|
|
6
|
-
import atexit
|
|
7
6
|
from pymscada.bus_client import BusClient
|
|
7
|
+
from pymscada.periodic import Periodic
|
|
8
8
|
from pymscada.tag import Tag, TYPES
|
|
9
9
|
|
|
10
10
|
ALM = 0
|
|
@@ -12,9 +12,32 @@ RTN = 1
|
|
|
12
12
|
ACT = 2
|
|
13
13
|
INF = 3
|
|
14
14
|
|
|
15
|
+
KIND = {
|
|
16
|
+
ALM: 'ALM',
|
|
17
|
+
RTN: 'RTN',
|
|
18
|
+
ACT: 'ACT',
|
|
19
|
+
INF: 'INF'
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
NORMAL = 0
|
|
16
23
|
ALARM = 1
|
|
17
24
|
|
|
25
|
+
"""
|
|
26
|
+
Database schema:
|
|
27
|
+
|
|
28
|
+
alarms contains an event log of changes as they occur, this
|
|
29
|
+
includes information on actions taken by the alarm system.
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS alarms (
|
|
32
|
+
id INTEGER PRIMARY KEY ASC,
|
|
33
|
+
date_ms INTEGER,
|
|
34
|
+
alarm_string TEXT,
|
|
35
|
+
kind INTEGER, # one of ALM, RTN, ACT, INF
|
|
36
|
+
desc TEXT,
|
|
37
|
+
group TEXT
|
|
38
|
+
)
|
|
39
|
+
"""
|
|
40
|
+
|
|
18
41
|
|
|
19
42
|
def standardise_tag_info(tagname: str, tag: dict):
|
|
20
43
|
"""Correct tag dictionary in place to be suitable for modules."""
|
|
@@ -23,6 +46,8 @@ def standardise_tag_info(tagname: str, tag: dict):
|
|
|
23
46
|
if 'desc' not in tag:
|
|
24
47
|
logging.warning(f"Tag {tagname} has no description, using name")
|
|
25
48
|
tag['desc'] = tag['name']
|
|
49
|
+
if 'area' not in tag:
|
|
50
|
+
tag['area'] = ''
|
|
26
51
|
if 'multi' in tag:
|
|
27
52
|
tag['type'] = int
|
|
28
53
|
else:
|
|
@@ -40,51 +65,116 @@ def standardise_tag_info(tagname: str, tag: dict):
|
|
|
40
65
|
tag['dp'] = 2
|
|
41
66
|
if 'units' not in tag:
|
|
42
67
|
tag['units'] = ''
|
|
68
|
+
if 'alarm' in tag:
|
|
69
|
+
if isinstance(tag['alarm'], str):
|
|
70
|
+
tag['alarm'] = [tag['alarm']]
|
|
71
|
+
if not isinstance(tag['alarm'], list):
|
|
72
|
+
logging.warning(f"Tag {tagname} has invalid alarm {tag['alarm']}")
|
|
73
|
+
del tag['alarm']
|
|
74
|
+
|
|
43
75
|
|
|
76
|
+
def split_operator(alarm: str) -> dict:
|
|
77
|
+
"""Split alarm string into operator and value."""
|
|
78
|
+
tokens = alarm.split(' ')
|
|
79
|
+
alm_dict = {'for': 0}
|
|
80
|
+
if len(tokens) not in (2, 4):
|
|
81
|
+
raise ValueError(f"Invalid alarm {alarm}")
|
|
82
|
+
if tokens[0] not in ['>', '<', '==', '>=', '<=']:
|
|
83
|
+
raise ValueError(f"Invalid alarm {alarm}")
|
|
84
|
+
alm_dict['operator'] = tokens[0]
|
|
85
|
+
try:
|
|
86
|
+
alm_dict['value'] = float(tokens[1])
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise ValueError(f"Invalid alarm {alarm}")
|
|
89
|
+
if len(tokens) == 4:
|
|
90
|
+
if tokens[2] != 'for':
|
|
91
|
+
raise ValueError(f"Invalid alarm {alarm}")
|
|
92
|
+
try:
|
|
93
|
+
alm_dict['for'] = int(tokens[3])
|
|
94
|
+
except ValueError:
|
|
95
|
+
raise ValueError(f"Invalid alarm {alarm}")
|
|
96
|
+
return alm_dict
|
|
44
97
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
|
|
99
|
+
class Alarm():
|
|
100
|
+
"""
|
|
101
|
+
Single alarm class.
|
|
102
|
+
|
|
103
|
+
Alarms are defined by a tag and a condition. Tags may have multiple
|
|
104
|
+
conditions, each combination of tag and condition is a separate Alarm.
|
|
105
|
+
|
|
106
|
+
Monitors tag value through the Tag callback. Tracks in alarm state.
|
|
107
|
+
Generates the ALM and RTN messages for Alarms to publish via rta_tag.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, tagname: str, tag: dict, alarm: str, area: str, rta_cb, alarms) -> None:
|
|
48
111
|
"""Initialize alarm with tag and condition(s)."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
self.tag = tag
|
|
53
|
-
self.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
self.alarm_id = f'{tagname} {alarm}'
|
|
113
|
+
self.tag = Tag(tagname, tag['type'])
|
|
114
|
+
self.tag.desc = tag['desc']
|
|
115
|
+
self.tag.dp = tag['dp']
|
|
116
|
+
self.tag.units = tag['units']
|
|
117
|
+
self.tag.add_callback(self.callback)
|
|
118
|
+
self.area = area
|
|
119
|
+
self.rta_cb = rta_cb
|
|
120
|
+
self.alarms = alarms
|
|
121
|
+
self.alarm = split_operator(alarm)
|
|
122
|
+
self.in_alarm = False
|
|
123
|
+
self.checking = False
|
|
124
|
+
|
|
125
|
+
def callback(self, tag: Tag):
|
|
126
|
+
"""Handle tag value changes and generate ALM/RTN messages."""
|
|
127
|
+
if tag.value is None:
|
|
128
|
+
return
|
|
129
|
+
value = float(tag.value)
|
|
130
|
+
time_us = tag.time_us
|
|
131
|
+
new_in_alarm = False
|
|
132
|
+
op = self.alarm['operator']
|
|
133
|
+
if op == '>':
|
|
134
|
+
new_in_alarm = value > self.alarm['value']
|
|
135
|
+
elif op == '<':
|
|
136
|
+
new_in_alarm = value < self.alarm['value']
|
|
137
|
+
elif op == '==':
|
|
138
|
+
new_in_alarm = value == self.alarm['value']
|
|
139
|
+
elif op == '>=':
|
|
140
|
+
new_in_alarm = value >= self.alarm['value']
|
|
141
|
+
elif op == '<=':
|
|
142
|
+
new_in_alarm = value <= self.alarm['value']
|
|
143
|
+
if new_in_alarm == self.in_alarm:
|
|
144
|
+
return
|
|
145
|
+
self.in_alarm = new_in_alarm
|
|
146
|
+
if self.in_alarm:
|
|
147
|
+
if self.alarm['for'] > 0:
|
|
148
|
+
if not self.checking:
|
|
149
|
+
self.checking = True
|
|
150
|
+
self.alarms.checking_alarms.append(self)
|
|
151
|
+
else:
|
|
152
|
+
self.generate_alarm(ALM, time_us, value)
|
|
153
|
+
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.area
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
def check_duration(self, current_time_us: int):
|
|
173
|
+
"""Check if alarm condition has been met for required duration."""
|
|
174
|
+
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)
|
|
88
178
|
|
|
89
179
|
|
|
90
180
|
class Alarms:
|
|
@@ -128,124 +218,56 @@ class Alarms:
|
|
|
128
218
|
raise ValueError('table must be a non-empty string')
|
|
129
219
|
|
|
130
220
|
logging.warning(f'Alarms {bus_ip} {bus_port} {db} {rta_tag}')
|
|
131
|
-
self.
|
|
132
|
-
self.
|
|
133
|
-
self.alarms: dict[str, Alarm] = {}
|
|
134
|
-
self.in_alarm: dict[str, int] = {}
|
|
221
|
+
self.alarms: list[Alarm] = []
|
|
222
|
+
self.checking_alarms: list[Alarm] = []
|
|
135
223
|
for tagname, tag in tag_info.items():
|
|
136
224
|
standardise_tag_info(tagname, tag)
|
|
137
225
|
if 'alarm' not in tag or tag['type'] not in (int, float):
|
|
138
226
|
continue
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
self.tags[tagname].add_callback(self.alarm_cb)
|
|
144
|
-
self.alarms[tagname] = Alarm(self.tags[tagname], tag['alarm'])
|
|
145
|
-
self.table = table
|
|
146
|
-
self.cursor = self.connection.cursor()
|
|
227
|
+
area = tag['area']
|
|
228
|
+
for alarm in tag['alarm']:
|
|
229
|
+
new_alarm = Alarm(tagname, tag, alarm, area, self.rta_cb, self)
|
|
230
|
+
self.alarms.append(new_alarm)
|
|
147
231
|
self.busclient = BusClient(bus_ip, bus_port, module='Alarms')
|
|
148
232
|
self.rta = Tag(rta_tag, dict)
|
|
149
233
|
self.rta.value = {}
|
|
150
234
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def alarm_cb(self, tag: Tag):
|
|
154
|
-
"""Callback for alarm tags."""
|
|
155
|
-
if tag.name not in self.alarms:
|
|
156
|
-
return
|
|
157
|
-
changes = self.alarms[tag.name].check_conditions(self.in_alarm)
|
|
158
|
-
for alarm_ref, is_in_alarm, value, time_us in changes:
|
|
159
|
-
self._handle_alarm_change(
|
|
160
|
-
alarm_ref,
|
|
161
|
-
is_in_alarm,
|
|
162
|
-
tag,
|
|
163
|
-
value,
|
|
164
|
-
time_us
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
def _handle_alarm_change(self, alarm_ref: str, is_in_alarm: bool,
|
|
168
|
-
tag: Tag, value: float, time_us: int):
|
|
169
|
-
"""Handle alarm state changes and database updates."""
|
|
170
|
-
if is_in_alarm:
|
|
171
|
-
logging.warning(f'Alarm {alarm_ref} {value}')
|
|
172
|
-
kind = ALM
|
|
173
|
-
state = ALARM
|
|
174
|
-
alarm_record = {
|
|
175
|
-
'action': 'ADD',
|
|
176
|
-
'date_ms': int(time_us / 1000),
|
|
177
|
-
'tag_alm': alarm_ref,
|
|
178
|
-
'kind': kind,
|
|
179
|
-
'desc': f'{tag.desc} {value:.{tag.dp}f} {tag.units}',
|
|
180
|
-
'in_alm': state
|
|
181
|
-
}
|
|
182
|
-
self.rta_cb(alarm_record)
|
|
183
|
-
self.in_alarm[alarm_ref] = self.rta.value['id']
|
|
184
|
-
else:
|
|
185
|
-
logging.info(f'No alarm {alarm_ref} {value}')
|
|
186
|
-
if alarm_ref in self.in_alarm:
|
|
187
|
-
# First update the existing alarm record to NORMAL
|
|
188
|
-
update_record = {
|
|
189
|
-
'action': 'UPDATE',
|
|
190
|
-
'id': self.in_alarm[alarm_ref],
|
|
191
|
-
'in_alm': NORMAL
|
|
192
|
-
}
|
|
193
|
-
self.rta_cb(update_record)
|
|
194
|
-
|
|
195
|
-
# Then add the RTN record
|
|
196
|
-
rtn_record = {
|
|
197
|
-
'action': 'ADD',
|
|
198
|
-
'date_ms': int(time_us / 1000),
|
|
199
|
-
'tag_alm': alarm_ref,
|
|
200
|
-
'kind': RTN,
|
|
201
|
-
'desc': f'{tag.desc} {value:.{tag.dp}f} {tag.units}',
|
|
202
|
-
'in_alm': NORMAL
|
|
203
|
-
}
|
|
204
|
-
self.rta_cb(rtn_record)
|
|
205
|
-
del self.in_alarm[alarm_ref]
|
|
235
|
+
self._init_db(db, table)
|
|
236
|
+
self.periodic = Periodic(self.periodic_cb, 1.0)
|
|
206
237
|
|
|
207
|
-
def
|
|
238
|
+
def _init_db(self, db, table):
|
|
208
239
|
"""Initialize the database table schema."""
|
|
240
|
+
self.connection = sqlite3.connect(db)
|
|
241
|
+
self.table = table
|
|
242
|
+
self.cursor = self.connection.cursor()
|
|
209
243
|
query = (
|
|
210
|
-
'CREATE TABLE IF NOT EXISTS ' + self.table +
|
|
244
|
+
'CREATE TABLE IF NOT EXISTS ' + self.table + ' '
|
|
211
245
|
'(id INTEGER PRIMARY KEY ASC, '
|
|
212
246
|
'date_ms INTEGER, '
|
|
213
|
-
'
|
|
247
|
+
'alarm_string TEXT, '
|
|
214
248
|
'kind INTEGER, '
|
|
215
249
|
'desc TEXT, '
|
|
216
|
-
'
|
|
250
|
+
'"group" TEXT)'
|
|
217
251
|
)
|
|
218
252
|
self.cursor.execute(query)
|
|
253
|
+
self.connection.commit()
|
|
219
254
|
|
|
220
|
-
# Clear any existing ALARM states
|
|
221
|
-
try:
|
|
222
|
-
with self.connection:
|
|
223
|
-
# Update all alarm records to NORMAL
|
|
224
|
-
self.cursor.execute(
|
|
225
|
-
f'SELECT id, tag_alm FROM {self.table} WHERE in_alm = ?',
|
|
226
|
-
(ALARM,))
|
|
227
|
-
alarm_records = self.cursor.fetchall()
|
|
228
|
-
for record_id, tag_alm in alarm_records:
|
|
229
|
-
update_record = {
|
|
230
|
-
'action': 'UPDATE',
|
|
231
|
-
'id': record_id,
|
|
232
|
-
'in_alm': NORMAL
|
|
233
|
-
}
|
|
234
|
-
self.rta_cb(update_record)
|
|
235
|
-
except sqlite3.Error as e:
|
|
236
|
-
logging.error(f'Error clearing alarm states during startup: {e}')
|
|
237
|
-
|
|
238
|
-
# Add startup record using existing ADD functionality
|
|
239
255
|
startup_record = {
|
|
240
256
|
'action': 'ADD',
|
|
241
257
|
'date_ms': int(time.time() * 1000),
|
|
242
|
-
'
|
|
258
|
+
'alarm_string': self.rta.name,
|
|
243
259
|
'kind': INF,
|
|
244
260
|
'desc': 'Alarm logging started',
|
|
245
|
-
'
|
|
261
|
+
'group': '__system__'
|
|
246
262
|
}
|
|
247
263
|
self.rta_cb(startup_record)
|
|
248
264
|
|
|
265
|
+
async def periodic_cb(self):
|
|
266
|
+
"""Periodic callback to check alarms."""
|
|
267
|
+
current_time_us = int(time.time() * 1000000)
|
|
268
|
+
for alarm in self.checking_alarms[:]:
|
|
269
|
+
alarm.check_duration(current_time_us)
|
|
270
|
+
|
|
249
271
|
def rta_cb(self, request):
|
|
250
272
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
251
273
|
if 'action' not in request:
|
|
@@ -256,18 +278,18 @@ class Alarms:
|
|
|
256
278
|
with self.connection:
|
|
257
279
|
self.cursor.execute(
|
|
258
280
|
f'INSERT INTO {self.table} '
|
|
259
|
-
'(date_ms,
|
|
260
|
-
'VALUES(:date_ms, :
|
|
281
|
+
'(date_ms, alarm_string, kind, desc, "group") '
|
|
282
|
+
'VALUES(:date_ms, :alarm_string, :kind, :desc, :group) '
|
|
261
283
|
'RETURNING *;',
|
|
262
284
|
request)
|
|
263
285
|
res = self.cursor.fetchone()
|
|
264
286
|
self.rta.value = {
|
|
265
287
|
'id': res[0],
|
|
266
288
|
'date_ms': res[1],
|
|
267
|
-
'
|
|
289
|
+
'alarm_string': res[2],
|
|
268
290
|
'kind': res[3],
|
|
269
291
|
'desc': res[4],
|
|
270
|
-
'
|
|
292
|
+
'group': res[5]
|
|
271
293
|
}
|
|
272
294
|
except sqlite3.IntegrityError as error:
|
|
273
295
|
logging.warning(f'Alarms rta_cb {error}')
|
|
@@ -284,10 +306,10 @@ class Alarms:
|
|
|
284
306
|
self.rta.value = {
|
|
285
307
|
'id': res[0],
|
|
286
308
|
'date_ms': res[1],
|
|
287
|
-
'
|
|
309
|
+
'alarm_string': res[2],
|
|
288
310
|
'kind': res[3],
|
|
289
311
|
'desc': res[4],
|
|
290
|
-
'
|
|
312
|
+
'group': res[5]
|
|
291
313
|
}
|
|
292
314
|
except sqlite3.IntegrityError as error:
|
|
293
315
|
logging.warning(f'Alarms rta_cb update {error}')
|
|
@@ -303,10 +325,10 @@ class Alarms:
|
|
|
303
325
|
'__rta_id__': request['__rta_id__'],
|
|
304
326
|
'id': res[0],
|
|
305
327
|
'date_ms': res[1],
|
|
306
|
-
'
|
|
328
|
+
'alarm_string': res[2],
|
|
307
329
|
'kind': res[3],
|
|
308
330
|
'desc': res[4],
|
|
309
|
-
'
|
|
331
|
+
'group': res[5]
|
|
310
332
|
}
|
|
311
333
|
except sqlite3.IntegrityError as error:
|
|
312
334
|
logging.warning(f'Alarms rta_cb {error}')
|
|
@@ -329,30 +351,4 @@ class Alarms:
|
|
|
329
351
|
async def start(self):
|
|
330
352
|
"""Async startup."""
|
|
331
353
|
await self.busclient.start()
|
|
332
|
-
self.
|
|
333
|
-
|
|
334
|
-
def close(self):
|
|
335
|
-
"""Clean shutdown of alarms logging."""
|
|
336
|
-
for alarm_ref, record_id in self.in_alarm.items():
|
|
337
|
-
update_record = {
|
|
338
|
-
'action': 'UPDATE',
|
|
339
|
-
'id': record_id,
|
|
340
|
-
'in_alm': NORMAL
|
|
341
|
-
}
|
|
342
|
-
try:
|
|
343
|
-
self.rta_cb(update_record)
|
|
344
|
-
except sqlite3.Error as e:
|
|
345
|
-
logging.error(f'Error clearing alarm {alarm_ref}: {e}')
|
|
346
|
-
|
|
347
|
-
shutdown_record = {
|
|
348
|
-
'action': 'ADD',
|
|
349
|
-
'date_ms': int(time.time() * 1000),
|
|
350
|
-
'tag_alm': self.rta.name,
|
|
351
|
-
'kind': INF,
|
|
352
|
-
'desc': 'Alarm logging stopped',
|
|
353
|
-
'in_alm': NORMAL
|
|
354
|
-
}
|
|
355
|
-
try:
|
|
356
|
-
self.rta_cb(shutdown_record)
|
|
357
|
-
except sqlite3.Error as e:
|
|
358
|
-
logging.error(f'Error during alarm shutdown: {e}')
|
|
354
|
+
await self.periodic.start()
|
|
@@ -3,7 +3,8 @@ import asyncio
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
5
|
from pymscada.bus_client import BusClient
|
|
6
|
-
from pymscada.tag import Tag
|
|
6
|
+
from pymscada.tag import Tag
|
|
7
|
+
from pymscada.www_server import standardise_tag_info
|
|
7
8
|
try:
|
|
8
9
|
import termios
|
|
9
10
|
import tty
|
|
@@ -153,7 +154,7 @@ class Console:
|
|
|
153
154
|
"""Provide a text console to interact with a Bus."""
|
|
154
155
|
|
|
155
156
|
def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
|
|
156
|
-
tag_info: dict
|
|
157
|
+
tag_info: dict = {}) -> None:
|
|
157
158
|
"""
|
|
158
159
|
Connect to bus_ip:bus_port and provide console interaction with a Bus.
|
|
159
160
|
|
|
@@ -177,7 +178,7 @@ class Console:
|
|
|
177
178
|
self.busclient = BusClient(bus_ip, bus_port, module='Console')
|
|
178
179
|
self.tags: dict[str, Tag] = {}
|
|
179
180
|
for tagname, tag in tag_info.items():
|
|
180
|
-
|
|
181
|
+
standardise_tag_info(tagname, tag)
|
|
181
182
|
self.tags[tagname] = Tag(tagname, tag['type'])
|
|
182
183
|
|
|
183
184
|
def write_tag(self, tag: Tag):
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
bus_ip: 127.0.0.1
|
|
2
|
+
bus_port: 1324
|
|
3
|
+
proxy:
|
|
4
|
+
api:
|
|
5
|
+
url: 'https://api.electricityinfo.co.nz'
|
|
6
|
+
client_id: ${WITS_CLIENT_ID}
|
|
7
|
+
client_secret: ${WITS_CLIENT_SECRET}
|
|
8
|
+
gxp_list:
|
|
9
|
+
- MAT1101
|
|
10
|
+
- CYD2201
|
|
11
|
+
- BEN2201
|
|
12
|
+
back: 1
|
|
13
|
+
forward: 12
|
|
14
|
+
tags:
|
|
15
|
+
- MAT1101_RTD
|
|
16
|
+
- CYD2201_RTD
|
|
17
|
+
- BEN2201_RTD
|