pymscada 0.1.2__tar.gz → 0.1.4__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.1.4/PKG-INFO +59 -0
- pymscada-0.1.4/README.md +38 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/pyproject.toml +1 -1
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/checkout.py +10 -7
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/console.py +1 -1
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/files.yaml +1 -1
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/history.yaml +1 -1
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/opnotes.yaml +1 -1
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-bus.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-demo-modbus_plc.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-files.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-history.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-io-logixclient.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-io-modbusclient.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-io-modbusserver.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-io-ping.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-io-snmpclient.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-opnotes.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/pymscada-wwwserver.service +2 -2
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/wwwserver.yaml +9 -3
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/snmp_client.py +4 -1
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/opnotes.py +39 -24
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/www_server.py +19 -5
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_opnotes.py +17 -17
- pymscada-0.1.2/PKG-INFO +0 -88
- pymscada-0.1.2/README.md +0 -67
- {pymscada-0.1.2 → pymscada-0.1.4}/LICENSE +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/__init__.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/__main__.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/bus_client.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/bus_server.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/config.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/logixclient.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/modbusserver.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/ping.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/snmpclient.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/demo/tags.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/files.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/history.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/logix_client.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/logix_map.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/modbus_client.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/modbus_map.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/modbus_server.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/ping_client.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/ping_map.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/iodrivers/snmp_map.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/main.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/misc.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/periodic.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/samplers.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/tag.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/tools/snmp_client2.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/tools/walk.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/src/pymscada/validate.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/__init__.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/bus_echo.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/iodrivers/test_logix.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/iodrivers/test_modbus.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/busserver.yaml +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/db.sqlite +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/hist_tag_0_0.dat +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/hist_tag_0_10_2.dat +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/hist_tag_0_15.dat +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/hist_tag_0_26.dat +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_assets/hist_tag_0_50.dat +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_bus_server.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_config.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_history.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_misc.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_periodic.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_samplers.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_tag.py +0 -0
- {pymscada-0.1.2 → pymscada-0.1.4}/tests/test_validate.py +0 -0
pymscada-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pymscada
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Shared tag value SCADA with python backup and Angular UI
|
|
5
|
+
Author-Email: Jamie Walton <jamie@walton.net.nz>
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: JavaScript
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Development Status :: 1 - Planning
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
15
|
+
Requires-Dist: aiohttp>=3.8.5
|
|
16
|
+
Requires-Dist: pymscada-html==0.1.0
|
|
17
|
+
Requires-Dist: cerberus>=1.3.5
|
|
18
|
+
Requires-Dist: pycomm3>=1.2.14
|
|
19
|
+
Requires-Dist: pysnmplib>=5.0.24
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# pymscada
|
|
23
|
+
#### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
|
|
24
|
+
|
|
25
|
+
#### [@Github](https://github.com/jamie0walton/pymscada/blob/main/README.md)
|
|
26
|
+
|
|
27
|
+
<span style='color:aqua'>WIP: Presently updating documentation and deployment.</span>
|
|
28
|
+
|
|
29
|
+
## Python Mobile SCADA
|
|
30
|
+
|
|
31
|
+
```pymscada``` read / write to Modbus and Logix PLCs. Read SNMP OIDs.
|
|
32
|
+
Collect history values and provide the ability to set values and trends
|
|
33
|
+
and issue commands.
|
|
34
|
+
|
|
35
|
+
User interface is via a web client embedded in this package. Examples included
|
|
36
|
+
for securing with Apache as a proxy.
|
|
37
|
+
|
|
38
|
+
Configuration with text yaml files, including the web page which are
|
|
39
|
+
procedurally built.
|
|
40
|
+
|
|
41
|
+
# See also
|
|
42
|
+
|
|
43
|
+
- The angular project [angmscada](https://github.com/jamie0walton/angmscada)
|
|
44
|
+
- Python container for the compiled angular pages [pymscada-html](https://github.com/jamie0walton/pymscada-html)
|
|
45
|
+
|
|
46
|
+
# Licence
|
|
47
|
+
|
|
48
|
+
```pymscada``` is distributed under the GPLv3 [license](./LICENSE).
|
|
49
|
+
|
|
50
|
+
# Running
|
|
51
|
+
|
|
52
|
+
While many parts of ```pymscada``` will run in windows, this is not intentional.
|
|
53
|
+
|
|
54
|
+
Running a useful subset requires quite a lot of steps, you have to choose the services
|
|
55
|
+
you want and providing meaningful configuation. ```pymscada checkout``` will create
|
|
56
|
+
templates of all of these for you that allows
|
|
57
|
+
[Debian Quickstart](./docs/debian_quickstart.md) to get you to a working web page,
|
|
58
|
+
however to connect to a PLC, trend data, read data and write setpoints, requires
|
|
59
|
+
knowledge of typical SCADA and PLC functionality.
|
pymscada-0.1.4/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# pymscada
|
|
2
|
+
#### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
|
|
3
|
+
|
|
4
|
+
#### [@Github](https://github.com/jamie0walton/pymscada/blob/main/README.md)
|
|
5
|
+
|
|
6
|
+
<span style='color:aqua'>WIP: Presently updating documentation and deployment.</span>
|
|
7
|
+
|
|
8
|
+
## Python Mobile SCADA
|
|
9
|
+
|
|
10
|
+
```pymscada``` read / write to Modbus and Logix PLCs. Read SNMP OIDs.
|
|
11
|
+
Collect history values and provide the ability to set values and trends
|
|
12
|
+
and issue commands.
|
|
13
|
+
|
|
14
|
+
User interface is via a web client embedded in this package. Examples included
|
|
15
|
+
for securing with Apache as a proxy.
|
|
16
|
+
|
|
17
|
+
Configuration with text yaml files, including the web page which are
|
|
18
|
+
procedurally built.
|
|
19
|
+
|
|
20
|
+
# See also
|
|
21
|
+
|
|
22
|
+
- The angular project [angmscada](https://github.com/jamie0walton/angmscada)
|
|
23
|
+
- Python container for the compiled angular pages [pymscada-html](https://github.com/jamie0walton/pymscada-html)
|
|
24
|
+
|
|
25
|
+
# Licence
|
|
26
|
+
|
|
27
|
+
```pymscada``` is distributed under the GPLv3 [license](./LICENSE).
|
|
28
|
+
|
|
29
|
+
# Running
|
|
30
|
+
|
|
31
|
+
While many parts of ```pymscada``` will run in windows, this is not intentional.
|
|
32
|
+
|
|
33
|
+
Running a useful subset requires quite a lot of steps, you have to choose the services
|
|
34
|
+
you want and providing meaningful configuation. ```pymscada checkout``` will create
|
|
35
|
+
templates of all of these for you that allows
|
|
36
|
+
[Debian Quickstart](./docs/debian_quickstart.md) to get you to a working web page,
|
|
37
|
+
however to connect to a PLC, trend data, read data and write setpoints, requires
|
|
38
|
+
knowledge of typical SCADA and PLC functionality.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Create base config folder and check out demo files."""
|
|
2
2
|
import difflib
|
|
3
|
+
import getpass
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
import sys
|
|
5
6
|
from pymscada.config import get_demo_files, get_pdf_files
|
|
@@ -8,16 +9,19 @@ from pymscada.config import get_demo_files, get_pdf_files
|
|
|
8
9
|
PATH = {
|
|
9
10
|
'__PYTHON__': Path(f'{sys.exec_prefix}/bin/python').absolute(),
|
|
10
11
|
'__PYMSCADA__': Path(sys.argv[0]).absolute(),
|
|
11
|
-
'__DIR__': Path('.').absolute()
|
|
12
|
+
'__DIR__': Path('.').absolute(),
|
|
13
|
+
'__HOME__': Path.home().absolute(),
|
|
14
|
+
'__USER__': getpass.getuser()
|
|
12
15
|
}
|
|
13
16
|
if sys.platform == "win32":
|
|
14
17
|
PATH = {
|
|
15
18
|
'__PYTHON__': Path(f'{sys.exec_prefix}/python.exe').absolute(),
|
|
16
19
|
'__PYMSCADA__': Path(sys.argv[0]).absolute(),
|
|
17
|
-
'__DIR__': Path('.').absolute()
|
|
20
|
+
'__DIR__': Path('.').absolute(),
|
|
21
|
+
'__HOME__': Path.home(),
|
|
22
|
+
'__USER__': getpass.getuser()
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
21
25
|
def make_history():
|
|
22
26
|
"""Make the history folder if missing."""
|
|
23
27
|
history_dir = PATH['__DIR__'].joinpath('history')
|
|
@@ -56,8 +60,7 @@ def make_config(overwrite: bool):
|
|
|
56
60
|
if str(target).endswith('service'):
|
|
57
61
|
rd_bytes = config_file.read_bytes()
|
|
58
62
|
for k, v in PATH.items():
|
|
59
|
-
rd_bytes = rd_bytes.replace(k.encode(),
|
|
60
|
-
str(v.absolute()).encode())
|
|
63
|
+
rd_bytes = rd_bytes.replace(k.encode(),str(v).encode())
|
|
61
64
|
target.write_bytes(rd_bytes)
|
|
62
65
|
else:
|
|
63
66
|
target.write_bytes(config_file.read_bytes())
|
|
@@ -68,7 +71,7 @@ def read_with_subst(file: Path):
|
|
|
68
71
|
rd = file.read_bytes().decode()
|
|
69
72
|
if str(file).endswith('service'):
|
|
70
73
|
for k, v in PATH.items():
|
|
71
|
-
rd = rd.replace(k, str(v
|
|
74
|
+
rd = rd.replace(k, str(v))
|
|
72
75
|
lines = rd.splitlines()
|
|
73
76
|
return lines
|
|
74
77
|
|
|
@@ -94,7 +97,7 @@ def compare_config():
|
|
|
94
97
|
|
|
95
98
|
def checkout(overwrite=False, diff=False):
|
|
96
99
|
"""Do it."""
|
|
97
|
-
for name in
|
|
100
|
+
for name in ['__PYTHON__', '__PYMSCADA__', '__DIR__', '__HOME__']:
|
|
98
101
|
if not PATH[name].exists():
|
|
99
102
|
raise SystemExit(f'{PATH[name]} is missing')
|
|
100
103
|
if diff:
|
|
@@ -2,8 +2,8 @@ bus_ip: 127.0.0.1
|
|
|
2
2
|
bus_port: 1324
|
|
3
3
|
ip: 0.0.0.0
|
|
4
4
|
port: 8324
|
|
5
|
-
get_path: /
|
|
6
|
-
serve_path: /
|
|
5
|
+
get_path: __HOME__/angmscada/dist/angmscada
|
|
6
|
+
serve_path: __HOME__/pymscada
|
|
7
7
|
pages:
|
|
8
8
|
- name: Default Main
|
|
9
9
|
parent:
|
|
@@ -35,6 +35,12 @@ pages:
|
|
|
35
35
|
- Four
|
|
36
36
|
- Five
|
|
37
37
|
- {tagname: FloatSelect, type: selectdict, opts: {type: float, dp: 2}}
|
|
38
|
+
- name: Notes
|
|
39
|
+
parent:
|
|
40
|
+
items:
|
|
41
|
+
- type: opnote
|
|
42
|
+
site: [Site 1, Site 2]
|
|
43
|
+
by: [B1, B2]
|
|
38
44
|
- name: Trends
|
|
39
45
|
parent: Dropdown
|
|
40
46
|
items:
|
|
@@ -81,7 +87,7 @@ pages:
|
|
|
81
87
|
- name: Files
|
|
82
88
|
parent: Dropdown
|
|
83
89
|
items:
|
|
84
|
-
-
|
|
90
|
+
- type: files
|
|
85
91
|
- name: Temperature
|
|
86
92
|
parent: Weather
|
|
87
93
|
items:
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Poll SNMP OIDs from devices."""
|
|
2
2
|
import logging
|
|
3
|
-
import pysnmp.hlapi.asyncio as snmp
|
|
4
3
|
from pymscada.bus_client import BusClient
|
|
5
4
|
from pymscada.periodic import Periodic
|
|
6
5
|
from pymscada.iodrivers.snmp_map import SnmpMaps
|
|
6
|
+
try:
|
|
7
|
+
import pysnmp.hlapi.asyncio as snmp
|
|
8
|
+
except ModuleNotFoundError:
|
|
9
|
+
logging.error('snmp_client not available.')
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class SnmpClientConnector:
|
|
@@ -27,15 +27,16 @@ class OpNotes:
|
|
|
27
27
|
self.cursor = self.connection.cursor()
|
|
28
28
|
query = (
|
|
29
29
|
'CREATE TABLE IF NOT EXISTS ' + self.table +
|
|
30
|
-
'(
|
|
31
|
-
'
|
|
30
|
+
'(id INTEGER PRIMARY KEY ASC, '
|
|
31
|
+
'date_ms INTEGER, '
|
|
32
32
|
'site TEXT, '
|
|
33
|
-
'
|
|
33
|
+
'by TEXT, '
|
|
34
34
|
'note TEXT)'
|
|
35
35
|
)
|
|
36
36
|
self.cursor.execute(query)
|
|
37
37
|
self.busclient = BusClient(bus_ip, bus_port, module='OpNotes')
|
|
38
38
|
self.rta = Tag(rta_tag, dict)
|
|
39
|
+
self.rta.value = {}
|
|
39
40
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
40
41
|
|
|
41
42
|
def rta_cb(self, request):
|
|
@@ -44,64 +45,78 @@ class OpNotes:
|
|
|
44
45
|
logging.warning(f'rta_cb malformed {request}')
|
|
45
46
|
elif request['action'] == 'ADD':
|
|
46
47
|
try:
|
|
48
|
+
logging.info(f'add {request}')
|
|
47
49
|
with self.connection:
|
|
48
50
|
self.cursor.execute(
|
|
49
|
-
f'INSERT INTO {self.table} (
|
|
50
|
-
'
|
|
51
|
-
'RETURNING *;',
|
|
51
|
+
f'INSERT INTO {self.table} (date_ms, site, by, note) '
|
|
52
|
+
'VALUES(:date_ms, :site, :by, :note) RETURNING *;',
|
|
52
53
|
request)
|
|
53
54
|
res = self.cursor.fetchone()
|
|
54
55
|
self.rta.value = {
|
|
55
|
-
'
|
|
56
|
-
'
|
|
56
|
+
'id': res[0],
|
|
57
|
+
'date_ms': res[1],
|
|
57
58
|
'site': res[2],
|
|
58
|
-
'
|
|
59
|
+
'by': res[3],
|
|
59
60
|
'note': res[4]
|
|
60
61
|
}
|
|
61
62
|
except sqlite3.IntegrityError as error:
|
|
62
63
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
63
64
|
elif request['action'] == 'MODIFY':
|
|
64
65
|
try:
|
|
66
|
+
logging.info(f'modify {request}')
|
|
65
67
|
with self.connection:
|
|
66
68
|
self.cursor.execute(
|
|
67
|
-
f'REPLACE INTO {self.table} VALUES(:
|
|
68
|
-
':site, :
|
|
69
|
+
f'REPLACE INTO {self.table} VALUES(:id, :date_ms, '
|
|
70
|
+
':site, :by, :note) RETURNING *;', request)
|
|
69
71
|
res = self.cursor.fetchone()
|
|
70
72
|
self.rta.value = {
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
+
'id': res[0],
|
|
74
|
+
'date_ms': res[1],
|
|
73
75
|
'site': res[2],
|
|
74
|
-
'
|
|
76
|
+
'by': res[3],
|
|
75
77
|
'note': res[4]
|
|
76
78
|
}
|
|
77
79
|
except sqlite3.IntegrityError as error:
|
|
78
80
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
79
81
|
elif request['action'] == 'DELETE':
|
|
80
82
|
try:
|
|
83
|
+
logging.info(f'delete {request}')
|
|
81
84
|
with self.connection:
|
|
82
85
|
self.cursor.execute(
|
|
83
|
-
f'DELETE FROM {self.table} WHERE
|
|
84
|
-
self.rta.value = {'
|
|
86
|
+
f'DELETE FROM {self.table} WHERE id = :id;', request)
|
|
87
|
+
self.rta.value = {'id': request['id']}
|
|
85
88
|
except sqlite3.IntegrityError as error:
|
|
86
89
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
87
90
|
elif request['action'] == 'HISTORY':
|
|
88
|
-
tag = Tag(request['reply_tag'], dict)
|
|
89
91
|
try:
|
|
92
|
+
logging.info(f'history {request}')
|
|
90
93
|
with self.connection:
|
|
91
94
|
self.cursor.execute(
|
|
92
|
-
f'SELECT * FROM {self.table} WHERE
|
|
93
|
-
'
|
|
94
|
-
'LIMIT 2;', request)
|
|
95
|
+
f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
|
|
96
|
+
'ORDER BY (date_ms - :date_ms);', request)
|
|
95
97
|
for res in self.cursor.fetchall():
|
|
96
|
-
|
|
97
|
-
'
|
|
98
|
-
'
|
|
98
|
+
self.rta.value = {
|
|
99
|
+
'__rta_id__': request['__rta_id__'],
|
|
100
|
+
'id': res[0],
|
|
101
|
+
'date_ms': res[1],
|
|
99
102
|
'site': res[2],
|
|
100
|
-
'
|
|
103
|
+
'by': res[3],
|
|
101
104
|
'note': res[4]
|
|
102
105
|
}
|
|
103
106
|
except sqlite3.IntegrityError as error:
|
|
104
107
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
108
|
+
elif request['action'] == 'BULK HISTORY':
|
|
109
|
+
try:
|
|
110
|
+
logging.info(f'bulk history {request}')
|
|
111
|
+
with self.connection:
|
|
112
|
+
self.cursor.execute(
|
|
113
|
+
f'SELECT * FROM {self.table} WHERE date_ms > :date_ms '
|
|
114
|
+
'ORDER BY -date_ms;', request)
|
|
115
|
+
results = list(self.cursor.fetchall())
|
|
116
|
+
self.rta.value = {'__rta_id__': request['__rta_id__'],
|
|
117
|
+
'data': results}
|
|
118
|
+
except sqlite3.IntegrityError as error:
|
|
119
|
+
logging.warning(f'OpNotes rta_cb {error}')
|
|
105
120
|
|
|
106
121
|
async def start(self):
|
|
107
122
|
"""Async startup."""
|
|
@@ -31,7 +31,7 @@ class WSHandler():
|
|
|
31
31
|
This are transitory, lasting for a given web browser client.
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
ids = set(range(1,
|
|
34
|
+
ids = set(range(1, 1000))
|
|
35
35
|
|
|
36
36
|
def __init__(self, ws: web.WebSocketResponse, pages: dict,
|
|
37
37
|
tag_info: dict[str, Tag], do_rta, interface: Interface):
|
|
@@ -57,8 +57,10 @@ class WSHandler():
|
|
|
57
57
|
while True:
|
|
58
58
|
as_bytes, message = await self.queue.get()
|
|
59
59
|
if as_bytes:
|
|
60
|
+
# logging.debug(f'{self.rta_id} as bytes {message}')
|
|
60
61
|
await self.ws.send_bytes(message)
|
|
61
62
|
else:
|
|
63
|
+
# logging.debug(f'{self.rta_id} as json {message}')
|
|
62
64
|
await self.ws.send_json(message)
|
|
63
65
|
except asyncio.CancelledError:
|
|
64
66
|
logging.warn(f'{self.rta_id}: send queue error, close '
|
|
@@ -103,7 +105,7 @@ class WSHandler():
|
|
|
103
105
|
tag.time_us, # Uint64
|
|
104
106
|
asbytes # Char as needed
|
|
105
107
|
)))
|
|
106
|
-
elif tag.type
|
|
108
|
+
elif tag.type is bytes:
|
|
107
109
|
rta_id = unpack_from('>H', tag.value)[0]
|
|
108
110
|
if rta_id in [0, self.rta_id]:
|
|
109
111
|
self.queue.put_nowait((True, pack(
|
|
@@ -115,7 +117,19 @@ class WSHandler():
|
|
|
115
117
|
)))
|
|
116
118
|
else:
|
|
117
119
|
logging.info(f'{self.rta_id}: {tag.name} bytes mismatch id')
|
|
118
|
-
elif tag.type
|
|
120
|
+
elif tag.type is list:
|
|
121
|
+
self.queue.put_nowait((False, {
|
|
122
|
+
'type': 'tag',
|
|
123
|
+
'payload': {
|
|
124
|
+
'tagid': tag.id,
|
|
125
|
+
'time_us': tag.time_us,
|
|
126
|
+
'value': tag.value
|
|
127
|
+
}
|
|
128
|
+
}))
|
|
129
|
+
elif tag.type is dict:
|
|
130
|
+
if '__rta_id__' in tag.value:
|
|
131
|
+
if tag.value['__rta_id__'] != self.rta_id:
|
|
132
|
+
return
|
|
119
133
|
self.queue.put_nowait((False, {
|
|
120
134
|
'type': 'tag',
|
|
121
135
|
'payload': {
|
|
@@ -174,8 +188,8 @@ class WSHandler():
|
|
|
174
188
|
value['_file_data'] = file.data
|
|
175
189
|
value['__rta_id__'] = self.rta_id
|
|
176
190
|
self.do_rta(tagname, value)
|
|
177
|
-
elif action == 'request_to_author':
|
|
178
|
-
|
|
191
|
+
# elif action == 'request_to_author':
|
|
192
|
+
# self.interface.ask(command)
|
|
179
193
|
elif action == 'sub': # pc.CMD_SUB
|
|
180
194
|
self.do_sub(tagname)
|
|
181
195
|
elif action == 'get': # pc.CMD_GET
|
|
@@ -33,24 +33,24 @@ def test_db_and_tag(opnotes_db, opnotes_tag):
|
|
|
33
33
|
tag = opnotes_tag # OpNotes sets the tag value for www clients.
|
|
34
34
|
record = {
|
|
35
35
|
'action': 'ADD',
|
|
36
|
-
'
|
|
36
|
+
'id': 15,
|
|
37
37
|
'site': 'Aniwhenua',
|
|
38
|
-
'
|
|
39
|
-
'
|
|
38
|
+
'by': 'Jamie Walton',
|
|
39
|
+
'date': 1234567890123,
|
|
40
40
|
'note': 'Note °±²³😖.'
|
|
41
41
|
}
|
|
42
42
|
db.rta_cb(record)
|
|
43
|
-
assert tag.value['
|
|
43
|
+
assert tag.value['id'] == 1
|
|
44
44
|
assert tag.value['note'] == 'Note °±²³😖.'
|
|
45
|
-
record['
|
|
45
|
+
record['id'] = tag.value['id']
|
|
46
46
|
record['action'] = 'MODIFY'
|
|
47
47
|
record['note'] = 'hi'
|
|
48
48
|
db.rta_cb(record)
|
|
49
|
-
assert tag.value['
|
|
49
|
+
assert tag.value['id'] == 1
|
|
50
50
|
assert tag.value['note'] == 'hi'
|
|
51
51
|
record['action'] = 'DELETE'
|
|
52
52
|
db.rta_cb(record)
|
|
53
|
-
assert tag.value == {'
|
|
53
|
+
assert tag.value == {'id': 1}
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def test_history_queries(opnotes_db, opnotes_tag, reply_tag):
|
|
@@ -70,20 +70,20 @@ def test_history_queries(opnotes_db, opnotes_tag, reply_tag):
|
|
|
70
70
|
o_tag.add_callback(o_cb, 999)
|
|
71
71
|
r_tag.add_callback(r_cb, 999)
|
|
72
72
|
record = {'action': 'ADD',
|
|
73
|
-
'
|
|
73
|
+
'date': 12345,
|
|
74
74
|
'site': 'Site',
|
|
75
|
-
'
|
|
75
|
+
'by': 'Me',
|
|
76
76
|
'note': 'hi'}
|
|
77
77
|
for i in range(10):
|
|
78
|
-
record['
|
|
79
|
-
db.rta_cb(record) #
|
|
80
|
-
assert o_values[9]['
|
|
78
|
+
record['date'] -= 1
|
|
79
|
+
db.rta_cb(record) # id 1-10
|
|
80
|
+
assert o_values[9]['id'] == 10
|
|
81
81
|
rq = {'action': 'HISTORY',
|
|
82
|
-
'
|
|
82
|
+
'date': 12345 - 3,
|
|
83
83
|
'reply_tag': '__wwwserver__'}
|
|
84
84
|
db.rta_cb(rq)
|
|
85
|
-
assert r_values[1]['
|
|
86
|
-
for i in range(1, 11): # sqlite3
|
|
87
|
-
rq = {'action': 'DELETE', '
|
|
85
|
+
assert r_values[1]['date'] == 12340
|
|
86
|
+
for i in range(1, 11): # sqlite3 id counts from 1
|
|
87
|
+
rq = {'action': 'DELETE', 'id': i}
|
|
88
88
|
db.rta_cb(rq)
|
|
89
|
-
o_values[19] == {'
|
|
89
|
+
o_values[19] == {'id': 10}
|
pymscada-0.1.2/PKG-INFO
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: pymscada
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: Shared tag value SCADA with python backup and Angular UI
|
|
5
|
-
Author-Email: Jamie Walton <jamie@walton.net.nz>
|
|
6
|
-
License: GPL-3.0-or-later
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: JavaScript
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Development Status :: 1 - Planning
|
|
13
|
-
Requires-Python: >=3.9
|
|
14
|
-
Requires-Dist: PyYAML>=6.0.1
|
|
15
|
-
Requires-Dist: aiohttp>=3.8.5
|
|
16
|
-
Requires-Dist: pymscada-html==0.1.0
|
|
17
|
-
Requires-Dist: cerberus>=1.3.5
|
|
18
|
-
Requires-Dist: pycomm3>=1.2.14
|
|
19
|
-
Requires-Dist: pysnmplib>=5.0.24
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
# pymscada
|
|
23
|
-
#### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
|
|
24
|
-
|
|
25
|
-
#### [@Github](https://github.com/jamie0walton/pymscada/blob/main/README.md)
|
|
26
|
-
|
|
27
|
-
## Python Mobile SCADA
|
|
28
|
-
|
|
29
|
-
```pymscada``` read / write to Modbus and Logix PLCs. Read SNMP OIDs.
|
|
30
|
-
Collect history values and provide the ability to set values and trends
|
|
31
|
-
and issue commands.
|
|
32
|
-
|
|
33
|
-
User interface is via a web client embedded in this package. Examples included
|
|
34
|
-
for securing with Apache as a proxy.
|
|
35
|
-
|
|
36
|
-
Configuration with text yaml files, including the web page which are
|
|
37
|
-
procedurally built.
|
|
38
|
-
|
|
39
|
-
# See also
|
|
40
|
-
|
|
41
|
-
- The angular project [angmscada](https://github.com/jamie0walton/angmscada)
|
|
42
|
-
- Python container for the compiled angular pages [pymscada-html](https://github.com/jamie0walton/pymscada-html)
|
|
43
|
-
|
|
44
|
-
# Licence
|
|
45
|
-
|
|
46
|
-
```pymscada``` is distributed under the GPLv3 [license](./LICENSE).
|
|
47
|
-
|
|
48
|
-
# Use
|
|
49
|
-
Checkout the example files.
|
|
50
|
-
```bash
|
|
51
|
-
mscada@raspberrypi:~/test $ pymscada checkout
|
|
52
|
-
making 'history' folder
|
|
53
|
-
making pdf dir
|
|
54
|
-
making config dir
|
|
55
|
-
Creating /home/mscada/test/config/modbusclient.yaml
|
|
56
|
-
Creating /home/mscada/test/config/pymscada-history.service
|
|
57
|
-
Creating /home/mscada/test/config/wwwserver.yaml
|
|
58
|
-
Creating /home/mscada/test/config/pymscada-demo-modbus_plc.service
|
|
59
|
-
Creating /home/mscada/test/config/files.yaml
|
|
60
|
-
Creating /home/mscada/test/config/pymscada-modbusserver.service
|
|
61
|
-
Creating /home/mscada/test/config/pymscada-wwwserver.service
|
|
62
|
-
Creating /home/mscada/test/config/simulate.yaml
|
|
63
|
-
Creating /home/mscada/test/config/tags.yaml
|
|
64
|
-
Creating /home/mscada/test/config/history.yaml
|
|
65
|
-
Creating /home/mscada/test/config/pymscada-files.service
|
|
66
|
-
Creating /home/mscada/test/config/bus.yaml
|
|
67
|
-
Creating /home/mscada/test/config/modbusserver.yaml
|
|
68
|
-
Creating /home/mscada/test/config/modbus_plc.py
|
|
69
|
-
Creating /home/mscada/test/config/pymscada-modbusclient.service
|
|
70
|
-
Creating /home/mscada/test/config/pymscada-bus.service
|
|
71
|
-
Creating /home/mscada/test/config/README.md
|
|
72
|
-
mscada@raspberrypi:~/test $ pymscada validate
|
|
73
|
-
WARNING:root:pymscada 0.1.0 starting
|
|
74
|
-
Config files in ./ valid.
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Runs on a Raspberry Pi and includes preconfigured systemd files to
|
|
78
|
-
automate running the services. Mostly works on Windows, works better
|
|
79
|
-
on linux.
|
|
80
|
-
|
|
81
|
-
Modules can be run from the command line, although you need
|
|
82
|
-
a terminal for each running module (better with systemd).
|
|
83
|
-
```bash
|
|
84
|
-
pymscada bus --config bus.yaml
|
|
85
|
-
pymscada wwwserver --config wwwserver.yaml --tags tags.yaml
|
|
86
|
-
pymscada history --config history.yaml --tags tags.yaml
|
|
87
|
-
python weather.py
|
|
88
|
-
```
|
pymscada-0.1.2/README.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# pymscada
|
|
2
|
-
#### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
|
|
3
|
-
|
|
4
|
-
#### [@Github](https://github.com/jamie0walton/pymscada/blob/main/README.md)
|
|
5
|
-
|
|
6
|
-
## Python Mobile SCADA
|
|
7
|
-
|
|
8
|
-
```pymscada``` read / write to Modbus and Logix PLCs. Read SNMP OIDs.
|
|
9
|
-
Collect history values and provide the ability to set values and trends
|
|
10
|
-
and issue commands.
|
|
11
|
-
|
|
12
|
-
User interface is via a web client embedded in this package. Examples included
|
|
13
|
-
for securing with Apache as a proxy.
|
|
14
|
-
|
|
15
|
-
Configuration with text yaml files, including the web page which are
|
|
16
|
-
procedurally built.
|
|
17
|
-
|
|
18
|
-
# See also
|
|
19
|
-
|
|
20
|
-
- The angular project [angmscada](https://github.com/jamie0walton/angmscada)
|
|
21
|
-
- Python container for the compiled angular pages [pymscada-html](https://github.com/jamie0walton/pymscada-html)
|
|
22
|
-
|
|
23
|
-
# Licence
|
|
24
|
-
|
|
25
|
-
```pymscada``` is distributed under the GPLv3 [license](./LICENSE).
|
|
26
|
-
|
|
27
|
-
# Use
|
|
28
|
-
Checkout the example files.
|
|
29
|
-
```bash
|
|
30
|
-
mscada@raspberrypi:~/test $ pymscada checkout
|
|
31
|
-
making 'history' folder
|
|
32
|
-
making pdf dir
|
|
33
|
-
making config dir
|
|
34
|
-
Creating /home/mscada/test/config/modbusclient.yaml
|
|
35
|
-
Creating /home/mscada/test/config/pymscada-history.service
|
|
36
|
-
Creating /home/mscada/test/config/wwwserver.yaml
|
|
37
|
-
Creating /home/mscada/test/config/pymscada-demo-modbus_plc.service
|
|
38
|
-
Creating /home/mscada/test/config/files.yaml
|
|
39
|
-
Creating /home/mscada/test/config/pymscada-modbusserver.service
|
|
40
|
-
Creating /home/mscada/test/config/pymscada-wwwserver.service
|
|
41
|
-
Creating /home/mscada/test/config/simulate.yaml
|
|
42
|
-
Creating /home/mscada/test/config/tags.yaml
|
|
43
|
-
Creating /home/mscada/test/config/history.yaml
|
|
44
|
-
Creating /home/mscada/test/config/pymscada-files.service
|
|
45
|
-
Creating /home/mscada/test/config/bus.yaml
|
|
46
|
-
Creating /home/mscada/test/config/modbusserver.yaml
|
|
47
|
-
Creating /home/mscada/test/config/modbus_plc.py
|
|
48
|
-
Creating /home/mscada/test/config/pymscada-modbusclient.service
|
|
49
|
-
Creating /home/mscada/test/config/pymscada-bus.service
|
|
50
|
-
Creating /home/mscada/test/config/README.md
|
|
51
|
-
mscada@raspberrypi:~/test $ pymscada validate
|
|
52
|
-
WARNING:root:pymscada 0.1.0 starting
|
|
53
|
-
Config files in ./ valid.
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Runs on a Raspberry Pi and includes preconfigured systemd files to
|
|
57
|
-
automate running the services. Mostly works on Windows, works better
|
|
58
|
-
on linux.
|
|
59
|
-
|
|
60
|
-
Modules can be run from the command line, although you need
|
|
61
|
-
a terminal for each running module (better with systemd).
|
|
62
|
-
```bash
|
|
63
|
-
pymscada bus --config bus.yaml
|
|
64
|
-
pymscada wwwserver --config wwwserver.yaml --tags tags.yaml
|
|
65
|
-
pymscada history --config history.yaml --tags tags.yaml
|
|
66
|
-
python weather.py
|
|
67
|
-
```
|
|
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
|