pymscada 0.2.0rc2__py3-none-any.whl → 0.2.0rc3__py3-none-any.whl

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/alarms.py ADDED
@@ -0,0 +1,194 @@
1
+ """Alarms handling."""
2
+ import logging
3
+ import sqlite3 # note that sqlite3 has blocking calls
4
+ import socket
5
+ import time
6
+ import atexit
7
+ from pymscada.bus_client import BusClient
8
+ from pymscada.tag import Tag, TagInfo, TYPES
9
+
10
+ ALM = 0
11
+ RTN = 1
12
+ ACT = 2
13
+ INF = 3
14
+
15
+
16
+ class Alarms:
17
+ """Connect to bus_ip:bus_port, store and provide Alarms."""
18
+
19
+ def __init__(
20
+ self,
21
+ bus_ip: str | None = '127.0.0.1',
22
+ bus_port: int | None = 1324,
23
+ db: str | None = None,
24
+ table: str = 'alarms',
25
+ tag_info: TagInfo = {},
26
+ rta_tag: str = '__alarms__'
27
+ ) -> None:
28
+ """
29
+ Connect to bus_ip:bus_port, serve and update alarms database.
30
+
31
+ Open an Alarms table, creating if necessary. Provide additions
32
+ and history requests via the rta_tag.
33
+
34
+ Event loop must be running.
35
+
36
+ For testing only: bus_ip can be None to skip connection.
37
+ """
38
+ if db is None:
39
+ raise SystemExit('Alarms db must be defined')
40
+ if bus_ip is None:
41
+ logging.warning('Alarms has bus_ip=None, only use for testing')
42
+ else:
43
+ try:
44
+ socket.gethostbyname(bus_ip)
45
+ except socket.gaierror as e:
46
+ raise ValueError(f'Cannot resolve IP/hostname: {e}')
47
+ if not isinstance(bus_port, int):
48
+ raise TypeError('bus_port must be an integer')
49
+ if not 1024 <= bus_port <= 65535:
50
+ raise ValueError('bus_port must be between 1024 and 65535')
51
+ if not isinstance(rta_tag, str) or not rta_tag:
52
+ raise ValueError('rta_tag must be a non-empty string')
53
+ if not isinstance(table, str) or not table:
54
+ raise ValueError('table must be a non-empty string')
55
+
56
+ logging.warning(f'Alarms {bus_ip} {bus_port} {db} {rta_tag}')
57
+ self.connection = sqlite3.connect(db)
58
+ self.tags: dict[str, Tag] = {}
59
+ self.alarm_test: dict[str, dict] = {}
60
+ self.in_alarm: set[str] = set()
61
+ for tagname, tag in tag_info.items():
62
+ if 'alarm' not in tag:
63
+ continue
64
+ self.tags[tagname] = Tag(tagname, tag['type'])
65
+ self.tags[tagname].desc = tag['desc']
66
+ self.tags[tagname].add_callback(self.alarm_cb)
67
+ operator, value = tag['alarm'].split(' ')
68
+ self.alarm_test[tagname] = [
69
+ {
70
+ '==': (lambda x, y: x == y),
71
+ '<': (lambda x, y: x < y),
72
+ '>': (lambda x, y: x > y),
73
+ '<=': (lambda x, y: x <= y),
74
+ '>=': (lambda x, y: x >= y)
75
+ }[operator],
76
+ float(value)
77
+ ]
78
+ self.table = table
79
+ self.cursor = self.connection.cursor()
80
+ self.busclient = BusClient(bus_ip, bus_port, module='Alarms')
81
+ self.rta = Tag(rta_tag, dict)
82
+ self.rta.value = {}
83
+ self.busclient.add_callback_rta(rta_tag, self.rta_cb)
84
+ self._init_table()
85
+ atexit.register(self.close)
86
+
87
+ def alarm_cb(self, tag):
88
+ """Callback for alarm tags."""
89
+ operator, value = self.alarm_test[tag.name]
90
+ if operator(tag.value, value) and tag.name not in self.in_alarm:
91
+ logging.warning(f'Alarm {tag.name} {tag.value}')
92
+ alarm_record = {
93
+ 'action': 'ADD',
94
+ 'date_ms': int(tag.time_us / 1000),
95
+ 'tagname': tag.name,
96
+ 'transition': ALM,
97
+ 'description': f'{tag.desc} {tag.value}'
98
+ }
99
+ self.rta_cb(alarm_record)
100
+ self.in_alarm.add(tag.name)
101
+ elif not operator(tag.value, value) and tag.name in self.in_alarm:
102
+ logging.info(f'No alarm {tag.name} {tag.value}')
103
+ alarm_record = {
104
+ 'action': 'ADD',
105
+ 'date_ms': int(tag.time_us / 1000),
106
+ 'tagname': tag.name,
107
+ 'transition': RTN,
108
+ 'description': f'{tag.desc} {tag.value}'
109
+ }
110
+ self.rta_cb(alarm_record)
111
+ self.in_alarm.remove(tag.name)
112
+
113
+ def _init_table(self):
114
+ """Initialize the database table schema."""
115
+ query = (
116
+ 'CREATE TABLE IF NOT EXISTS ' + self.table +
117
+ '(id INTEGER PRIMARY KEY ASC, '
118
+ 'date_ms INTEGER, '
119
+ 'tagname TEXT, '
120
+ 'transition INTEGER, '
121
+ 'description TEXT)'
122
+ )
123
+ self.cursor.execute(query)
124
+
125
+ # Add startup record using existing ADD functionality
126
+ startup_record = {
127
+ 'action': 'ADD',
128
+ 'date_ms': int(time.time() * 1000),
129
+ 'tagname': self.rta.name,
130
+ 'transition': INF,
131
+ 'description': 'Alarm logging started'
132
+ }
133
+ self.rta_cb(startup_record)
134
+
135
+ def rta_cb(self, request):
136
+ """Respond to Request to Author and publish on rta_tag as needed."""
137
+ if 'action' not in request:
138
+ logging.warning(f'rta_cb malformed {request}')
139
+ elif request['action'] == 'ADD':
140
+ try:
141
+ logging.info(f'add {request}')
142
+ with self.connection:
143
+ self.cursor.execute(
144
+ f'INSERT INTO {self.table} '
145
+ '(date_ms, tagname, transition, description) '
146
+ 'VALUES(:date_ms, :tagname, :transition, :description) '
147
+ 'RETURNING *;',
148
+ request)
149
+ res = self.cursor.fetchone()
150
+ self.rta.value = {
151
+ 'id': res[0],
152
+ 'date_ms': res[1],
153
+ 'tagname': res[2],
154
+ 'transition': res[3],
155
+ 'description': res[4]
156
+ }
157
+ except sqlite3.IntegrityError as error:
158
+ logging.warning(f'Alarms rta_cb {error}')
159
+ elif request['action'] == 'HISTORY':
160
+ try:
161
+ logging.info(f'history {request}')
162
+ with self.connection:
163
+ self.cursor.execute(
164
+ f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
165
+ 'ORDER BY (date_ms - :date_ms);', request)
166
+ for res in self.cursor.fetchall():
167
+ self.rta.value = {
168
+ '__rta_id__': request['__rta_id__'],
169
+ 'id': res[0],
170
+ 'date_ms': res[1],
171
+ 'tagname': res[2],
172
+ 'transition': res[3],
173
+ 'description': res[4]
174
+ }
175
+ except sqlite3.IntegrityError as error:
176
+ logging.warning(f'Alarms rta_cb {error}')
177
+
178
+ async def start(self):
179
+ """Async startup."""
180
+ await self.busclient.start()
181
+
182
+ def close(self):
183
+ """Clean shutdown of alarms logging."""
184
+ shutdown_record = {
185
+ 'action': 'ADD',
186
+ 'date_ms': int(time.time() * 1000),
187
+ 'tagname': self.rta.name,
188
+ 'transition': INF,
189
+ 'description': 'Alarm logging stopped'
190
+ }
191
+ try:
192
+ self.rta_cb(shutdown_record)
193
+ except sqlite3.Error as e:
194
+ logging.error(f'Error during alarm shutdown: {e}')
@@ -0,0 +1,5 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1324
3
+ rta_tag: __alarms__
4
+ db: __HOME__/config/pymscada-alarms.sqlite
5
+ table: alarms
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=pymscada - alarms
3
+ BindsTo=pymscada-bus.service
4
+ After=pymscada-bus.service
5
+
6
+ [Service]
7
+ WorkingDirectory=__DIR__
8
+ ExecStart=__PYMSCADA__ alarms --config __DIR__/config/alarms.yaml --tags __DIR__/config/tags.yaml
9
+ Restart=always
10
+ RestartSec=5
11
+ User=__USER__
12
+ Group=__USER__
13
+ KillSignal=SIGINT
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target
pymscada/module_config.py CHANGED
@@ -5,7 +5,6 @@ from textwrap import dedent
5
5
  from importlib.metadata import version
6
6
  import logging
7
7
  from pymscada.config import Config
8
- from pymscada.console import Console
9
8
 
10
9
  class ModuleArgument:
11
10
  def __init__(self, args: tuple[str, ...], kwargs: dict[str, Any]):
@@ -59,6 +58,11 @@ def create_module_registry():
59
58
  module_class='pymscada.opnotes:OpNotes',
60
59
  tags=False
61
60
  ),
61
+ ModuleDefinition(
62
+ name='alarms',
63
+ help='alarms',
64
+ module_class='pymscada.alarms:Alarms'
65
+ ),
62
66
  ModuleDefinition(
63
67
  name='validate',
64
68
  help='validate config files',
pymscada/opnotes.py CHANGED
@@ -24,17 +24,22 @@ class OpNotes:
24
24
  updates, deletions and history requests via the rta_tag.
25
25
 
26
26
  Event loop must be running.
27
+
28
+ For testing only: bus_ip can be None to skip connection.
27
29
  """
28
30
  if db is None:
29
31
  raise SystemExit('OpNotes db must be defined')
30
- try:
31
- socket.gethostbyname(bus_ip)
32
- except socket.gaierror as e:
33
- raise ValueError(f'Cannot resolve IP/hostname: {e}')
34
- if not isinstance(bus_port, int):
35
- raise TypeError('bus_port must be an integer')
36
- if not 1024 <= bus_port <= 65535:
37
- raise ValueError('bus_port must be between 1024 and 65535')
32
+ if bus_ip is None:
33
+ logging.warning('OpNotes has bus_ip=None, only use for testing')
34
+ else:
35
+ try:
36
+ socket.gethostbyname(bus_ip)
37
+ except socket.gaierror as e:
38
+ raise ValueError(f'Cannot resolve IP/hostname: {e}')
39
+ if not isinstance(bus_port, int):
40
+ raise TypeError('bus_port must be an integer')
41
+ if not 1024 <= bus_port <= 65535:
42
+ raise ValueError('bus_port must be between 1024 and 65535')
38
43
  if not isinstance(rta_tag, str) or not rta_tag:
39
44
  raise ValueError('rta_tag must be a non-empty string')
40
45
  if not isinstance(table, str) or not table:
pymscada/tag.py CHANGED
@@ -24,7 +24,7 @@ def tag_for_web(tagname: str, tag: dict):
24
24
  tag['name'] = tagname
25
25
  tag['id'] = None
26
26
  if 'desc' not in tag:
27
- tag['desc'] = tag.name
27
+ tag['desc'] = tagname
28
28
  if 'multi' in tag:
29
29
  tag['type'] = 'int'
30
30
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymscada
3
- Version: 0.2.0rc2
3
+ Version: 0.2.0rc3
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,5 +1,6 @@
1
1
  pymscada/__init__.py,sha256=NV_cIIwe66Ugp8ns426rtfJIIyskWbqwImD9p_5p0bQ,739
2
2
  pymscada/__main__.py,sha256=WcyVlrYOoDdktJhOoyubTOycMwpayksFdxwelRU5xpQ,272
3
+ pymscada/alarms.py,sha256=BM6R-PFf7EazhGdGHJ1XeAcCoi83Ro9XvEl_9cp-iCI,7326
3
4
  pymscada/bus_client.py,sha256=ROShMcR2-y_i5CIvPxRdCRr-NpnMANjKFdLjKjMTRwo,9117
4
5
  pymscada/bus_server.py,sha256=k7ht2SAr24Oab0hBOPeW4NRDF_RK-F46iE0cMzh7K4w,12323
5
6
  pymscada/checkout.py,sha256=RLuCMTEuUI7pp1hIRAUPbo8xYFta8TjArelx0SD4gOY,3897
@@ -9,17 +10,18 @@ pymscada/files.py,sha256=iouEOPfEkVI0Qbbf1p-L324Y04zSrynVypLW0-1MThA,2499
9
10
  pymscada/history.py,sha256=qXbCkHQC_j7HF9b1PvnF93d4HvW9ODJeSc9tvy63o10,11513
10
11
  pymscada/main.py,sha256=d6EnK4-tEcvM5AqMHYhvqlnSh-E_wd0Tuxk-kXYSiDw,1854
11
12
  pymscada/misc.py,sha256=0Cj6OFhQonyhyk9x0BG5MiS-6EPk_w6zvavt8o_Hlf0,622
12
- pymscada/module_config.py,sha256=3-15qhCLsi_xR4i0cy2XZ74w5n0ZcqW2nPcX_Qtc6fU,8720
13
- pymscada/opnotes.py,sha256=vHJYPlPJLLsUHPbh4eBOUUnEnj9oXrrStb9Jzz-m524,7736
13
+ pymscada/module_config.py,sha256=r1JBjOXtMk7n09kvlvnmA3d_BySEmJpdefFhRNKPdAY,8824
14
+ pymscada/opnotes.py,sha256=MKM51IrB93B2-kgoTzlpOLpaMYs-7rPQHWmRLME-hQQ,7952
14
15
  pymscada/periodic.py,sha256=MLlL93VLvFqBBgjO1Us1t0aLHTZ5BFdW0B__G02T1nQ,1235
15
16
  pymscada/protocol_constants.py,sha256=lPJ4JEgFJ_puJjTym83EJIOw3UTUFbuFMwg3ohyUAGY,2414
16
17
  pymscada/samplers.py,sha256=t0IscgsCm5YByioOZ6aOKMO_guDFS_wxnJSiOGKI4Nw,2583
17
- pymscada/tag.py,sha256=GINy8gE1TPLFnuJLVVSui4Ke3IQlOGRy6_ePAlGlvj4,10683
18
+ pymscada/tag.py,sha256=G0P9t3G6heQ1kBWPnc0u8qN6UvjG4xtyI9w_KqGr4Q0,10682
18
19
  pymscada/validate.py,sha256=fPMlP6RscYuTIgdEJjJ0ZZI0OyVSat1lpqg254wqpdE,13140
19
20
  pymscada/www_server.py,sha256=rz6Yictg_Ony8zW76An5bv_cZN5aLi-QW3B85uWMgXQ,13152
20
21
  pymscada/demo/README.md,sha256=iNcVbCTkq-d4agLV-979lNRaqf_hbJCn3OFzY-6qfU8,880
21
22
  pymscada/demo/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
22
23
  pymscada/demo/accuweather.yaml,sha256=Fk4rV0S8jCau0173QCzKW8TdUbc4crYVi0aD8fPLNgU,322
24
+ pymscada/demo/alarms.yaml,sha256=Ea8tLZ0aEiyKM_m5MN1TF6xS-lI5ReXiz2oUPO8GvmQ,110
23
25
  pymscada/demo/bus.yaml,sha256=zde5JDo2Yv5s7NvJ569gAEoTDvsvgBwRPxfrYhsxj3w,26
24
26
  pymscada/demo/files.yaml,sha256=XWtmGDJxtD4qdl2h7miUfJYkDKsvwNTgQjlGpR6LQNs,163
25
27
  pymscada/demo/history.yaml,sha256=c0OuYe8LbTeZqJGU2WKGgTEkOA0IYAjO3e046ddQB8E,55
@@ -30,6 +32,7 @@ pymscada/demo/modbusserver.yaml,sha256=67_mED6jXgtnzlDIky9Cg4j-nXur06iz9ve3JUwSy
30
32
  pymscada/demo/openweather.yaml,sha256=n8aPc_Ar6uiM-XbrEbBydABxFYm2uv_49dGo8u7DI8Q,433
31
33
  pymscada/demo/opnotes.yaml,sha256=gdT8DKaAV4F6u9trLCPyBgf449wYaP_FF8GCbjXm9-k,105
32
34
  pymscada/demo/ping.yaml,sha256=fm3eUdR2BwnPI_lU_V07qgmDxjSoPP6lPazYB6ZgpVg,149
35
+ pymscada/demo/pymscada-alarms.service,sha256=nHjEMsonat-3ny0QJoY6KTZoPIt2HZiarKgW5uasY8k,383
33
36
  pymscada/demo/pymscada-bus.service,sha256=F3ViriRXyMKdCY3tHa3wXAnv2Fo2_16-EScTLsYnSOA,261
34
37
  pymscada/demo/pymscada-demo-modbus_plc.service,sha256=EtbWDwqAs4nLNLKScUiHcUWU1b6_tRBeAAVGi9q95hY,320
35
38
  pymscada/demo/pymscada-files.service,sha256=iLOfbl4SCxAwYHT20XCGHU0BJsUVicNHjHzKS8xIdgA,326
@@ -64,9 +67,9 @@ pymscada/pdf/two.pdf,sha256=TAuW5yLU1_wfmTH_I5ezHwY0pxhCVuZh3ixu0kwmJwE,1516
64
67
  pymscada/pdf/__pycache__/__init__.cpython-311.pyc,sha256=4KTfXrV9bGDbTIEv-zgIj_LvzLbVTj77lEC1wzMh9e0,194
65
68
  pymscada/tools/snmp_client2.py,sha256=pdn5dYyEv4q-ubA0zQ8X-3tQDYxGC7f7Xexa7QPaL40,1675
66
69
  pymscada/tools/walk.py,sha256=OgpprUbKLhEWMvJGfU1ckUt_PFEpwZVOD8HucCgzmOc,1625
67
- pymscada-0.2.0rc2.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
68
- pymscada-0.2.0rc2.dist-info/METADATA,sha256=LQXKkPOljU9yFJccl7DrUoLjz1MsGWTnirZ-w8Xaj-U,2371
69
- pymscada-0.2.0rc2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
70
- pymscada-0.2.0rc2.dist-info/entry_points.txt,sha256=2UJBi8jrqujnerrcXcq4F8GHJYVDt26sacXl94t3sd8,56
71
- pymscada-0.2.0rc2.dist-info/top_level.txt,sha256=LxIB-zrtgObJg0fgdGZXBkmNKLDYHfaH1Hw2YP2ZMms,9
72
- pymscada-0.2.0rc2.dist-info/RECORD,,
70
+ pymscada-0.2.0rc3.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
+ pymscada-0.2.0rc3.dist-info/METADATA,sha256=ezP-4JEdjL8qwHVyi0q9xrpgaft2BJNez1Ai96k1e5k,2371
72
+ pymscada-0.2.0rc3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
73
+ pymscada-0.2.0rc3.dist-info/entry_points.txt,sha256=2UJBi8jrqujnerrcXcq4F8GHJYVDt26sacXl94t3sd8,56
74
+ pymscada-0.2.0rc3.dist-info/top_level.txt,sha256=LxIB-zrtgObJg0fgdGZXBkmNKLDYHfaH1Hw2YP2ZMms,9
75
+ pymscada-0.2.0rc3.dist-info/RECORD,,