pymscada 0.1.7__tar.gz → 0.1.9__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.
Files changed (85) hide show
  1. {pymscada-0.1.7 → pymscada-0.1.9}/PKG-INFO +1 -1
  2. {pymscada-0.1.7 → pymscada-0.1.9}/pyproject.toml +1 -1
  3. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/__init__.py +2 -0
  4. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/bus_client.py +1 -1
  5. pymscada-0.1.9/src/pymscada/demo/accuweather.yaml +16 -0
  6. pymscada-0.1.9/src/pymscada/demo/pymscada-io-accuweather.service +15 -0
  7. pymscada-0.1.9/src/pymscada/iodrivers/accuweather.py +96 -0
  8. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/main.py +14 -0
  9. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/tag.py +1 -1
  10. {pymscada-0.1.7 → pymscada-0.1.9}/LICENSE +0 -0
  11. {pymscada-0.1.7 → pymscada-0.1.9}/README.md +0 -0
  12. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/__main__.py +0 -0
  13. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/bus_server.py +0 -0
  14. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/checkout.py +0 -0
  15. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/config.py +0 -0
  16. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/console.py +0 -0
  17. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/README.md +0 -0
  18. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/__init__.py +0 -0
  19. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/bus.yaml +0 -0
  20. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/files.yaml +0 -0
  21. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/history.yaml +0 -0
  22. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/logixclient.yaml +0 -0
  23. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/modbus_plc.py +0 -0
  24. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/modbusclient.yaml +0 -0
  25. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/modbusserver.yaml +0 -0
  26. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/opnotes.yaml +0 -0
  27. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/ping.yaml +0 -0
  28. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-bus.service +0 -0
  29. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  30. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-files.service +0 -0
  31. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-history.service +0 -0
  32. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-io-logixclient.service +0 -0
  33. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  34. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  35. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-io-ping.service +0 -0
  36. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-io-snmpclient.service +0 -0
  37. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-opnotes.service +0 -0
  38. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  39. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/snmpclient.yaml +0 -0
  40. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/tags.yaml +0 -0
  41. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/demo/wwwserver.yaml +6 -6
  42. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/files.py +0 -0
  43. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/history.py +0 -0
  44. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/__init__.py +0 -0
  45. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/logix_client.py +0 -0
  46. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/logix_map.py +0 -0
  47. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/modbus_client.py +0 -0
  48. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/modbus_map.py +0 -0
  49. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/modbus_server.py +0 -0
  50. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/ping_client.py +0 -0
  51. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/ping_map.py +0 -0
  52. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/snmp_client.py +0 -0
  53. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/iodrivers/snmp_map.py +0 -0
  54. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/misc.py +0 -0
  55. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/opnotes.py +0 -0
  56. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/pdf/__init__.py +0 -0
  57. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/pdf/one.pdf +0 -0
  58. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/pdf/two.pdf +0 -0
  59. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/periodic.py +0 -0
  60. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/protocol_constants.py +0 -0
  61. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/samplers.py +0 -0
  62. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/tools/snmp_client2.py +0 -0
  63. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/tools/walk.py +0 -0
  64. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/validate.py +0 -0
  65. {pymscada-0.1.7 → pymscada-0.1.9}/src/pymscada/www_server.py +0 -0
  66. {pymscada-0.1.7 → pymscada-0.1.9}/tests/__init__.py +0 -0
  67. {pymscada-0.1.7 → pymscada-0.1.9}/tests/bus_echo.py +0 -0
  68. {pymscada-0.1.7 → pymscada-0.1.9}/tests/iodrivers/test_logix.py +0 -0
  69. {pymscada-0.1.7 → pymscada-0.1.9}/tests/iodrivers/test_modbus.py +0 -0
  70. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/busserver.yaml +0 -0
  71. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/db.sqlite +0 -0
  72. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/hist_tag_0_0.dat +0 -0
  73. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/hist_tag_0_10_2.dat +0 -0
  74. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/hist_tag_0_15.dat +0 -0
  75. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/hist_tag_0_26.dat +0 -0
  76. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_assets/hist_tag_0_50.dat +0 -0
  77. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_bus_server.py +0 -0
  78. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_config.py +0 -0
  79. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_history.py +0 -0
  80. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_misc.py +0 -0
  81. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_opnotes.py +0 -0
  82. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_periodic.py +0 -0
  83. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_samplers.py +0 -0
  84. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_tag.py +0 -0
  85. {pymscada-0.1.7 → pymscada-0.1.9}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymscada
3
- Version: 0.1.7
3
+ Version: 0.1.9
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pymscada"
3
- version = "0.1.7"
3
+ version = "0.1.9"
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" },
@@ -2,6 +2,7 @@
2
2
  from pymscada.bus_client import BusClient
3
3
  from pymscada.bus_server import BusServer
4
4
  from pymscada.config import Config
5
+ from pymscada.iodrivers.accuweather import AccuWeatherClient
5
6
  from pymscada.iodrivers.logix_client import LogixClient
6
7
  from pymscada.iodrivers.modbus_client import ModbusClient
7
8
  from pymscada.iodrivers.modbus_server import ModbusServer
@@ -14,6 +15,7 @@ __all__ = [
14
15
  'BusClient',
15
16
  'BusServer',
16
17
  'Config',
18
+ 'AccuWeatherClient',
17
19
  'LogixClient',
18
20
  'ModbusClient',
19
21
  'ModbusServer',
@@ -109,7 +109,7 @@ class BusClient:
109
109
  self.ip, self.port)
110
110
  self.addr = self.writer.get_extra_info('sockname')
111
111
  self.write(pc.CMD_LOG, 0, 0, f'{self.module} connected'.encode())
112
- logging.info(f'connected {self.addr}')
112
+ logging.warning(f'connected {self.addr} {self.port}')
113
113
  for tag in Tag.get_all_tags().values():
114
114
  self.add_tag(tag)
115
115
  Tag.set_notify(self.add_tag)
@@ -0,0 +1,16 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1325
3
+ proxy: 'http://127.0.0.1:3128'
4
+ api:
5
+ url: http://dataservice.accuweather.com/forecasts/v1/hourly/12hour/
6
+ query:
7
+ apikey: <your key>
8
+ details: 'true'
9
+ metric: 'true'
10
+ locations:
11
+ Murupara: 246970
12
+ times: [1, 3, 12]
13
+ tags:
14
+ - MuruparaTemp_1
15
+ - MuruparaTemp_3
16
+ - MuruparaTemp_12
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=pymscada - AccuWeather client
3
+ BindsTo=pymscada-bus.service
4
+ After=pymscada-bus.service
5
+
6
+ [Service]
7
+ WorkingDirectory=__DIR__
8
+ ExecStart=__PYMSCADA__ accuweatherclient --config __DIR__/config/accuweather.yaml
9
+ Restart=always
10
+ RestartSec=5
11
+ User=__USER__
12
+ Group=__USER__
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -0,0 +1,96 @@
1
+ """Poll SNMP OIDs from devices."""
2
+ import asyncio
3
+ import aiohttp
4
+ import logging
5
+ from time import time
6
+ from pymscada.bus_client import BusClient
7
+ from pymscada.periodic import Periodic
8
+ from pymscada.tag import Tag
9
+
10
+
11
+ class AccuWeatherClient:
12
+ """Get forecast information from AccuWeather."""
13
+
14
+ def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
15
+ proxy: str = None, api: dict = {}, tags: dict = {}) -> None:
16
+ """
17
+ Connect to bus on bus_ip:bus_port.
18
+
19
+ Event loop must be running.
20
+ """
21
+ self.busclient = None
22
+ if bus_ip is not None:
23
+ self.busclient = BusClient(bus_ip, bus_port, module='AccuWeather')
24
+ self.proxy = proxy
25
+ self.map_bus = id(self)
26
+ self.tags = {tagname: Tag(tagname, float) for tagname in tags}
27
+ self.api = api
28
+ self.urls = [[f'{api["url"]}{x}?', api['query']]
29
+ for x in api['locations'].values()]
30
+ self.session = aiohttp.ClientSession()
31
+ self.queue = asyncio.Queue()
32
+ self.init_run = True
33
+
34
+ async def handle_response(self):
35
+ """Unpack the weather values from the json response."""
36
+ while True:
37
+ resp = await self.queue.get()
38
+ site = resp[0]
39
+ json = resp[1]
40
+ time_s = int(time())
41
+ time_mod = time_s - time_s % 3600
42
+ for record in json:
43
+ epoch = record['EpochDateTime']
44
+ hour = int((epoch - time_mod) / 3600)
45
+ logging.warning(f'epoch {epoch} time_s {time_s} hour {hour}')
46
+ if hour not in self.api['times']:
47
+ continue
48
+ suffix = ''
49
+ if hour > 0:
50
+ suffix = f'_{int(hour)}'
51
+ for parm, key1, key2, key in [
52
+ ['Temp', 'Temperature', None, 'Value'],
53
+ ['WindDir', 'Wind', 'Direction', 'Degrees'],
54
+ ['WindSpeed', 'Wind', 'Speed', 'Value'],
55
+ ['Rain', 'Rain', None, 'Value'],
56
+ ['Snow', 'Snow', None, 'Value']
57
+ ]:
58
+ tag = self.tags[f'{site}{parm}{suffix}']
59
+ if key2 is None:
60
+ value = record[key1][key]
61
+ else:
62
+ value = record[key1][key2][key]
63
+ if tag.value != value:
64
+ tag.value = value, int(epoch * 1e6), self.map_bus
65
+
66
+ async def fetch_data(self, location, url, query):
67
+ """HTTP get."""
68
+ logging.warning(f'poll {location} {url}')
69
+ try:
70
+ async with self.session.get(url, params=query,
71
+ proxy=self.proxy) as resp:
72
+ self.queue.put_nowait([location, await resp.json()])
73
+ except asyncio.TimeoutError as e:
74
+ logging.warning('AccuWeather {e}')
75
+
76
+ async def poll(self):
77
+ """Poll the weather site near the start of each hour."""
78
+ now = int(time())
79
+ if now % 3600 != 120 and not self.init_run:
80
+ return
81
+ self.init_run = False
82
+ if not self.queue.empty():
83
+ return
84
+ # Get the weather forecasts
85
+ query = self.api['query']
86
+ for location, location_id in self.api['locations'].items():
87
+ url = f'{self.api["url"]}{location_id}'
88
+ asyncio.create_task(self.fetch_data(location, url, query))
89
+
90
+ async def start(self):
91
+ """Start bus connection and PLC polling."""
92
+ if self.busclient is not None:
93
+ await self.busclient.start()
94
+ self.handle = asyncio.create_task(self.handle_response())
95
+ self.periodic = Periodic(self.poll, 1.0)
96
+ await self.periodic.start()
@@ -12,6 +12,7 @@ from pymscada.files import Files
12
12
  from pymscada.history import History
13
13
  from pymscada.opnotes import OpNotes
14
14
  from pymscada.www_server import WwwServer
15
+ from pymscada.iodrivers.accuweather import AccuWeatherClient
15
16
  from pymscada.iodrivers.logix_client import LogixClient
16
17
  from pymscada.iodrivers.modbus_client import ModbusClient
17
18
  from pymscada.iodrivers.modbus_server import ModbusServer
@@ -186,6 +187,18 @@ class _validate(Module):
186
187
  print(e)
187
188
 
188
189
 
190
+ class _AccuWeatherClient(Module):
191
+ """Bus Module."""
192
+
193
+ name = 'accuweatherclient'
194
+ help = 'poll weather information'
195
+
196
+ def run_once(self, options):
197
+ """Create the module."""
198
+ config = Config(options.config)
199
+ self.module = AccuWeatherClient(**config)
200
+
201
+
189
202
  class _LogixClient(Module):
190
203
  """Bus Module."""
191
204
 
@@ -275,6 +288,7 @@ def args(_version: str):
275
288
  _Console(s)
276
289
  _checkout(s)
277
290
  _validate(s)
291
+ _AccuWeatherClient(s)
278
292
  _LogixClient(s)
279
293
  _ModbusServer(s)
280
294
  _ModbusClient(s)
@@ -240,7 +240,7 @@ class Tag(metaclass=UniqueTag):
240
240
  def time_us(self, time_us: int):
241
241
  """Make sure this is int, should _always_ be. TODO remove."""
242
242
  if type(time_us) is not int:
243
- logging.warning(f"{self.name} was not an int, FIX.")
243
+ logging.warning(f"{self.name} time_us was not an int, FIX.")
244
244
  self.__time_us = int(time_us)
245
245
  else:
246
246
  self.__time_us = time_us
File without changes
File without changes
@@ -5,6 +5,12 @@ port: 8324
5
5
  get_path: __HOME__/angmscada/dist/angmscada
6
6
  serve_path: __HOME__/pymscada
7
7
  pages:
8
+ - name: Notes
9
+ parent:
10
+ items:
11
+ - type: opnote
12
+ site: [Site 1, Site 2]
13
+ by: [B1, B2]
8
14
  - name: Default Main
9
15
  parent:
10
16
  items:
@@ -35,12 +41,6 @@ pages:
35
41
  - Four
36
42
  - Five
37
43
  - {tagname: FloatSelect, type: selectdict, opts: {type: float, dp: 2}}
38
- - name: Notes
39
- parent:
40
- items:
41
- - type: opnote
42
- site: [Site 1, Site 2]
43
- by: [B1, B2]
44
44
  - name: Trends
45
45
  parent: Dropdown
46
46
  items:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes