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/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.module_name, 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: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, module_name: str, options: argparse.Namespace):
212
+ def create_module(self, options: argparse.Namespace):
212
213
  """Create a module instance based on configuration and options."""
213
- module_def = self.modules[module_name]
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 module_name == 'console':
230
- return Console(options.ip, options.port,
231
- kwargs.get('tag_info',{}))
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__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
12
- db: str = None, rta_tag: str = '__opnotes__',
13
- table: str = 'opnotes') -> None:
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
- TODO
18
- Write something.
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.busclient = BusClient(bus_ip, bus_port, module='OpNotes')
38
- self.rta = Tag(rta_tag, dict)
39
- self.rta.value = {}
40
- self.busclient.add_callback_rta(rta_tag, self.rta_cb)
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} (date_ms, site, by, note) '
52
- 'VALUES(:date_ms, :site, :by, :note) RETURNING *;',
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}')
@@ -1,44 +1,62 @@
1
1
  """
2
2
  Protocol description and protocol constants.
3
3
 
4
- Bus holds a tag forever, assigns a tag id forever, holds a tag value with len
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
- - version 16-bit unsigned int == 0x01
10
- - command 16-bit unsigned int
11
- - tag_id 32-bit unsigned int 0 is not a valid tag_id
12
- - size 32-bit unsigned int
13
- - if size == 0xff continuation mandatory
14
- - size 0x00 completes an empty continuation
15
- - time_us 128-bit unsigned int, UTC microseconds
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
- command
19
- - CMD.ID data is tagname
20
- - reply: CMD_ID with tag_id and data as tagname
21
- - CMD.SET id, data is typed or json packed
22
- - no reply
23
- - CMD.UNSUB id
24
- - no reply
25
- - CMD.GET id
26
- - CMD.RTA id, data is request to author
27
- - CMD.SUB id
28
- - reply: SET id and value, value may be None
29
- - CMD.LIST
30
- - size == 0x00
31
- - tags with values newer than time_us
32
- - size > 0x00
33
- - ^text matches start of tagname
34
- - text$ matches start of tagname
35
- - text matches anywhere in tagname
36
- - reply: LIST data as space separated tagnames
37
- - CMD.LOG data to logging.warning
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 # TODO fix server(?) when 3
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__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
214
- ip: str = '127.0.0.1', port: int = 8324, get_path: str = None,
215
- tag_info: dict = {}, pages: dict = {}, serve_path: str = None,
216
- www_tag: str = '__wwwserver__'
217
- ) -> None:
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.1.11b10
3
+ Version: 0.2.0rc2
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
- Author-Email: Jamie Walton <jamie@walton.net.nz>
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>=0.1.10b5
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=ZUCynJtEvmHruzLS6ZAzp-_0dv1A1u_POgemb38kHuc,8989
8
- pymscada/bus_server.py,sha256=eD4Fz4Sv4EIu2vney_d0jAryiCk5eoH5NQA-dAZRTqA,12029
9
- pymscada/checkout.py,sha256=1Al29nVqCebupYvMgcY5boz772PtsVp1MVbYaQajV5Q,3373
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=b4gm7cuhYKGFNtHoxygWkrqiN42mU8DM4KUi-Q74M4U,8793
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=eFaRc7JxkEwRswCtmJiypsfR5xgQR-R-UtwvfwDHw-w,430
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/files.py,sha256=MisnKoWvkffPMSj6sGVmm-4fh866x4UX3t9LJg4oCfk,2400
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=w_OSL5UG30IBTuaR6Exmao7XJae_hKo1JM04XT8Ke7M,5580
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/periodic.py,sha256=MLlL93VLvFqBBgjO1Us1t0aLHTZ5BFdW0B__G02T1nQ,1235
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/validate.py,sha256=fPMlP6RscYuTIgdEJjJ0ZZI0OyVSat1lpqg254wqpdE,13140
68
- pymscada/www_server.py,sha256=rV1Vsk3J1wBhFMBCnK33SziNuTGgVwNc5zLjQQFxJ-s,12021
69
- pymscada-0.1.11b10.dist-info/RECORD,,
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,,
@@ -1,4 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.3)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
+
@@ -1,5 +1,2 @@
1
1
  [console_scripts]
2
2
  pymscada = pymscada.__main__:cmd_line
3
-
4
- [gui_scripts]
5
-
@@ -0,0 +1 @@
1
+ pymscada