pymscada 0.2.0rc1__tar.gz → 0.2.0rc2__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.

Files changed (88) hide show
  1. {pymscada-0.2.0rc1/src/pymscada.egg-info → pymscada-0.2.0rc2}/PKG-INFO +2 -2
  2. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/pyproject.toml +2 -2
  3. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/bus_client.py +4 -3
  4. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/bus_server.py +14 -2
  5. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/console.py +2 -2
  6. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/files.py +3 -3
  7. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/history.py +26 -3
  8. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/openweather.py +21 -2
  9. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/opnotes.py +22 -3
  10. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/www_server.py +43 -10
  11. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2/src/pymscada.egg-info}/PKG-INFO +2 -2
  12. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada.egg-info/requires.txt +1 -1
  13. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_openweather.py +1 -2
  14. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/LICENSE +0 -0
  15. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/MANIFEST.in +0 -0
  16. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/README.md +0 -0
  17. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/setup.cfg +0 -0
  18. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/__init__.py +0 -0
  19. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/__main__.py +0 -0
  20. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/checkout.py +0 -0
  21. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/config.py +0 -0
  22. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/README.md +0 -0
  23. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/__init__.py +0 -0
  24. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
  25. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/accuweather.yaml +0 -0
  26. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/bus.yaml +0 -0
  27. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/files.yaml +0 -0
  28. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/history.yaml +0 -0
  29. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/logixclient.yaml +0 -0
  30. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/modbus_plc.py +0 -0
  31. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/modbusclient.yaml +0 -0
  32. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/modbusserver.yaml +0 -0
  33. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/openweather.yaml +0 -0
  34. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/opnotes.yaml +0 -0
  35. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/ping.yaml +0 -0
  36. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-bus.service +0 -0
  37. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  38. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-files.service +0 -0
  39. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-history.service +0 -0
  40. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
  41. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  42. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  43. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-io-openweather.service +0 -0
  44. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-io-ping.service +0 -0
  45. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
  46. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-opnotes.service +0 -0
  47. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  48. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/snmpclient.yaml +0 -0
  49. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/tags.yaml +0 -0
  50. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/demo/wwwserver.yaml +0 -0
  51. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/__init__.py +0 -0
  52. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/accuweather.py +0 -0
  53. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/logix_client.py +0 -0
  54. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/logix_map.py +0 -0
  55. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/modbus_client.py +0 -0
  56. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/modbus_map.py +0 -0
  57. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/modbus_server.py +0 -0
  58. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/ping_client.py +0 -0
  59. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/ping_map.py +0 -0
  60. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/snmp_client.py +0 -0
  61. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/iodrivers/snmp_map.py +0 -0
  62. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/main.py +0 -0
  63. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/misc.py +0 -0
  64. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/module_config.py +0 -0
  65. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/pdf/__init__.py +0 -0
  66. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
  67. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/pdf/one.pdf +0 -0
  68. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/pdf/two.pdf +0 -0
  69. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/periodic.py +0 -0
  70. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/protocol_constants.py +0 -0
  71. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/samplers.py +0 -0
  72. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/tag.py +0 -0
  73. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/tools/snmp_client2.py +0 -0
  74. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/tools/walk.py +0 -0
  75. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada/validate.py +0 -0
  76. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada.egg-info/SOURCES.txt +0 -0
  77. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada.egg-info/dependency_links.txt +0 -0
  78. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada.egg-info/entry_points.txt +0 -0
  79. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/src/pymscada.egg-info/top_level.txt +0 -0
  80. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_bus_server.py +0 -0
  81. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_config.py +0 -0
  82. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_history.py +0 -0
  83. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_misc.py +0 -0
  84. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_opnotes.py +0 -0
  85. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_periodic.py +0 -0
  86. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_samplers.py +0 -0
  87. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_tag.py +0 -0
  88. {pymscada-0.2.0rc1 → pymscada-0.2.0rc2}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymscada
3
- Version: 0.2.0rc1
3
+ Version: 0.2.0rc2
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: PyYAML>=6.0.1
19
19
  Requires-Dist: aiohttp>=3.8.5
20
- Requires-Dist: pymscada-html==0.2.0rc1
20
+ Requires-Dist: pymscada-html==0.2.0rc2
21
21
  Requires-Dist: cerberus>=1.3.5
22
22
  Requires-Dist: pycomm3>=1.2.14
23
23
  Requires-Dist: pysnmplib>=5.0.24
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pymscada"
3
- version = "0.2.0rc1"
3
+ version = "0.2.0rc2"
4
4
  description = "Shared tag value SCADA with python backup and Angular UI"
5
5
  authors = [
6
6
  {name = "Jamie Walton", email = "jamie@walton.net.nz"},
@@ -8,7 +8,7 @@ authors = [
8
8
  dependencies = [
9
9
  "PyYAML>=6.0.1", # all
10
10
  "aiohttp>=3.8.5", # www_server
11
- "pymscada-html==0.2.0rc1", # www_server
11
+ "pymscada-html==0.2.0rc2", # www_server
12
12
  "cerberus>=1.3.5", # validate
13
13
  "pycomm3>=1.2.14", # logix_client
14
14
  "pysnmplib>=5.0.24", # DON'T use pysnmp, dead
@@ -12,8 +12,9 @@ class BusClient:
12
12
  """
13
13
  Connects to a Bus Server.
14
14
 
15
- await client.connect() to make the connection. If bus server connection
16
- fails, die.
15
+ The client is created without a connection. client.start() creates the
16
+ connection and checks the tags at that time. When the connection fails
17
+ the client dies. A connection is mandatory for the client to run.
17
18
  """
18
19
 
19
20
  def __init__(self, ip: str = '127.0.0.1', port: int = 1324, tag_info=None,
@@ -125,7 +126,6 @@ class BusClient:
125
126
 
126
127
  async def read(self):
127
128
  """Read forever."""
128
- await self.open_connection()
129
129
  while True:
130
130
  try:
131
131
  head = await self.reader.readexactly(14)
@@ -223,4 +223,5 @@ class BusClient:
223
223
 
224
224
  async def start(self):
225
225
  """Start async."""
226
+ await self.open_connection()
226
227
  self.read_task = asyncio.create_task(self.read())
@@ -3,6 +3,7 @@ import asyncio
3
3
  from struct import pack, unpack
4
4
  import time
5
5
  import logging
6
+ import socket
6
7
  import pymscada.protocol_constants as pc
7
8
 
8
9
 
@@ -143,8 +144,12 @@ class BusServer:
143
144
 
144
145
  __slots__ = ('ip', 'port', 'server', 'connections', 'bus_tag')
145
146
 
146
- def __init__(self, ip: str = '127.0.0.1', port: int = 1324,
147
- bus_tag: str = '__bus__'):
147
+ def __init__(
148
+ self,
149
+ ip: str = '127.0.0.1',
150
+ port: int = 1324,
151
+ bus_tag: str = '__bus__'
152
+ ) -> None:
148
153
  """
149
154
  Serve Tags on ip:port, echoing changes to any subscribers.
150
155
 
@@ -154,6 +159,13 @@ class BusServer:
154
159
 
155
160
  Event loop must be running.
156
161
  """
162
+ try:
163
+ socket.gethostbyname(ip)
164
+ except socket.gaierror as e:
165
+ raise ValueError(f'Cannot resolve IP/hostname: {e}')
166
+ if not isinstance(bus_tag, str):
167
+ raise ValueError('bus_tag must be a string')
168
+
157
169
  self.ip = ip
158
170
  self.port = port
159
171
  self.server = None
@@ -3,7 +3,7 @@ import asyncio
3
3
  import logging
4
4
  import sys
5
5
  from pymscada.bus_client import BusClient
6
- from pymscada.tag import Tag, tag_for_web
6
+ from pymscada.tag import Tag, tag_for_web, TagInfo
7
7
  try:
8
8
  import termios
9
9
  import tty
@@ -153,7 +153,7 @@ class Console:
153
153
  """Provide a text console to interact with a Bus."""
154
154
 
155
155
  def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
156
- tag_info: dict = {}):
156
+ tag_info: dict[str, TagInfo] = {}) -> None:
157
157
  """
158
158
  Connect to bus_ip:bus_port and provide console interaction with a Bus.
159
159
 
@@ -10,13 +10,13 @@ class Files():
10
10
  """Connect to bus_ip:bus_port, store and provide a value history."""
11
11
 
12
12
  def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
13
- path: str = '', files: list = None,
13
+ path: str = '', files: list[dict] = [],
14
14
  rta_tag: str = '__files__') -> None:
15
15
  """
16
16
  Connect to bus_ip:bus_port, serve and update files.
17
17
 
18
- TODO
19
- Write something.
18
+ Scans paths in files at the root defined by path. These should
19
+ be readable by wwwserver so that download links work.
20
20
 
21
21
  Event loop must be running.
22
22
  """
@@ -25,6 +25,7 @@ import logging
25
25
  from pathlib import Path
26
26
  from struct import pack, pack_into, unpack_from, error
27
27
  import time
28
+ import socket
28
29
  from typing import TypedDict, Optional
29
30
  from pymscada.bus_client import BusClient
30
31
  from pymscada.tag import Tag, TagInfo, TYPES
@@ -220,9 +221,14 @@ class TagHistory():
220
221
  class History():
221
222
  """Connect to bus_ip:bus_port, store and provide a value history."""
222
223
 
223
- def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
224
- path: str = 'history', tag_info: TagInfo = {},
225
- rta_tag: str = '__history__') -> None:
224
+ def __init__(
225
+ self,
226
+ bus_ip: str = '127.0.0.1',
227
+ bus_port: int = 1324,
228
+ path: str = 'history',
229
+ tag_info: TagInfo = {},
230
+ rta_tag: str | None = '__history__'
231
+ ) -> None:
226
232
  """
227
233
  Connect to bus_ip:bus_port, store and provide a value history.
228
234
 
@@ -232,6 +238,23 @@ class History():
232
238
 
233
239
  Event loop must be running.
234
240
  """
241
+ if not isinstance(bus_ip, str):
242
+ raise ValueError("bus_ip must be a string")
243
+ try:
244
+ socket.gethostbyname(bus_ip)
245
+ except socket.gaierror:
246
+ raise ValueError(f"Invalid bus_ip: {bus_ip}")
247
+ if not isinstance(bus_port, int):
248
+ raise ValueError("bus_port must be an integer")
249
+ if not 1024 <= bus_port <= 65535:
250
+ raise ValueError(f"bus_port must be between 1024 and 65535")
251
+ if not isinstance(path, str):
252
+ raise ValueError("path must be a string")
253
+ if not isinstance(tag_info, dict):
254
+ raise ValueError("tag_info must be a dictionary")
255
+ if not isinstance(rta_tag, (str, type(None))):
256
+ raise ValueError("rta_tag must be a string or None")
257
+
235
258
  self.busclient = BusClient(bus_ip, bus_port, module='History')
236
259
  self.path = path
237
260
  self.tags: dict[str, Tag] = {}
@@ -2,6 +2,7 @@
2
2
  import asyncio
3
3
  import aiohttp
4
4
  import logging
5
+ import socket
5
6
  from time import time
6
7
  from pymscada.bus_client import BusClient
7
8
  from pymscada.periodic import Periodic
@@ -10,8 +11,14 @@ from pymscada.tag import Tag
10
11
  class OpenWeatherClient:
11
12
  """Get weather data from OpenWeather Current and Forecast APIs."""
12
13
 
13
- def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
14
- proxy: str = None, api: dict = {}, tags: dict = {}) -> None:
14
+ def __init__(
15
+ self,
16
+ bus_ip: str | None = '127.0.0.1',
17
+ bus_port: int = 1324,
18
+ proxy: str | None = None,
19
+ api: dict = {},
20
+ tags: list = []
21
+ ) -> None:
15
22
  """
16
23
  Connect to bus on bus_ip:bus_port.
17
24
 
@@ -21,6 +28,18 @@ class OpenWeatherClient:
21
28
  - times: list of hours ahead to fetch forecast data for
22
29
  - units: optional units (standard, metric, imperial)
23
30
  """
31
+ if bus_ip is not None:
32
+ try:
33
+ socket.gethostbyname(bus_ip)
34
+ except socket.gaierror:
35
+ raise ValueError(f"Invalid bus_ip: {bus_ip}")
36
+ if not isinstance(proxy, str) and proxy is not None:
37
+ raise ValueError("proxy must be a string or None")
38
+ if not isinstance(api, dict):
39
+ raise ValueError("api must be a dictionary")
40
+ if not isinstance(tags, list):
41
+ raise ValueError("tags must be a list")
42
+
24
43
  self.busclient = None
25
44
  if bus_ip is not None:
26
45
  self.busclient = BusClient(bus_ip, bus_port, module='OpenWeather')
@@ -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,9 +9,14 @@ 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
 
@@ -21,6 +27,19 @@ class OpNotes:
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
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymscada
3
- Version: 0.2.0rc1
3
+ Version: 0.2.0rc2
4
4
  Summary: Shared tag value SCADA with python backup and Angular UI
5
5
  Author-email: Jamie Walton <jamie@walton.net.nz>
6
6
  License: GPL-3.0-or-later
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: PyYAML>=6.0.1
19
19
  Requires-Dist: aiohttp>=3.8.5
20
- Requires-Dist: pymscada-html==0.2.0rc1
20
+ Requires-Dist: pymscada-html==0.2.0rc2
21
21
  Requires-Dist: cerberus>=1.3.5
22
22
  Requires-Dist: pycomm3>=1.2.14
23
23
  Requires-Dist: pysnmplib>=5.0.24
@@ -1,6 +1,6 @@
1
1
  PyYAML>=6.0.1
2
2
  aiohttp>=3.8.5
3
- pymscada-html==0.2.0rc1
3
+ pymscada-html==0.2.0rc2
4
4
  cerberus>=1.3.5
5
5
  pycomm3>=1.2.14
6
6
  pysnmplib>=5.0.24
@@ -19,8 +19,7 @@ async def test_openweather_temperature():
19
19
  }
20
20
  tags = ['London_Temp', 'London_Temp_01', 'London_Temp_02',
21
21
  'London_Temp_03']
22
- client = OpenWeatherClient(bus_ip=None, bus_port=None, api=api_config,
23
- tags=tags)
22
+ client = OpenWeatherClient(bus_ip=None, api=api_config, tags=tags)
24
23
  try:
25
24
  handle_task = asyncio.create_task(client.handle_response())
26
25
  await client.fetch_current_data()
File without changes
File without changes
File without changes
File without changes