pymscada 0.1.11b10__py3-none-any.whl → 0.2.0rc2__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/bus_client.py +4 -3
- pymscada/bus_server.py +14 -2
- pymscada/checkout.py +87 -89
- pymscada/console.py +2 -2
- pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/demo/openweather.yaml +1 -1
- pymscada/demo/{pymscada-io-accuweather.service → pymscada-io-openweather.service} +2 -2
- pymscada/files.py +3 -3
- pymscada/history.py +61 -6
- pymscada/iodrivers/openweather.py +131 -50
- pymscada/main.py +1 -1
- pymscada/module_config.py +14 -10
- pymscada/opnotes.py +76 -16
- pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/protocol_constants.py +51 -33
- pymscada/tag.py +18 -0
- pymscada/www_server.py +43 -10
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0rc2.dist-info}/METADATA +7 -6
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0rc2.dist-info}/RECORD +27 -24
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0rc2.dist-info}/WHEEL +2 -1
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0rc2.dist-info}/entry_points.txt +0 -3
- pymscada-0.2.0rc2.dist-info/top_level.txt +1 -0
- {pymscada-0.1.11b10.dist-info/licenses → pymscada-0.2.0rc2.dist-info}/LICENSE +0 -0
pymscada/main.py
CHANGED
|
@@ -39,7 +39,7 @@ async def run():
|
|
|
39
39
|
if not options.verbose:
|
|
40
40
|
root_logger.setLevel(logging.WARNING)
|
|
41
41
|
factory = ModuleFactory()
|
|
42
|
-
module = factory.create_module(options
|
|
42
|
+
module = factory.create_module(options)
|
|
43
43
|
if module is not None:
|
|
44
44
|
if hasattr(module, 'start'):
|
|
45
45
|
await module.start()
|
pymscada/module_config.py
CHANGED
|
@@ -75,9 +75,10 @@ def create_module_registry():
|
|
|
75
75
|
ModuleDefinition(
|
|
76
76
|
name='checkout',
|
|
77
77
|
help='create example config files',
|
|
78
|
-
module_class='pymscada.checkout:
|
|
78
|
+
module_class='pymscada.checkout:Checkout',
|
|
79
79
|
config=False,
|
|
80
80
|
tags=False,
|
|
81
|
+
await_future=False,
|
|
81
82
|
epilog=dedent("""
|
|
82
83
|
To add to systemd:
|
|
83
84
|
su -
|
|
@@ -156,12 +157,12 @@ def create_module_registry():
|
|
|
156
157
|
like to see correctly typed values and set values."""),
|
|
157
158
|
extra_args=[
|
|
158
159
|
ModuleArgument(
|
|
159
|
-
('-p', '--port'),
|
|
160
|
+
('-p', '--bus-port'),
|
|
160
161
|
{'action': 'store', 'type': int, 'default': 1324,
|
|
161
162
|
'help': 'connect to port (default: 1324)'}
|
|
162
163
|
),
|
|
163
164
|
ModuleArgument(
|
|
164
|
-
('-i', '--ip'),
|
|
165
|
+
('-i', '--bus-ip'),
|
|
165
166
|
{'action': 'store', 'default': 'localhost',
|
|
166
167
|
'help': 'connect to ip address (default: localhost)'}
|
|
167
168
|
)
|
|
@@ -208,9 +209,11 @@ class ModuleFactory:
|
|
|
208
209
|
parser.add_argument(*arg.args, **arg.kwargs)
|
|
209
210
|
return parser
|
|
210
211
|
|
|
211
|
-
def create_module(self,
|
|
212
|
+
def create_module(self, options: argparse.Namespace):
|
|
212
213
|
"""Create a module instance based on configuration and options."""
|
|
213
|
-
|
|
214
|
+
if options.module_name not in self.modules:
|
|
215
|
+
raise ValueError(f'{options.module_name} does not exist')
|
|
216
|
+
module_def = self.modules[options.module_name]
|
|
214
217
|
logging.info(f'Python Mobile SCADA {version("pymscada")} '
|
|
215
218
|
f'starting {module_def.name}')
|
|
216
219
|
# Import the module class only when needed
|
|
@@ -220,13 +223,14 @@ class ModuleFactory:
|
|
|
220
223
|
actual_class = getattr(module, class_name)
|
|
221
224
|
else:
|
|
222
225
|
actual_class = module_def.module_class
|
|
223
|
-
|
|
224
226
|
kwargs = {}
|
|
225
227
|
if module_def.config:
|
|
226
|
-
kwargs.update(Config(options.config))
|
|
228
|
+
kwargs.update(Config(options.config))
|
|
227
229
|
if module_def.tags:
|
|
228
230
|
kwargs['tag_info'] = dict(Config(options.tags))
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
if module_def.extra_args:
|
|
232
|
+
for arg in module_def.extra_args:
|
|
233
|
+
arg_name = arg.args[-1].lstrip('-').replace('-', '_')
|
|
234
|
+
if hasattr(options, arg_name):
|
|
235
|
+
kwargs[arg_name] = getattr(options, arg_name)
|
|
232
236
|
return actual_class(**kwargs)
|
pymscada/opnotes.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Operator Notes."""
|
|
2
2
|
import logging
|
|
3
3
|
import sqlite3 # note that sqlite3 has blocking calls
|
|
4
|
+
import socket
|
|
4
5
|
from pymscada.bus_client import BusClient
|
|
5
6
|
from pymscada.tag import Tag
|
|
6
7
|
|
|
@@ -8,36 +9,90 @@ from pymscada.tag import Tag
|
|
|
8
9
|
class OpNotes:
|
|
9
10
|
"""Connect to bus_ip:bus_port, store and provide Operator Notes."""
|
|
10
11
|
|
|
11
|
-
def __init__(
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
bus_ip: str = '127.0.0.1',
|
|
15
|
+
bus_port: int = 1324,
|
|
16
|
+
db: str | None = None,
|
|
17
|
+
table: str = 'opnotes',
|
|
18
|
+
rta_tag: str = '__opnotes__'
|
|
19
|
+
) -> None:
|
|
14
20
|
"""
|
|
15
21
|
Connect to bus_ip:bus_port, serve and update operator notes database.
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
Open an Operator notes table, creating if necessary. Provide additions,
|
|
24
|
+
updates, deletions and history requests via the rta_tag.
|
|
19
25
|
|
|
20
26
|
Event loop must be running.
|
|
21
27
|
"""
|
|
22
28
|
if db is None:
|
|
23
29
|
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')
|
|
38
|
+
if not isinstance(rta_tag, str) or not rta_tag:
|
|
39
|
+
raise ValueError('rta_tag must be a non-empty string')
|
|
40
|
+
if not isinstance(table, str) or not table:
|
|
41
|
+
raise ValueError('table must be a non-empty string')
|
|
42
|
+
|
|
24
43
|
logging.warning(f'OpNotes {bus_ip} {bus_port} {db} {rta_tag}')
|
|
25
44
|
self.connection = sqlite3.connect(db)
|
|
26
45
|
self.table = table
|
|
27
46
|
self.cursor = self.connection.cursor()
|
|
47
|
+
self._init_table()
|
|
48
|
+
self.busclient = BusClient(bus_ip, bus_port, module='OpNotes')
|
|
49
|
+
self.rta = Tag(rta_tag, dict)
|
|
50
|
+
self.rta.value = {}
|
|
51
|
+
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
52
|
+
|
|
53
|
+
def _init_table(self):
|
|
54
|
+
"""Initialize or upgrade the database table schema."""
|
|
28
55
|
query = (
|
|
29
56
|
'CREATE TABLE IF NOT EXISTS ' + self.table +
|
|
30
57
|
'(id INTEGER PRIMARY KEY ASC, '
|
|
31
58
|
'date_ms INTEGER, '
|
|
32
59
|
'site TEXT, '
|
|
33
60
|
'by TEXT, '
|
|
34
|
-
'note TEXT
|
|
61
|
+
'note TEXT, '
|
|
62
|
+
'abnormal INTEGER)'
|
|
35
63
|
)
|
|
36
64
|
self.cursor.execute(query)
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
self.cursor.execute(f"PRAGMA table_info({self.table})")
|
|
66
|
+
columns = {col[1]: col[2] for col in self.cursor.fetchall()}
|
|
67
|
+
if 'abnormal' not in columns:
|
|
68
|
+
# Add abnormal as INTEGER from original schema
|
|
69
|
+
logging.warning(f'Upgrading {self.table} schema to include '
|
|
70
|
+
'abnormal INTEGER, CANNOT revert automatically!')
|
|
71
|
+
self.cursor.execute(
|
|
72
|
+
f'ALTER TABLE {self.table} ADD COLUMN abnormal INTEGER')
|
|
73
|
+
elif columns['abnormal'].upper() == 'BOOLEAN':
|
|
74
|
+
# Change abnormal from BOOLEAN to INTEGER
|
|
75
|
+
logging.warning(f'Upgrading {self.table} abnormal from BOOLEAN to '
|
|
76
|
+
'INTEGER, CANNOT revert automatically!')
|
|
77
|
+
self.cursor.execute(
|
|
78
|
+
f'CREATE TABLE {self.table}_new '
|
|
79
|
+
'(id INTEGER PRIMARY KEY ASC, '
|
|
80
|
+
'date_ms INTEGER, '
|
|
81
|
+
'site TEXT, '
|
|
82
|
+
'by TEXT, '
|
|
83
|
+
'note TEXT, '
|
|
84
|
+
'abnormal INTEGER)'
|
|
85
|
+
)
|
|
86
|
+
self.cursor.execute(
|
|
87
|
+
f'INSERT INTO {self.table}_new '
|
|
88
|
+
f'SELECT id, date_ms, site, by, note, '
|
|
89
|
+
f'CASE WHEN abnormal THEN 1 ELSE 0 END '
|
|
90
|
+
f'FROM {self.table}'
|
|
91
|
+
)
|
|
92
|
+
self.cursor.execute(f'DROP TABLE {self.table}')
|
|
93
|
+
self.cursor.execute(
|
|
94
|
+
f'ALTER TABLE {self.table}_new RENAME TO {self.table}'
|
|
95
|
+
)
|
|
41
96
|
|
|
42
97
|
def rta_cb(self, request):
|
|
43
98
|
"""Respond to Request to Author and publish on rta_tag as needed."""
|
|
@@ -48,8 +103,10 @@ class OpNotes:
|
|
|
48
103
|
logging.info(f'add {request}')
|
|
49
104
|
with self.connection:
|
|
50
105
|
self.cursor.execute(
|
|
51
|
-
f'INSERT INTO {self.table}
|
|
52
|
-
'
|
|
106
|
+
f'INSERT INTO {self.table} '
|
|
107
|
+
'(date_ms, site, by, note, abnormal) '
|
|
108
|
+
'VALUES(:date_ms, :site, :by, :note, :abnormal) '
|
|
109
|
+
'RETURNING *;',
|
|
53
110
|
request)
|
|
54
111
|
res = self.cursor.fetchone()
|
|
55
112
|
self.rta.value = {
|
|
@@ -57,7 +114,8 @@ class OpNotes:
|
|
|
57
114
|
'date_ms': res[1],
|
|
58
115
|
'site': res[2],
|
|
59
116
|
'by': res[3],
|
|
60
|
-
'note': res[4]
|
|
117
|
+
'note': res[4],
|
|
118
|
+
'abnormal': res[5]
|
|
61
119
|
}
|
|
62
120
|
except sqlite3.IntegrityError as error:
|
|
63
121
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
@@ -67,14 +125,15 @@ class OpNotes:
|
|
|
67
125
|
with self.connection:
|
|
68
126
|
self.cursor.execute(
|
|
69
127
|
f'REPLACE INTO {self.table} VALUES(:id, :date_ms, '
|
|
70
|
-
':site, :by, :note) RETURNING *;', request)
|
|
128
|
+
':site, :by, :note, :abnormal) RETURNING *;', request)
|
|
71
129
|
res = self.cursor.fetchone()
|
|
72
130
|
self.rta.value = {
|
|
73
131
|
'id': res[0],
|
|
74
132
|
'date_ms': res[1],
|
|
75
133
|
'site': res[2],
|
|
76
134
|
'by': res[3],
|
|
77
|
-
'note': res[4]
|
|
135
|
+
'note': res[4],
|
|
136
|
+
'abnormal': res[5]
|
|
78
137
|
}
|
|
79
138
|
except sqlite3.IntegrityError as error:
|
|
80
139
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
@@ -101,7 +160,8 @@ class OpNotes:
|
|
|
101
160
|
'date_ms': res[1],
|
|
102
161
|
'site': res[2],
|
|
103
162
|
'by': res[3],
|
|
104
|
-
'note': res[4]
|
|
163
|
+
'note': res[4],
|
|
164
|
+
'abnormal': res[5]
|
|
105
165
|
}
|
|
106
166
|
except sqlite3.IntegrityError as error:
|
|
107
167
|
logging.warning(f'OpNotes rta_cb {error}')
|
|
Binary file
|
pymscada/protocol_constants.py
CHANGED
|
@@ -1,44 +1,62 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Protocol description and protocol constants.
|
|
3
3
|
|
|
4
|
-
Bus holds a tag forever,
|
|
5
|
-
< 1000 forever, otherwise wipes periodically. So assign tagnames once, at the
|
|
6
|
-
start of your program; use RTA as an update messenger, share whole structures
|
|
7
|
-
rarely.
|
|
4
|
+
Bus holds a tag forever, assigning a new ID for each new tagname.
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
- data size of 8-bit char
|
|
6
|
+
Bus protocol message format:
|
|
7
|
+
- version: 8-bit unsigned int == 0x01
|
|
8
|
+
- command: 8-bit unsigned int
|
|
9
|
+
- tag_id: 16-bit unsigned int (0 is valid for ID requests)
|
|
10
|
+
- size: 16-bit unsigned int
|
|
11
|
+
- time_us: 64-bit unsigned int, UTC microseconds
|
|
12
|
+
- data: variable length bytes of size
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
- CMD.ID
|
|
20
|
-
-
|
|
21
|
-
- CMD.
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- CMD.
|
|
28
|
-
|
|
29
|
-
- CMD.
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
- CMD.
|
|
14
|
+
Commands:
|
|
15
|
+
- CMD.ID: Query/inform tag ID
|
|
16
|
+
- Request: data is tagname as bytes
|
|
17
|
+
- Reply: CMD.ID with assigned tag_id and tagname as data
|
|
18
|
+
- Error: CMD.ERR if tagname undefined
|
|
19
|
+
|
|
20
|
+
- CMD.SET: Set a tag value
|
|
21
|
+
- Data format: TYPE byte + typed value
|
|
22
|
+
- No reply
|
|
23
|
+
- Error: CMD.ERR if tag_id invalid
|
|
24
|
+
|
|
25
|
+
- CMD.GET: Get a tag value
|
|
26
|
+
- Request: empty data
|
|
27
|
+
- Reply: CMD.SET with current value
|
|
28
|
+
- Error: CMD.ERR if tag_id invalid
|
|
29
|
+
|
|
30
|
+
- CMD.RTA: Request to author
|
|
31
|
+
- Request: JSON encoded request
|
|
32
|
+
- Reply: Comes from target client, not server
|
|
33
|
+
- Error: CMD.ERR if tag_id invalid or target gone
|
|
34
|
+
|
|
35
|
+
- CMD.SUB: Subscribe to tag updates
|
|
36
|
+
- Request: empty data
|
|
37
|
+
- Reply: CMD.SET with current value
|
|
38
|
+
- Error: CMD.ERR if tag_id invalid
|
|
39
|
+
|
|
40
|
+
- CMD.UNSUB: Unsubscribe from tag
|
|
41
|
+
- No reply
|
|
42
|
+
- Error: CMD.ERR if tag_id invalid
|
|
43
|
+
|
|
44
|
+
- CMD.LIST: List tags
|
|
45
|
+
- Empty data: tags newer than time_us
|
|
46
|
+
- ^text: tags starting with text
|
|
47
|
+
- text$: tags ending with text
|
|
48
|
+
- text: tags containing text
|
|
49
|
+
- Reply: CMD.LIST with space-separated tagnames
|
|
50
|
+
|
|
51
|
+
- CMD.LOG: Log message
|
|
52
|
+
- Data: Message to log (max 300 bytes)
|
|
53
|
+
- Updates __bus__ tag with client address and message
|
|
54
|
+
|
|
55
|
+
Large messages are split into MAX_LEN chunks. Final chunk size < MAX_LEN.
|
|
38
56
|
"""
|
|
39
57
|
|
|
40
58
|
# Tuning constants
|
|
41
|
-
MAX_LEN = 65535 - 14 #
|
|
59
|
+
MAX_LEN = 65535 - 14 # Maximum data size per message
|
|
42
60
|
|
|
43
61
|
from enum import IntEnum
|
|
44
62
|
|
pymscada/tag.py
CHANGED
|
@@ -7,6 +7,7 @@ bus_id is python id(), 0 is null pointer in c, 0 is local bus.
|
|
|
7
7
|
import time
|
|
8
8
|
import array
|
|
9
9
|
import logging
|
|
10
|
+
from typing import TypedDict, Union, Optional, Type, List
|
|
10
11
|
|
|
11
12
|
TYPES = {
|
|
12
13
|
'int': int,
|
|
@@ -310,3 +311,20 @@ class Tag(metaclass=UniqueTag):
|
|
|
310
311
|
self.values = array.array('d')
|
|
311
312
|
else:
|
|
312
313
|
raise TypeError(f"shard invalid {self.name} not int, float")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TagInfo(TypedDict, total=False):
|
|
317
|
+
"""Type definition for tag information dictionary."""
|
|
318
|
+
name: str
|
|
319
|
+
id: Optional[int]
|
|
320
|
+
desc: str
|
|
321
|
+
type: Union[str, Type[int], Type[float], Type[str], Type[list],
|
|
322
|
+
Type[dict], Type[bytes]]
|
|
323
|
+
multi: Optional[List[str]]
|
|
324
|
+
min: Optional[Union[float, int]]
|
|
325
|
+
max: Optional[Union[float, int]]
|
|
326
|
+
deadband: Optional[Union[float, int]]
|
|
327
|
+
units: Optional[str]
|
|
328
|
+
dp: Optional[int]
|
|
329
|
+
format: Optional[str]
|
|
330
|
+
init: Optional[Union[int, float, str]]
|
pymscada/www_server.py
CHANGED
|
@@ -4,6 +4,7 @@ from aiohttp import web, WSMsgType
|
|
|
4
4
|
import logging
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from struct import pack, unpack_from
|
|
7
|
+
import socket
|
|
7
8
|
import time
|
|
8
9
|
from pymscada.bus_client import BusClient
|
|
9
10
|
import pymscada.protocol_constants as pc
|
|
@@ -34,11 +35,13 @@ class WSHandler():
|
|
|
34
35
|
ids = set(range(1, 1000))
|
|
35
36
|
|
|
36
37
|
def __init__(self, ws: web.WebSocketResponse, pages: dict,
|
|
37
|
-
tag_info: dict[str, Tag], do_rta, interface: Interface
|
|
38
|
+
tag_info: dict[str, Tag], do_rta, interface: Interface,
|
|
39
|
+
webclient: dict):
|
|
38
40
|
"""Create callbacks to monitor tag values."""
|
|
39
41
|
self.ws = ws
|
|
40
42
|
self.pages = pages
|
|
41
43
|
self.tag_info = tag_info
|
|
44
|
+
self.webclient = webclient
|
|
42
45
|
self.tag_by_id: dict[int, Tag] = {}
|
|
43
46
|
self.tag_by_name: dict[str, Tag] = {}
|
|
44
47
|
self.queue = asyncio.Queue()
|
|
@@ -168,6 +171,8 @@ class WSHandler():
|
|
|
168
171
|
async def connection_active(self):
|
|
169
172
|
"""Run while the connection is active and don't return."""
|
|
170
173
|
send_queue = asyncio.create_task(self.send_queue())
|
|
174
|
+
self.queue.put_nowait(
|
|
175
|
+
(False, {'type': 'webclient', 'payload': self.webclient}))
|
|
171
176
|
self.queue.put_nowait(
|
|
172
177
|
(False, {'type': 'pages', 'payload': self.pages}))
|
|
173
178
|
async for msg in self.ws:
|
|
@@ -210,11 +215,19 @@ class WSHandler():
|
|
|
210
215
|
class WwwServer:
|
|
211
216
|
"""Connect to bus on bus_ip:bus_port, serve on ip:port for webclient."""
|
|
212
217
|
|
|
213
|
-
def __init__(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
def __init__(
|
|
219
|
+
self,
|
|
220
|
+
bus_ip: str = '127.0.0.1',
|
|
221
|
+
bus_port: int = 1324,
|
|
222
|
+
ip: str = '127.0.0.1',
|
|
223
|
+
port: int = 8324,
|
|
224
|
+
get_path: str | None = None,
|
|
225
|
+
tag_info: dict = {},
|
|
226
|
+
pages: dict = {},
|
|
227
|
+
webclient: dict = {},
|
|
228
|
+
serve_path: str | None = None,
|
|
229
|
+
www_tag: str = '__wwwserver__'
|
|
230
|
+
) -> None:
|
|
218
231
|
"""
|
|
219
232
|
Connect to bus on bus_ip:bus_port, serve on ip:port for webclient.
|
|
220
233
|
|
|
@@ -224,16 +237,36 @@ class WwwServer:
|
|
|
224
237
|
|
|
225
238
|
Event loop must be running.
|
|
226
239
|
"""
|
|
240
|
+
try:
|
|
241
|
+
socket.gethostbyname(bus_ip)
|
|
242
|
+
except socket.gaierror as e:
|
|
243
|
+
raise ValueError(f'Cannot resolve bus IP/hostname: {e}')
|
|
244
|
+
if not isinstance(bus_port, int):
|
|
245
|
+
raise TypeError('bus_port must be an integer')
|
|
246
|
+
if not 1024 <= bus_port <= 65535:
|
|
247
|
+
raise ValueError('bus_port must be between 1024 and 65535')
|
|
248
|
+
try:
|
|
249
|
+
socket.gethostbyname(ip)
|
|
250
|
+
except socket.gaierror as e:
|
|
251
|
+
raise ValueError(f'Cannot resolve IP/hostname: {e}')
|
|
252
|
+
if not isinstance(port, int):
|
|
253
|
+
raise TypeError('port must be an integer')
|
|
254
|
+
if not 1024 <= port <= 65535:
|
|
255
|
+
raise ValueError('port must be between 1024 and 65535')
|
|
256
|
+
if not isinstance(www_tag, str) or not www_tag:
|
|
257
|
+
raise ValueError('www_tag must be a non-empty string')
|
|
258
|
+
|
|
227
259
|
self.busclient = BusClient(bus_ip, bus_port, tag_info,
|
|
228
260
|
module='WWW Server')
|
|
229
261
|
self.ip = ip
|
|
230
262
|
self.port = port
|
|
231
263
|
self.get_path = get_path
|
|
232
|
-
self.serve_path = Path(serve_path)
|
|
264
|
+
self.serve_path = Path(serve_path) if serve_path else None
|
|
233
265
|
for tagname, tag in tag_info.items():
|
|
234
266
|
tag_for_web(tagname, tag)
|
|
235
267
|
self.tag_info = tag_info
|
|
236
268
|
self.pages = pages
|
|
269
|
+
self.webclient = webclient
|
|
237
270
|
self.interface = Interface(www_tag)
|
|
238
271
|
|
|
239
272
|
async def redirect_handler(self, _request: web.Request):
|
|
@@ -256,14 +289,14 @@ class WwwServer:
|
|
|
256
289
|
async def path_handler(self, request: web.Request):
|
|
257
290
|
"""Plain files."""
|
|
258
291
|
logging.info(f"path {request.match_info['path']}")
|
|
292
|
+
if self.serve_path is None:
|
|
293
|
+
return web.HTTPForbidden(reason='path not configured')
|
|
259
294
|
path = self.serve_path.joinpath(request.match_info['path'])
|
|
260
295
|
if path.is_dir():
|
|
261
296
|
return web.HTTPForbidden(reason='folder not permitted')
|
|
262
297
|
if not path.exists():
|
|
263
298
|
return web.HTTPNotFound(reason='no such file in path')
|
|
264
299
|
return web.FileResponse(path)
|
|
265
|
-
# logging.warning(f"path not configured {request.match_info['path']}")
|
|
266
|
-
# return web.HTTPForbidden(reason='path not permitted')
|
|
267
300
|
|
|
268
301
|
async def websocket_handler(self, request: web.Request):
|
|
269
302
|
"""Wait for connections. Create a new one each time."""
|
|
@@ -272,7 +305,7 @@ class WwwServer:
|
|
|
272
305
|
ws = web.WebSocketResponse(max_msg_size=0) # disables max message size
|
|
273
306
|
await ws.prepare(request)
|
|
274
307
|
await WSHandler(ws, self.pages, self.tag_info, self.busclient.rta,
|
|
275
|
-
self.interface).connection_active()
|
|
308
|
+
self.interface, self.webclient).connection_active()
|
|
276
309
|
await ws.close()
|
|
277
310
|
logging.info(f"WS closed {peer}")
|
|
278
311
|
return ws
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pymscada
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0rc2
|
|
4
4
|
Summary: Shared tag value SCADA with python backup and Angular UI
|
|
5
|
-
Author-
|
|
5
|
+
Author-email: Jamie Walton <jamie@walton.net.nz>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/jamie0walton/pymscada
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jamie0walton/pymscada/issues
|
|
7
9
|
Classifier: Programming Language :: Python :: 3
|
|
8
10
|
Classifier: Programming Language :: JavaScript
|
|
9
11
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
12
|
Classifier: Operating System :: OS Independent
|
|
11
13
|
Classifier: Environment :: Console
|
|
12
14
|
Classifier: Development Status :: 1 - Planning
|
|
13
|
-
Project-URL: Homepage, https://github.com/jamie0walton/pymscada
|
|
14
|
-
Project-URL: Bug Tracker, https://github.com/jamie0walton/pymscada/issues
|
|
15
15
|
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
16
18
|
Requires-Dist: PyYAML>=6.0.1
|
|
17
19
|
Requires-Dist: aiohttp>=3.8.5
|
|
18
|
-
Requires-Dist: pymscada-html
|
|
20
|
+
Requires-Dist: pymscada-html==0.2.0rc2
|
|
19
21
|
Requires-Dist: cerberus>=1.3.5
|
|
20
22
|
Requires-Dist: pycomm3>=1.2.14
|
|
21
23
|
Requires-Dist: pysnmplib>=5.0.24
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
24
25
|
# pymscada
|
|
25
26
|
#### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
pymscada-0.1.11b10.dist-info/METADATA,sha256=eBGZjmCSP1HPBnfBlpacMN1Q2VdGFm0_52G2FU-TlLg,2350
|
|
2
|
-
pymscada-0.1.11b10.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
|
3
|
-
pymscada-0.1.11b10.dist-info/entry_points.txt,sha256=j_UgZmqFhNquuFC2M8g5-8X9FCpp2RaDb7NrExzkj1c,72
|
|
4
|
-
pymscada-0.1.11b10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
1
|
pymscada/__init__.py,sha256=NV_cIIwe66Ugp8ns426rtfJIIyskWbqwImD9p_5p0bQ,739
|
|
6
2
|
pymscada/__main__.py,sha256=WcyVlrYOoDdktJhOoyubTOycMwpayksFdxwelRU5xpQ,272
|
|
7
|
-
pymscada/bus_client.py,sha256=
|
|
8
|
-
pymscada/bus_server.py,sha256=
|
|
9
|
-
pymscada/checkout.py,sha256=
|
|
3
|
+
pymscada/bus_client.py,sha256=ROShMcR2-y_i5CIvPxRdCRr-NpnMANjKFdLjKjMTRwo,9117
|
|
4
|
+
pymscada/bus_server.py,sha256=k7ht2SAr24Oab0hBOPeW4NRDF_RK-F46iE0cMzh7K4w,12323
|
|
5
|
+
pymscada/checkout.py,sha256=RLuCMTEuUI7pp1hIRAUPbo8xYFta8TjArelx0SD4gOY,3897
|
|
10
6
|
pymscada/config.py,sha256=vwGxieaJBYXiHNQEOYVDFaPuGmnUlCnbNm_W9bugKlc,1851
|
|
11
|
-
pymscada/console.py,sha256=
|
|
7
|
+
pymscada/console.py,sha256=zM6TNXJY8ROcVzO3UBzi1qDSiit7mfLr2YprRoxv2oQ,8824
|
|
8
|
+
pymscada/files.py,sha256=iouEOPfEkVI0Qbbf1p-L324Y04zSrynVypLW0-1MThA,2499
|
|
9
|
+
pymscada/history.py,sha256=qXbCkHQC_j7HF9b1PvnF93d4HvW9ODJeSc9tvy63o10,11513
|
|
10
|
+
pymscada/main.py,sha256=d6EnK4-tEcvM5AqMHYhvqlnSh-E_wd0Tuxk-kXYSiDw,1854
|
|
11
|
+
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
|
|
14
|
+
pymscada/periodic.py,sha256=MLlL93VLvFqBBgjO1Us1t0aLHTZ5BFdW0B__G02T1nQ,1235
|
|
15
|
+
pymscada/protocol_constants.py,sha256=lPJ4JEgFJ_puJjTym83EJIOw3UTUFbuFMwg3ohyUAGY,2414
|
|
16
|
+
pymscada/samplers.py,sha256=t0IscgsCm5YByioOZ6aOKMO_guDFS_wxnJSiOGKI4Nw,2583
|
|
17
|
+
pymscada/tag.py,sha256=GINy8gE1TPLFnuJLVVSui4Ke3IQlOGRy6_ePAlGlvj4,10683
|
|
18
|
+
pymscada/validate.py,sha256=fPMlP6RscYuTIgdEJjJ0ZZI0OyVSat1lpqg254wqpdE,13140
|
|
19
|
+
pymscada/www_server.py,sha256=rz6Yictg_Ony8zW76An5bv_cZN5aLi-QW3B85uWMgXQ,13152
|
|
12
20
|
pymscada/demo/README.md,sha256=iNcVbCTkq-d4agLV-979lNRaqf_hbJCn3OFzY-6qfU8,880
|
|
13
21
|
pymscada/demo/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
|
|
14
22
|
pymscada/demo/accuweather.yaml,sha256=Fk4rV0S8jCau0173QCzKW8TdUbc4crYVi0aD8fPLNgU,322
|
|
@@ -19,17 +27,17 @@ pymscada/demo/logixclient.yaml,sha256=G_NlJhBYwT1a9ceHDgO6fCNKFmBM2pVO_t9Xa1NqlR
|
|
|
19
27
|
pymscada/demo/modbus_plc.py,sha256=3zZHHbyrdxyryEHBeNIw-fpcDGcS1MaJiqEwQDr6zWI,2397
|
|
20
28
|
pymscada/demo/modbusclient.yaml,sha256=geeCsUJZkkEj7jjXR_Yk6R5zA5Ta9IczrHsARz7ZgXY,1099
|
|
21
29
|
pymscada/demo/modbusserver.yaml,sha256=67_mED6jXgtnzlDIky9Cg4j-nXur06iz9ve3JUwSyG8,1133
|
|
22
|
-
pymscada/demo/openweather.yaml,sha256=
|
|
30
|
+
pymscada/demo/openweather.yaml,sha256=n8aPc_Ar6uiM-XbrEbBydABxFYm2uv_49dGo8u7DI8Q,433
|
|
23
31
|
pymscada/demo/opnotes.yaml,sha256=gdT8DKaAV4F6u9trLCPyBgf449wYaP_FF8GCbjXm9-k,105
|
|
24
32
|
pymscada/demo/ping.yaml,sha256=fm3eUdR2BwnPI_lU_V07qgmDxjSoPP6lPazYB6ZgpVg,149
|
|
25
33
|
pymscada/demo/pymscada-bus.service,sha256=F3ViriRXyMKdCY3tHa3wXAnv2Fo2_16-EScTLsYnSOA,261
|
|
26
34
|
pymscada/demo/pymscada-demo-modbus_plc.service,sha256=EtbWDwqAs4nLNLKScUiHcUWU1b6_tRBeAAVGi9q95hY,320
|
|
27
35
|
pymscada/demo/pymscada-files.service,sha256=iLOfbl4SCxAwYHT20XCGHU0BJsUVicNHjHzKS8xIdgA,326
|
|
28
36
|
pymscada/demo/pymscada-history.service,sha256=61c5RqOmJ13Dl1yyRfsChKOdXp2W-xfYyCOYoJHLkh8,386
|
|
29
|
-
pymscada/demo/pymscada-io-accuweather.service,sha256=M_dnPYwmvTpYGF5AbMStbYL6VCWgOFR50nRRpfqvpBo,358
|
|
30
37
|
pymscada/demo/pymscada-io-logixclient.service,sha256=mn4UzkiOfYqHvgfTFSkUeoPFQQXZboet5I0m7-L5SAY,348
|
|
31
38
|
pymscada/demo/pymscada-io-modbusclient.service,sha256=eTgNdK10dJCs2lLPhmHBh-3j6Ltx2oyU_MNl2f3xnhg,348
|
|
32
39
|
pymscada/demo/pymscada-io-modbusserver.service,sha256=g7Rzm6zGLq_qvTJRL_pcLl4Ps7CNIa2toeGhPNp_oEc,348
|
|
40
|
+
pymscada/demo/pymscada-io-openweather.service,sha256=SQnZ-cq1V3qvZY7EgR_Vx36vCOw1ipfGoLoutHsxtNk,359
|
|
33
41
|
pymscada/demo/pymscada-io-ping.service,sha256=Fm8qR4IVq0NupEvHLGONXGwjjQsx5VqaBYPewhg7-k4,329
|
|
34
42
|
pymscada/demo/pymscada-io-snmpclient.service,sha256=Rsm8uiwnoGx-1MkXqYgtj4UP9-r7AEEeB9yoR0y0oVA,343
|
|
35
43
|
pymscada/demo/pymscada-opnotes.service,sha256=TlrTRgP3rzrlXT8isAGT_Wy38ScDjT1VvnlgW84XiS8,354
|
|
@@ -37,8 +45,7 @@ pymscada/demo/pymscada-wwwserver.service,sha256=7Qy2wsMmFEsQn-b5mgAcsrAQZgXynkv8
|
|
|
37
45
|
pymscada/demo/snmpclient.yaml,sha256=z8iACrFvMftYUtqGrRjPZYZTpn7aOXI-Kp675NAM8cU,2013
|
|
38
46
|
pymscada/demo/tags.yaml,sha256=9xydsQriKT0lNAW533rz-FMVgoedn6Lwc50AnNig7-k,2733
|
|
39
47
|
pymscada/demo/wwwserver.yaml,sha256=mmwvSLUXUDCIPaHeCJdCETAp9Cc4wb5CuK_aGv01KWk,2759
|
|
40
|
-
pymscada/
|
|
41
|
-
pymscada/history.py,sha256=G079gHfzasmGtI5ANS8MdETD4bdZg5vHE_yTKk7atHw,9504
|
|
48
|
+
pymscada/demo/__pycache__/__init__.cpython-311.pyc,sha256=tpxZoW429YA-2mbwzOlhBmbSTcbvTJqgKCfDRMrhEJE,195
|
|
42
49
|
pymscada/iodrivers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
50
|
pymscada/iodrivers/accuweather.py,sha256=p_OYJtCrtbSQYjgf06Yk3Qc9wGpkx8ogn81XNGd19fE,3842
|
|
44
51
|
pymscada/iodrivers/logix_client.py,sha256=eqmiYLYUBHbr7wTljomGIZVNvXe-5WleGKfzwcHXO8w,2829
|
|
@@ -46,24 +53,20 @@ pymscada/iodrivers/logix_map.py,sha256=ljjBAMJcw199v1V5u0Yfl38U6zbZzba5mdY4I3Zvd
|
|
|
46
53
|
pymscada/iodrivers/modbus_client.py,sha256=DIGrEPz_Bwwj9CEeog5fQqiAu1UMV7xVL6KxlKgXNPs,9592
|
|
47
54
|
pymscada/iodrivers/modbus_map.py,sha256=af2J3CGSeYQ4mSy8rNsERp9z7fRgRUYk3it5Mrc_IQA,7255
|
|
48
55
|
pymscada/iodrivers/modbus_server.py,sha256=VqvjOJ4-LOVaD1jOK22raXhrCshJEaWlMxLvn5xMnFc,6570
|
|
49
|
-
pymscada/iodrivers/openweather.py,sha256=
|
|
56
|
+
pymscada/iodrivers/openweather.py,sha256=IVzmaEjdwm1NDhsOYpEV5vzB8HFaQEpWsnm6fhpsPCQ,8926
|
|
50
57
|
pymscada/iodrivers/ping_client.py,sha256=UOQgUfoIcYqy5VvKyJ8XGHHjeSRTfjmrhyWEojhIZQk,4188
|
|
51
58
|
pymscada/iodrivers/ping_map.py,sha256=EbOteqfEYKIOMqPymROJ4now2If-ekEj6jnM5hthoSA,1403
|
|
52
59
|
pymscada/iodrivers/snmp_client.py,sha256=66-IDzddeKcSnqOzNXIZ8wuuAqhIxZjyLNrDwDvHCvw,2708
|
|
53
60
|
pymscada/iodrivers/snmp_map.py,sha256=sDdIR5ZPAETpozDfBt_XQiZ-f4t99UCPlzj7BxFxQyM,2369
|
|
54
|
-
pymscada/main.py,sha256=XtASmPZoQMuzDCHlH3P9XwgYskeLtNdl-hc1tDI7ljc,1875
|
|
55
|
-
pymscada/misc.py,sha256=0Cj6OFhQonyhyk9x0BG5MiS-6EPk_w6zvavt8o_Hlf0,622
|
|
56
|
-
pymscada/module_config.py,sha256=Sq9DDND0WotxwdY7mRHG8G-WNPYqWp9fxvx38--kwr0,8462
|
|
57
|
-
pymscada/opnotes.py,sha256=pxjFgy4uMnAmJcfrk8BX4Gl5j0z4fFb5waXcqI6UJ_M,5133
|
|
58
61
|
pymscada/pdf/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
|
|
59
62
|
pymscada/pdf/one.pdf,sha256=eoJ45DrAjVZrwmwdA_EAz1fwmT44eRnt_tkc2pmMrKY,1488
|
|
60
63
|
pymscada/pdf/two.pdf,sha256=TAuW5yLU1_wfmTH_I5ezHwY0pxhCVuZh3ixu0kwmJwE,1516
|
|
61
|
-
pymscada/
|
|
62
|
-
pymscada/protocol_constants.py,sha256=ooGmBM7WGWWdN10FObdjzcYieK8WN7Zy6qVxtD93LMk,1963
|
|
63
|
-
pymscada/samplers.py,sha256=t0IscgsCm5YByioOZ6aOKMO_guDFS_wxnJSiOGKI4Nw,2583
|
|
64
|
-
pymscada/tag.py,sha256=Oxh70q2MrPAEI94v4QsWt4gD8QP6BlfzNv9xXeeUFys,10103
|
|
64
|
+
pymscada/pdf/__pycache__/__init__.cpython-311.pyc,sha256=4KTfXrV9bGDbTIEv-zgIj_LvzLbVTj77lEC1wzMh9e0,194
|
|
65
65
|
pymscada/tools/snmp_client2.py,sha256=pdn5dYyEv4q-ubA0zQ8X-3tQDYxGC7f7Xexa7QPaL40,1675
|
|
66
66
|
pymscada/tools/walk.py,sha256=OgpprUbKLhEWMvJGfU1ckUt_PFEpwZVOD8HucCgzmOc,1625
|
|
67
|
-
pymscada/
|
|
68
|
-
pymscada/
|
|
69
|
-
pymscada-0.
|
|
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,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymscada
|
|
File without changes
|