pymscada 0.0.14__py3-none-any.whl → 0.1.0__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/checkout.py +38 -5
- pymscada/demo/logixclient.yaml +24 -23
- pymscada/demo/modbusclient.yaml +22 -18
- pymscada/demo/modbusserver.yaml +10 -2
- pymscada/demo/ping.yaml +12 -0
- pymscada/demo/pymscada-io-ping.service +15 -0
- pymscada/demo/snmpclient.yaml +17 -17
- pymscada/demo/tags.yaml +50 -1
- pymscada/demo/wwwserver.yaml +447 -1
- pymscada/iodrivers/logix_client.py +9 -9
- pymscada/iodrivers/logix_map.py +74 -62
- pymscada/iodrivers/modbus_client.py +20 -34
- pymscada/iodrivers/modbus_map.py +4 -1
- pymscada/iodrivers/modbus_server.py +34 -48
- pymscada/iodrivers/ping_client.py +120 -0
- pymscada/iodrivers/ping_map.py +43 -0
- pymscada/iodrivers/snmp_client.py +4 -2
- pymscada/iodrivers/snmp_map.py +1 -1
- pymscada/main.py +239 -71
- pymscada/validate.py +127 -35
- pymscada-0.1.0.dist-info/METADATA +88 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/RECORD +28 -26
- pymscada/demo/simulate.yaml +0 -21
- pymscada/simulate.py +0 -66
- pymscada-0.0.14.dist-info/METADATA +0 -251
- /pymscada/demo/{pymscada-modbusclient.service → pymscada-io-modbusclient.service} +0 -0
- /pymscada/demo/{pymscada-modbusserver.service → pymscada-io-modbusserver.service} +0 -0
- /pymscada/{iodrivers → tools}/snmp_client2.py +0 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/WHEEL +0 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/entry_points.txt +0 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pymscada
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
```
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
pymscada-0.0.
|
|
2
|
-
pymscada-0.0.
|
|
3
|
-
pymscada-0.0.
|
|
4
|
-
pymscada-0.0.
|
|
1
|
+
pymscada-0.1.0.dist-info/METADATA,sha256=4k0xZCUfrBdcOG1vnI9reNEvrOg8UKqG6MaN5vp_Oqs,3212
|
|
2
|
+
pymscada-0.1.0.dist-info/WHEEL,sha256=N2J68yzZqJh3mI_Wg92rwhw0rtJDFpZj9bwQIMJgaVg,90
|
|
3
|
+
pymscada-0.1.0.dist-info/entry_points.txt,sha256=AcZZ7HFj8k1ztP6ge-5bdRinYF8glW2s6lFEQG3esN4,57
|
|
4
|
+
pymscada-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
5
|
pymscada/__init__.py,sha256=D_4aEDWkW6xMVQane3CbehBKPxT3FCaDY_QpFwglCe8,653
|
|
6
6
|
pymscada/__main__.py,sha256=WcyVlrYOoDdktJhOoyubTOycMwpayksFdxwelRU5xpQ,272
|
|
7
7
|
pymscada/bus_client.py,sha256=A6EI3OafyOBKd8UVFXS4K39SpmwBoFE-kSIwldVeDKE,8695
|
|
8
8
|
pymscada/bus_server.py,sha256=-Bn4rfdE4OWXE0av459sROnCwAqV0VKFWulF0m1abHE,11255
|
|
9
|
-
pymscada/checkout.py,sha256=
|
|
9
|
+
pymscada/checkout.py,sha256=ISXhwRJbZBRu0cMeVHwgHzMgk9YslhnPtjs7-0lgjRo,3351
|
|
10
10
|
pymscada/config.py,sha256=vwGxieaJBYXiHNQEOYVDFaPuGmnUlCnbNm_W9bugKlc,1851
|
|
11
11
|
pymscada/console.py,sha256=sw1k6eXY53S8qMR-kx8pHOyhXHHt88e4RTcXmC7AQlw,946
|
|
12
12
|
pymscada/demo/README.md,sha256=iNcVbCTkq-d4agLV-979lNRaqf_hbJCn3OFzY-6qfU8,880
|
|
@@ -14,35 +14,37 @@ pymscada/demo/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
|
|
|
14
14
|
pymscada/demo/bus.yaml,sha256=zde5JDo2Yv5s7NvJ569gAEoTDvsvgBwRPxfrYhsxj3w,26
|
|
15
15
|
pymscada/demo/files.yaml,sha256=Uizvo-LEhHCRePGNLdvl9m_Z40qOlT7aZDLE4zTFSMc,169
|
|
16
16
|
pymscada/demo/history.yaml,sha256=mn_Xf4h_bK6vwZVQ0Iz9BzJpwWok2gEgSKbDgEM8AOQ,46
|
|
17
|
-
pymscada/demo/logixclient.yaml,sha256=
|
|
17
|
+
pymscada/demo/logixclient.yaml,sha256=G_NlJhBYwT1a9ceHDgO6fCNKFmBM2pVO_t9Xa1NqlRY,912
|
|
18
18
|
pymscada/demo/modbus_plc.py,sha256=3zZHHbyrdxyryEHBeNIw-fpcDGcS1MaJiqEwQDr6zWI,2397
|
|
19
|
-
pymscada/demo/modbusclient.yaml,sha256=
|
|
20
|
-
pymscada/demo/modbusserver.yaml,sha256=
|
|
19
|
+
pymscada/demo/modbusclient.yaml,sha256=geeCsUJZkkEj7jjXR_Yk6R5zA5Ta9IczrHsARz7ZgXY,1099
|
|
20
|
+
pymscada/demo/modbusserver.yaml,sha256=67_mED6jXgtnzlDIky9Cg4j-nXur06iz9ve3JUwSyG8,1133
|
|
21
|
+
pymscada/demo/ping.yaml,sha256=r_VBGTLU5r4cZi9bIGL3M4eNw70KnoBptOUoNrSbnFY,210
|
|
21
22
|
pymscada/demo/pymscada-bus.service,sha256=rRTFwHaS8XWd9YAIB3cET4QvASaIO9emmxFiUAbl14g,257
|
|
22
23
|
pymscada/demo/pymscada-demo-modbus_plc.service,sha256=jmgk_peoxwKVXe-LbyK2VluMS1JMmoTud4JZHi9Tgec,316
|
|
23
24
|
pymscada/demo/pymscada-files.service,sha256=yGTxYnmQ_QBjIzItFatw_uIg_cG11vadLDaR0C9pPEk,322
|
|
24
25
|
pymscada/demo/pymscada-history.service,sha256=kEU_RsrRSmEUE-nR23n2q2yZxjALm_nCrJhjxS4pcSQ,382
|
|
25
26
|
pymscada/demo/pymscada-io-logixclient.service,sha256=gvnCJgUeqnIgnuN-Wf1XhB0FJxVYyLksmMO7JC0wT-Y,344
|
|
27
|
+
pymscada/demo/pymscada-io-modbusclient.service,sha256=4tenKcrfRi0iMdv8-k2gtMQA4OPTM59zAyKpovwemxM,344
|
|
28
|
+
pymscada/demo/pymscada-io-modbusserver.service,sha256=FqCMD3EJKoiq6EbYnoijRLX5UeUWbZrNzDs50eQj7iE,344
|
|
29
|
+
pymscada/demo/pymscada-io-ping.service,sha256=d1n32srVKGd8qo8JWeBYEEznCRZWRWaBQLOYzdqEXWg,327
|
|
26
30
|
pymscada/demo/pymscada-io-snmpclient.service,sha256=wrA2kDR3bgO30lP_lNJrIsVXNQiXmWKnphoqUj3QTRI,339
|
|
27
|
-
pymscada/demo/pymscada-modbusclient.service,sha256=4tenKcrfRi0iMdv8-k2gtMQA4OPTM59zAyKpovwemxM,344
|
|
28
|
-
pymscada/demo/pymscada-modbusserver.service,sha256=FqCMD3EJKoiq6EbYnoijRLX5UeUWbZrNzDs50eQj7iE,344
|
|
29
31
|
pymscada/demo/pymscada-wwwserver.service,sha256=uDnqzfvAdAnTrqOCqDm1PN7SmeMSuOdmAhorHPJdEVI,366
|
|
30
|
-
pymscada/demo/
|
|
31
|
-
pymscada/demo/
|
|
32
|
-
pymscada/demo/
|
|
33
|
-
pymscada/demo/wwwserver.yaml,sha256=KDbe3mmslcK1L0ZnuvtOhhuJG-Hyw9OztMz813pChYw,1920
|
|
32
|
+
pymscada/demo/snmpclient.yaml,sha256=z8iACrFvMftYUtqGrRjPZYZTpn7aOXI-Kp675NAM8cU,2013
|
|
33
|
+
pymscada/demo/tags.yaml,sha256=GH90X3QRBANUhvd2E9OuyIoiZD25OBihHHlDBw1uzlw,4231
|
|
34
|
+
pymscada/demo/wwwserver.yaml,sha256=jyboShIbwkwgMAcFLkpn88VMvGfoc7a44jNrI_B6OqY,12493
|
|
34
35
|
pymscada/files.py,sha256=kEkiD7j5k69EK4jfMHEp-lbTnulYyrtUjkQDa5xmGPc,1784
|
|
35
36
|
pymscada/history.py,sha256=yAUfjzo4O3w9-hGEAbtvz9UseMTwRVz7NqS832RtvIs,9455
|
|
36
37
|
pymscada/iodrivers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
-
pymscada/iodrivers/logix_client.py,sha256=
|
|
38
|
-
pymscada/iodrivers/logix_map.py,sha256=
|
|
39
|
-
pymscada/iodrivers/modbus_client.py,sha256=
|
|
40
|
-
pymscada/iodrivers/modbus_map.py,sha256=
|
|
41
|
-
pymscada/iodrivers/modbus_server.py,sha256=
|
|
42
|
-
pymscada/iodrivers/
|
|
43
|
-
pymscada/iodrivers/
|
|
44
|
-
pymscada/iodrivers/
|
|
45
|
-
pymscada/
|
|
38
|
+
pymscada/iodrivers/logix_client.py,sha256=wDhl0pCqs_k7Hd1MtbOuBrEx9emOwDrLJqBNussH1qU,2806
|
|
39
|
+
pymscada/iodrivers/logix_map.py,sha256=ljjBAMJcw199v1V5u0Yfl38U6zbZzba5mdY4I3ZvdIM,5401
|
|
40
|
+
pymscada/iodrivers/modbus_client.py,sha256=mo7VRLWi4W2J5Ft3M6Y5jSbK5vbd6wn4R2dncbiL4nU,9529
|
|
41
|
+
pymscada/iodrivers/modbus_map.py,sha256=af2J3CGSeYQ4mSy8rNsERp9z7fRgRUYk3it5Mrc_IQA,7255
|
|
42
|
+
pymscada/iodrivers/modbus_server.py,sha256=nnUjd6iCny_Abylk06J8dq4slNAyNPQC2Rw2ZWEElKM,6507
|
|
43
|
+
pymscada/iodrivers/ping_client.py,sha256=dIR4SuKeNCu2FyhUNfTbkv-El3qPFMb0zUG1GJSD-80,4166
|
|
44
|
+
pymscada/iodrivers/ping_map.py,sha256=EbOteqfEYKIOMqPymROJ4now2If-ekEj6jnM5hthoSA,1403
|
|
45
|
+
pymscada/iodrivers/snmp_client.py,sha256=e-ea5BQnp-PxTetM9d61Mm-GkQrNZkUUlNp1Km9ZasI,2601
|
|
46
|
+
pymscada/iodrivers/snmp_map.py,sha256=sDdIR5ZPAETpozDfBt_XQiZ-f4t99UCPlzj7BxFxQyM,2369
|
|
47
|
+
pymscada/main.py,sha256=LxFUuijrIuNLOGhNy_5yNIq4pvNz4npsUwHaDtvrdCQ,7191
|
|
46
48
|
pymscada/misc.py,sha256=0Cj6OFhQonyhyk9x0BG5MiS-6EPk_w6zvavt8o_Hlf0,622
|
|
47
49
|
pymscada/pdf/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
|
|
48
50
|
pymscada/pdf/one.pdf,sha256=eoJ45DrAjVZrwmwdA_EAz1fwmT44eRnt_tkc2pmMrKY,1488
|
|
@@ -50,9 +52,9 @@ pymscada/pdf/two.pdf,sha256=TAuW5yLU1_wfmTH_I5ezHwY0pxhCVuZh3ixu0kwmJwE,1516
|
|
|
50
52
|
pymscada/periodic.py,sha256=MLlL93VLvFqBBgjO1Us1t0aLHTZ5BFdW0B__G02T1nQ,1235
|
|
51
53
|
pymscada/protocol_constants.py,sha256=ndFeuzhWjKTr_ahvmGJc6Cs5pYkHM8CRgNNDe0Tqecs,1997
|
|
52
54
|
pymscada/samplers.py,sha256=t0IscgsCm5YByioOZ6aOKMO_guDFS_wxnJSiOGKI4Nw,2583
|
|
53
|
-
pymscada/simulate.py,sha256=39PfIc_jJC4ekuSEqlVb8SlxvIjwgyTkCL5fSlwmanc,2404
|
|
54
55
|
pymscada/tag.py,sha256=Q2EgacTnsxnGLM_zoHYsVWdwmd1faqfbxw9byI5fCgY,9463
|
|
56
|
+
pymscada/tools/snmp_client2.py,sha256=pdn5dYyEv4q-ubA0zQ8X-3tQDYxGC7f7Xexa7QPaL40,1675
|
|
55
57
|
pymscada/tools/walk.py,sha256=OgpprUbKLhEWMvJGfU1ckUt_PFEpwZVOD8HucCgzmOc,1625
|
|
56
|
-
pymscada/validate.py,sha256=
|
|
58
|
+
pymscada/validate.py,sha256=VPpAVEwfgori5OREEwWlbPoPxz5Tfqr6dw-O5pINHyI,13125
|
|
57
59
|
pymscada/www_server.py,sha256=BW6-ctE5BgyFDiyUJsOpe3c06DRp8k83MCGjwPh5fco,11473
|
|
58
|
-
pymscada-0.0.
|
|
60
|
+
pymscada-0.1.0.dist-info/RECORD,,
|
pymscada/demo/simulate.yaml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
bus_ip: 127.0.0.1
|
|
2
|
-
bus_port: 1324
|
|
3
|
-
process:
|
|
4
|
-
one:
|
|
5
|
-
type: sine
|
|
6
|
-
offset: 50
|
|
7
|
-
amplitude: 40
|
|
8
|
-
frequency: 0.0005
|
|
9
|
-
sub:
|
|
10
|
-
offset: FloatSet
|
|
11
|
-
pub:
|
|
12
|
-
result: FloatVal
|
|
13
|
-
two:
|
|
14
|
-
type: sawtooth
|
|
15
|
-
offset: 10
|
|
16
|
-
amplitude: 80
|
|
17
|
-
frequency: 0.00005
|
|
18
|
-
sub:
|
|
19
|
-
offset: IntSet
|
|
20
|
-
pub:
|
|
21
|
-
result: IntVal
|
pymscada/simulate.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"""Simulate logic."""
|
|
2
|
-
import asyncio
|
|
3
|
-
import logging
|
|
4
|
-
import math
|
|
5
|
-
from pymscada.bus_client import BusClient
|
|
6
|
-
from pymscada.misc import find_nodes
|
|
7
|
-
from pymscada.tag import Tag, TYPES
|
|
8
|
-
from time import monotonic
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Simulate():
|
|
12
|
-
"""Modify tag values to simulate a real process."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
|
|
15
|
-
tag_info: dict = {}, process: dict = {}) -> None:
|
|
16
|
-
"""
|
|
17
|
-
Connect to bus on bus_ip:bus_port, modify tag values per process.
|
|
18
|
-
|
|
19
|
-
Event loop must be running.
|
|
20
|
-
"""
|
|
21
|
-
self.busclient = BusClient(bus_ip, bus_port, tag_info)
|
|
22
|
-
self.process = process
|
|
23
|
-
self.tags = {}
|
|
24
|
-
for x in ['sub', 'pub']: # resist the temptation to
|
|
25
|
-
for t in find_nodes(x, self.process):
|
|
26
|
-
for tagname in t[x].values():
|
|
27
|
-
self.tags[tagname] = Tag(
|
|
28
|
-
tagname, TYPES[tag_info[tagname]['type']])
|
|
29
|
-
|
|
30
|
-
def step(self):
|
|
31
|
-
"""Do simulation step."""
|
|
32
|
-
for e in self.process.values():
|
|
33
|
-
time = monotonic()
|
|
34
|
-
tagname = e['pub']['result']
|
|
35
|
-
if e['type'] == 'sine':
|
|
36
|
-
result = e['offset'] + e['amplitude'] * \
|
|
37
|
-
math.sin(e['frequency'] * time)
|
|
38
|
-
elif e['type'] == 'sawtooth':
|
|
39
|
-
period = 1 / e['frequency']
|
|
40
|
-
result = int(e['offset'] + e['amplitude'] * (time %
|
|
41
|
-
period) / period)
|
|
42
|
-
else:
|
|
43
|
-
return
|
|
44
|
-
if self.tags[tagname].value is None or \
|
|
45
|
-
abs(self.tags[tagname].value - result) > 0.01:
|
|
46
|
-
self.tags[tagname].value = result
|
|
47
|
-
logging.warning(f'simulate {tagname} {result}')
|
|
48
|
-
|
|
49
|
-
async def periodic(self):
|
|
50
|
-
"""Run simulation step every 5 seconds."""
|
|
51
|
-
while True:
|
|
52
|
-
self.next_run += 1.0
|
|
53
|
-
self.step()
|
|
54
|
-
self.last_ran = monotonic()
|
|
55
|
-
sleep_time = self.next_run - self.last_ran
|
|
56
|
-
if sleep_time < 0:
|
|
57
|
-
self.next_run = self.last_ran
|
|
58
|
-
logging.warning(f'Health count skipped at {self.last_ran}')
|
|
59
|
-
else:
|
|
60
|
-
await asyncio.sleep(sleep_time)
|
|
61
|
-
|
|
62
|
-
async def start(self):
|
|
63
|
-
"""Provide the simulation process."""
|
|
64
|
-
await self.busclient.start()
|
|
65
|
-
self.next_run = monotonic()
|
|
66
|
-
await asyncio.create_task(self.periodic())
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: pymscada
|
|
3
|
-
Version: 0.0.14
|
|
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
|
-
This is a small SCADA package that will run on Linux (preferably) or
|
|
30
|
-
Windows. The server runs as several modules on the host, sharing
|
|
31
|
-
information through a message bus. A __subset__ of modules is:
|
|
32
|
-
|
|
33
|
-
- Bus server - shares tag values with by exception updates
|
|
34
|
-
- Modbus client - reads and writes to a PLC using Modbus/TCP
|
|
35
|
-
- History - saves data changes, serves history to web pages
|
|
36
|
-
- Web server - serves web pages which connect with a web socket
|
|
37
|
-
- Web pages - an Angular single page web application
|
|
38
|
-
|
|
39
|
-
Web pages are responsive and defined procedurally from the
|
|
40
|
-
```wwwserver.yaml``` config file.
|
|
41
|
-
|
|
42
|
-
Trends use [uPlot](https://github.com/leeoniya/uPlot).
|
|
43
|
-
|
|
44
|
-
## Objectives
|
|
45
|
-
|
|
46
|
-
Traditional SCADA has a fixed 19:6, 1920x1080 or some equivalent layout.
|
|
47
|
-
It's great on a big screen but not good on a phone. Hence __Mobile__
|
|
48
|
-
SCADA with a responsive layout.
|
|
49
|
-
|
|
50
|
-
I wrote Mobile SCADA to provide a GUI to the other things I was trying to
|
|
51
|
-
do, I wanted to leverage web browsers and eliminate a dedicated
|
|
52
|
-
_viewer.exe_. Display on the client is fast, trends, as fast as I can
|
|
53
|
-
make them.
|
|
54
|
-
|
|
55
|
-
Uptimes should be excellent. The best I have on an earlier version is
|
|
56
|
-
over 5 years for about half of the script modules. This version is a
|
|
57
|
-
complete rewrite, however the aim is the same.
|
|
58
|
-
|
|
59
|
-
All tag value updates are by exception. So an update from you setting a
|
|
60
|
-
value to seeing the feedback should be __FAST__.
|
|
61
|
-
|
|
62
|
-
# See also
|
|
63
|
-
|
|
64
|
-
- The angular project [angmscada](https://github.com/jamie0walton/angmscada)
|
|
65
|
-
- Python container for the compiled angular pages [pymscada-html](https://github.com/jamie0walton/pymscada-html)
|
|
66
|
-
- An add-on IO driver for Rockwell using pycomm3 [pymscada-pycomm3](https://github.com/jamie0walton/pymscada-pycomm3)
|
|
67
|
-
|
|
68
|
-
# Licence
|
|
69
|
-
|
|
70
|
-
```pymscada``` is distributed under the GPLv3 [license](./LICENSE).
|
|
71
|
-
|
|
72
|
-
# Example Use
|
|
73
|
-
This was all run on a Raspberry Pi 3B+ with a 16GB SDRAM card.
|
|
74
|
-
|
|
75
|
-
## First
|
|
76
|
-
Checkout the example files. Start in an empty directory. Plan to keep
|
|
77
|
-
in the directory you check out into as the config file path details
|
|
78
|
-
are auto-generated for the location you check out in to.
|
|
79
|
-
```bash
|
|
80
|
-
mscada@raspberrypi:~/test $ pymscada checkout
|
|
81
|
-
making 'history' folder
|
|
82
|
-
making pdf dir
|
|
83
|
-
making config dir
|
|
84
|
-
Creating /home/mscada/test/config/modbusclient.yaml
|
|
85
|
-
Creating /home/mscada/test/config/pymscada-history.service
|
|
86
|
-
Creating /home/mscada/test/config/wwwserver.yaml
|
|
87
|
-
Creating /home/mscada/test/config/pymscada-demo-modbus_plc.service
|
|
88
|
-
Creating /home/mscada/test/config/files.yaml
|
|
89
|
-
Creating /home/mscada/test/config/pymscada-modbusserver.service
|
|
90
|
-
Creating /home/mscada/test/config/pymscada-wwwserver.service
|
|
91
|
-
Creating /home/mscada/test/config/simulate.yaml
|
|
92
|
-
Creating /home/mscada/test/config/tags.yaml
|
|
93
|
-
Creating /home/mscada/test/config/history.yaml
|
|
94
|
-
Creating /home/mscada/test/config/pymscada-files.service
|
|
95
|
-
Creating /home/mscada/test/config/bus.yaml
|
|
96
|
-
Creating /home/mscada/test/config/modbusserver.yaml
|
|
97
|
-
Creating /home/mscada/test/config/modbus_plc.py
|
|
98
|
-
Creating /home/mscada/test/config/pymscada-modbusclient.service
|
|
99
|
-
Creating /home/mscada/test/config/pymscada-bus.service
|
|
100
|
-
Creating /home/mscada/test/config/README.md
|
|
101
|
-
mscada@raspberrypi:~/test $
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Objective
|
|
105
|
-
To show a trend of the temperature forecast with a custom pymscada bus
|
|
106
|
-
client program. The end result should look like ...
|
|
107
|
-
|
|
108
|
-

|
|
109
|
-
|
|
110
|
-
## Configuration
|
|
111
|
-
### Bus
|
|
112
|
-
Defaults in ```bus.yaml``` are fine.
|
|
113
|
-
|
|
114
|
-
### Tags
|
|
115
|
-
Add some tags in ```tags.yaml```:
|
|
116
|
-
```yaml
|
|
117
|
-
temperature:
|
|
118
|
-
desc: temperature
|
|
119
|
-
type: float
|
|
120
|
-
min: 0
|
|
121
|
-
max: 35
|
|
122
|
-
units: C
|
|
123
|
-
dp: 1
|
|
124
|
-
temperature_01:
|
|
125
|
-
desc: temperature_01
|
|
126
|
-
type: float
|
|
127
|
-
min: 0
|
|
128
|
-
max: 35
|
|
129
|
-
units: C
|
|
130
|
-
dp: 1
|
|
131
|
-
... etc.
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### History
|
|
135
|
-
Defaults in ```history.yaml``` are fine.
|
|
136
|
-
|
|
137
|
-
### Web Server
|
|
138
|
-
You will need to add a trend page to ```wwwserver.yaml``` as:
|
|
139
|
-
```yaml
|
|
140
|
-
- name: Temperature # Creates a Temperature page in the web client
|
|
141
|
-
parent: Weather # Add the Temperature page in a submenu under Weather
|
|
142
|
-
items:
|
|
143
|
-
- type: uplot # Identify the Angular component to use
|
|
144
|
-
ms:
|
|
145
|
-
desc: Temperature
|
|
146
|
-
age: 172800
|
|
147
|
-
legend_pos: left
|
|
148
|
-
time_pos: left
|
|
149
|
-
time_res: m
|
|
150
|
-
axes:
|
|
151
|
-
- scale: x
|
|
152
|
-
range: [-604800, 86400] # initial time range for the trend
|
|
153
|
-
- scale: 'C'
|
|
154
|
-
range: [0.0, 35.0]
|
|
155
|
-
dp: 1
|
|
156
|
-
series:
|
|
157
|
-
- tagname: temperature # pymscada Tag name
|
|
158
|
-
label: Current Temperature
|
|
159
|
-
scale: 'C'
|
|
160
|
-
color: black # standard html colour names
|
|
161
|
-
width: 2
|
|
162
|
-
dp: 1 # number of decimal places
|
|
163
|
-
... etc for additional series
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Your custom pymscada Module
|
|
167
|
-
For this example I polled [tomorrow.io](https://www.tomorrow.io/weather-api/)
|
|
168
|
-
|
|
169
|
-
```weather.py```
|
|
170
|
-
```python
|
|
171
|
-
from datetime import datetime
|
|
172
|
-
import time
|
|
173
|
-
from pymscada import BusClient, Periodic, Tag
|
|
174
|
-
|
|
175
|
-
URL = 'https://api.tomorrow.io/v4/timelines'
|
|
176
|
-
QUERY = {'location': '-43.527934570040124, 172.6415203551829',
|
|
177
|
-
'fields': ['temperature'],
|
|
178
|
-
'units': 'metric',
|
|
179
|
-
'timesteps': '1h',
|
|
180
|
-
'startTime': 'now',
|
|
181
|
-
'endTime': 'nowPlus24h',
|
|
182
|
-
'apikey': '<your key>'}
|
|
183
|
-
|
|
184
|
-
class PollWeather():
|
|
185
|
-
def __init__(self):
|
|
186
|
-
self.tags = {}
|
|
187
|
-
for tagname in ['temperature', 'temperature_01', 'temperature_04',
|
|
188
|
-
'temperature_12', 'temperature_24']:
|
|
189
|
-
# Create pymscada tags, tags are singletons by 'tagname'
|
|
190
|
-
self.tags[tagname] = Tag(tagname, float)
|
|
191
|
-
|
|
192
|
-
async def periodic(self):
|
|
193
|
-
now = int(time.time())
|
|
194
|
-
if now % 3600 != 120:
|
|
195
|
-
return
|
|
196
|
-
# Get the weather forecast from tomorrow.io
|
|
197
|
-
async with aiohttp.ClientSession() as session:
|
|
198
|
-
async with session.get(URL, params=QUERY) as resp:
|
|
199
|
-
response = await resp.json()
|
|
200
|
-
utc_now = None
|
|
201
|
-
for row in response['data']['timelines'][0]['intervals']:
|
|
202
|
-
convert = row['startTime'].replace('Z', '+0000')
|
|
203
|
-
utc = datetime.strptime(convert, '%Y-%m-%dT%H:%M:%S%z').timestamp()
|
|
204
|
-
if utc_now is None:
|
|
205
|
-
utc_now = utc
|
|
206
|
-
forecast = ''
|
|
207
|
-
else:
|
|
208
|
-
forecast = f'_{int((utc - utc_now) / 3600):02d}'
|
|
209
|
-
if forecast not in ['', '_01', '_04', '_12', '_24']:
|
|
210
|
-
continue
|
|
211
|
-
for k, v in row['values'].items():
|
|
212
|
-
ftag = k + forecast
|
|
213
|
-
value = float(v)
|
|
214
|
-
time_us = int(utc * 1000000)
|
|
215
|
-
if ftag in self.tags:
|
|
216
|
-
# Write the tag value. This is one of the following:
|
|
217
|
-
# - value # time_us, bus_id auto-generated
|
|
218
|
-
# - value, time_us # bus_id auto-generated
|
|
219
|
-
# - value, time_us, bus_id # Don't use this one
|
|
220
|
-
self.tags[ftag].value = value, time_us
|
|
221
|
-
|
|
222
|
-
async def main():
|
|
223
|
-
# Connect to the bus and poll the weather service.
|
|
224
|
-
bus = BusClient()
|
|
225
|
-
await bus.start()
|
|
226
|
-
weather = PollWeather() # demo function
|
|
227
|
-
periodic = Periodic(weather.periodic, 1.0) # part of pymscada
|
|
228
|
-
await periodic.start()
|
|
229
|
-
await asyncio.get_event_loop().create_future() # run forever
|
|
230
|
-
|
|
231
|
-
if __name__ == '__main__':
|
|
232
|
-
asyncio.run(main())
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
# Run the modules
|
|
236
|
-
You can run the modules in one of: individual terminals, ```nohup ... &``` or as a
|
|
237
|
-
```systemd``` service. I run as a service, the snips are abbreviated (no path) from
|
|
238
|
-
the exec line in the auto-generated service files.
|
|
239
|
-
|
|
240
|
-
Run the bus first! This needs to remain running all the time. It does not need to
|
|
241
|
-
know the tagnames in advance so it can run forever for most tests. It will gather
|
|
242
|
-
dead tagnames over time as you are experimenting, however this only requires a
|
|
243
|
-
small amount of memory (unless you are setting tag values in the MB - which does
|
|
244
|
-
work).
|
|
245
|
-
|
|
246
|
-
```bash
|
|
247
|
-
pymscada bus --config bus.yaml
|
|
248
|
-
pymscada wwwserver --config wwwserver.yaml --tags tags.yaml
|
|
249
|
-
pymscada history --config history.yaml --tags tags.yaml
|
|
250
|
-
python weather.py
|
|
251
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|