pymscada 0.2.0rc4__py3-none-any.whl → 0.2.0rc7__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.

@@ -0,0 +1,16 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1324
3
+ rta_tag: __callout__
4
+ alarms: __alarms__
5
+ ack: SI_Alarm_Ack
6
+ status: SO_Alarm_Status
7
+ callees:
8
+ - name: A name
9
+ sms: A number
10
+ - name: B name
11
+ sms: B number
12
+ groups:
13
+ - name: Aniwhenua Station
14
+ group:
15
+ - name: Aniwhenua System
16
+ group: System
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=pymscada - WITS client
3
+ BindsTo=pymscada-bus.service
4
+ After=pymscada-bus.service
5
+
6
+ [Service]
7
+ WorkingDirectory=__DIR__
8
+ ExecStart=__PYMSCADA__ witsapiclient --config __DIR__/config/witsapi.yaml
9
+ Restart=always
10
+ RestartSec=5
11
+ User=__USER__
12
+ Group=__USER__
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
pymscada/demo/tags.yaml CHANGED
@@ -113,10 +113,14 @@ localhost_ping:
113
113
  desc: Ping time to localhost
114
114
  type: float
115
115
  units: ms
116
+ alarm: '> 500 for 300'
117
+ group: System
116
118
  google_ping:
117
119
  desc: Ping time to google
118
120
  type: float
119
121
  units: ms
122
+ alarm: '> 100 for 30'
123
+ group: System
120
124
  Murupara_Temp:
121
125
  desc: Murupara Temperature
122
126
  type: float
@@ -0,0 +1,17 @@
1
+ bus_ip: 127.0.0.1
2
+ bus_port: 1324
3
+ proxy:
4
+ api:
5
+ url: 'https://api.electricityinfo.co.nz'
6
+ client_id: ${WITS_CLIENT_ID}
7
+ client_secret: ${WITS_CLIENT_SECRET}
8
+ gxp_list:
9
+ - MAT1101
10
+ - CYD2201
11
+ - BEN2201
12
+ back: 1
13
+ forward: 12
14
+ tags:
15
+ - MAT1101_RTD
16
+ - CYD2201_RTD
17
+ - BEN2201_RTD
@@ -0,0 +1,217 @@
1
+ """Poll WITS GXP pricing real time dispatch and forecast."""
2
+ import asyncio
3
+ import aiohttp
4
+ import datetime
5
+ import logging
6
+ import socket
7
+ from time import time
8
+ from pymscada.bus_client import BusClient
9
+ from pymscada.periodic import Periodic
10
+ from pymscada.tag import Tag
11
+
12
+ class WitsAPIClient:
13
+ """Get pricing data from WITS GXP APIs."""
14
+
15
+ def __init__(
16
+ self,
17
+ bus_ip: str | None = '127.0.0.1',
18
+ bus_port: int = 1324,
19
+ proxy: str | None = None,
20
+ api: dict = {},
21
+ tags: list = []
22
+ ) -> None:
23
+ """
24
+ Connect to bus on bus_ip:bus_port.
25
+
26
+ api dict should contain:
27
+ - client_id: WITS API client ID
28
+ - client_secret: WITS API client secret
29
+ - url: WITS API base URL
30
+ - gxp_list: list of GXP nodes to fetch prices for
31
+ - schedules: list of schedule types to fetch
32
+ - back: number of periods to look back
33
+ - forward: number of periods to look forward
34
+ """
35
+ if bus_ip is not None:
36
+ try:
37
+ socket.gethostbyname(bus_ip)
38
+ except socket.gaierror:
39
+ raise ValueError(f"Invalid bus_ip: {bus_ip}")
40
+ if not isinstance(proxy, str) and proxy is not None:
41
+ raise ValueError("proxy must be a string or None")
42
+ if not isinstance(api, dict):
43
+ raise ValueError("api must be a dictionary")
44
+ if not isinstance(tags, list):
45
+ raise ValueError("tags must be a list")
46
+
47
+ self.busclient = None
48
+ if bus_ip is not None:
49
+ self.busclient = BusClient(bus_ip, bus_port, module='WitsAPI')
50
+ self.proxy = proxy
51
+ self.map_bus = id(self)
52
+ self.tags = {tagname: Tag(tagname, float) for tagname in tags}
53
+
54
+ # API configuration
55
+ self.client_id = api['client_id']
56
+ self.client_secret = api['client_secret']
57
+ self.base_url = api['url']
58
+ self.gxp_list = api.get('gxp_list', [])
59
+ self.back = api.get('back', 2)
60
+ self.forward = api.get('forward', 72)
61
+
62
+ self.session = None
63
+ self.handle = None
64
+ self.periodic = None
65
+ self.queue = asyncio.Queue()
66
+
67
+ async def get_token(self):
68
+ """Get a new OAuth token"""
69
+ auth_url = f"{self.base_url}/login/oauth2/token"
70
+ data = {
71
+ "grant_type": "client_credentials",
72
+ "client_id": self.client_id,
73
+ "client_secret": self.client_secret
74
+ }
75
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
76
+ try:
77
+ async with self.session.post(auth_url, data=data, headers=headers) as response:
78
+ if response.status == 200:
79
+ result = await response.json()
80
+ self.session.headers.update({
81
+ "Authorization": f"Bearer {result['access_token']}"
82
+ })
83
+ return result["access_token"]
84
+ else:
85
+ error_text = await response.text()
86
+ logging.error(f'WITS API auth error: {error_text}')
87
+ return None
88
+ except Exception as e:
89
+ logging.error(f'WITS API auth error: {type(e).__name__} - {str(e)}')
90
+ return None
91
+
92
+ async def get_multi_schedule_prices(self):
93
+ """Get prices across multiple schedules"""
94
+ endpoint = "api/market-prices/v1/prices"
95
+ params = {
96
+ 'schedules': 'RTD,PRSS,PRSL',
97
+ 'marketType': 'E',
98
+ 'offset': 0
99
+ }
100
+ if self.gxp_list:
101
+ params['nodes'] = ','.join(self.gxp_list)
102
+ if self.back:
103
+ params['back'] = min(self.back, 48)
104
+ if self.forward:
105
+ params['forward'] = min(self.forward, 48)
106
+
107
+ query = '&'.join(f"{k}={v}" for k, v in params.items())
108
+ url = f"{self.base_url}/{endpoint}?{query}"
109
+
110
+ try:
111
+ async with self.session.get(url, proxy=self.proxy) as response:
112
+ if response.status == 200:
113
+ return await response.json()
114
+ else:
115
+ error_text = await response.text()
116
+ logging.error(f'WITS API error: {error_text}')
117
+ return None
118
+ except Exception as e:
119
+ logging.error(f'WITS API error: {type(e).__name__} - {str(e)}')
120
+ return None
121
+
122
+ def parse_prices(self, response):
123
+ """Parse API response into structured price dictionary"""
124
+ if not response:
125
+ return {}
126
+ prices = {}
127
+ for schedule_data in response:
128
+ schedule = schedule_data['schedule']
129
+ if 'prices' not in schedule_data:
130
+ continue
131
+ for price in schedule_data['prices']:
132
+ node = price['node']
133
+ trading_time = int(datetime.datetime.fromisoformat(
134
+ price['tradingDateTime'].replace('Z', '+00:00')
135
+ ).timestamp())
136
+ last_run = int(datetime.datetime.fromisoformat(
137
+ price['lastRunTime'].replace('Z', '+00:00')
138
+ ).timestamp())
139
+
140
+ if node not in prices:
141
+ prices[node] = {}
142
+ if trading_time not in prices[node]:
143
+ prices[node][trading_time] = {}
144
+ prices[node][trading_time][schedule] = [price['price'], last_run]
145
+ return prices
146
+
147
+ def update_tags(self, prices):
148
+ """Update tags with price data"""
149
+ for node in prices:
150
+ rtd = {}
151
+ for trading_time in prices[node]:
152
+ if 'RTD' in prices[node][trading_time]:
153
+ rtd_price, _ = prices[node][trading_time]['RTD']
154
+ rtd[trading_time] = rtd_price
155
+ continue
156
+ prss_price = None
157
+ prsl_price = None
158
+ if 'PRSS' in prices[node][trading_time]:
159
+ prss_price, prss_last_run = prices[node][trading_time]['PRSS']
160
+ if 'PRSL' in prices[node][trading_time]:
161
+ prsl_price, prsl_last_run = prices[node][trading_time]['PRSL']
162
+ if prsl_price is not None and prss_price is not None:
163
+ if prss_last_run > prsl_last_run:
164
+ rtd[trading_time] = prss_price
165
+ else:
166
+ rtd[trading_time] = prsl_price
167
+ continue
168
+ if prss_price is not None:
169
+ rtd[trading_time] = prss_price
170
+ elif prsl_price is not None:
171
+ rtd[trading_time] = prsl_price
172
+ tagname = f"{node}_RTD"
173
+ if tagname in self.tags:
174
+ for trading_time in sorted(rtd.keys()):
175
+ time_us = int(trading_time * 1_000_000)
176
+ self.tags[tagname].value = rtd[trading_time], time_us, self.map_bus
177
+
178
+ async def handle_response(self):
179
+ """Handle responses from the API."""
180
+ while True:
181
+ try:
182
+ prices = await self.queue.get()
183
+ if prices:
184
+ parsed_prices = self.parse_prices(prices)
185
+ self.update_tags(parsed_prices)
186
+ self.queue.task_done()
187
+ except Exception as e:
188
+ logging.error(f'Error handling response: {type(e).__name__} - {str(e)}')
189
+
190
+ async def fetch_data(self):
191
+ """Fetch price data from WITS API."""
192
+ try:
193
+ if self.session is None:
194
+ self.session = aiohttp.ClientSession()
195
+ token = await self.get_token()
196
+ if token:
197
+ prices = await self.get_multi_schedule_prices()
198
+ if prices:
199
+ await self.queue.put(prices)
200
+ except Exception as e:
201
+ logging.error(f'Error fetching data: {type(e).__name__} - {str(e)}')
202
+
203
+ async def poll(self):
204
+ """Poll WITS API every 5 minutes."""
205
+ now = int(time())
206
+ if now % 300 == 0: # Every 5 minutes
207
+ asyncio.create_task(self.fetch_data())
208
+
209
+ async def start(self):
210
+ """Start bus connection and API polling."""
211
+ if self.busclient is not None:
212
+ await self.busclient.start()
213
+ self.handle = asyncio.create_task(self.handle_response())
214
+ self.periodic = Periodic(self.poll, 1.0)
215
+ await self.periodic.start()
216
+
217
+
@@ -0,0 +1,246 @@
1
+ import aiohttp
2
+ import asyncio
3
+ import datetime
4
+
5
+ class WitsAPIClient:
6
+ def __init__(self, url, client_id, client_secret):
7
+ self.client_id = client_id
8
+ self.client_secret = client_secret
9
+ self.base_url = url
10
+ self.session = None
11
+
12
+ async def __aenter__(self):
13
+ """Create session and get token on entry"""
14
+ self.session = aiohttp.ClientSession()
15
+ return self
16
+
17
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
18
+ """Close session on exit"""
19
+ if self.session:
20
+ await self.session.close()
21
+
22
+ async def get_token(self):
23
+ """Get a new OAuth token"""
24
+ auth_url = f"{self.base_url}/login/oauth2/token"
25
+ data = {
26
+ "grant_type": "client_credentials",
27
+ "client_id": self.client_id,
28
+ "client_secret": self.client_secret
29
+ }
30
+
31
+ headers = {
32
+ "Content-Type": "application/x-www-form-urlencoded"
33
+ }
34
+
35
+ try:
36
+ async with self.session.post(auth_url, data=data, headers=headers) as response:
37
+ if response.status == 200:
38
+ result = await response.json()
39
+ self.session.headers.update({
40
+ "Authorization": f"Bearer {result['access_token']}"
41
+ })
42
+ return result["access_token"]
43
+ else:
44
+ error_text = await response.text()
45
+ return None
46
+ except Exception as e:
47
+ return None
48
+
49
+ async def get(self, endpoint):
50
+ """Make a GET request to the WITS API"""
51
+ url = f"{self.base_url}/{endpoint}"
52
+ try:
53
+ async with self.session.get(url) as response:
54
+ if response.status == 200:
55
+ result = await response.json()
56
+ return result
57
+ else:
58
+ error_text = await response.text()
59
+ return None
60
+ except Exception as e:
61
+ return None
62
+
63
+ async def get_schedules(self):
64
+ """Get list of schedules for which pricing data is available"""
65
+ endpoint = "api/market-prices/v1/schedules"
66
+ return await self.get(endpoint)
67
+
68
+ async def get_nodes(self):
69
+ """Get list of GXP/GIP nodes supported by this API"""
70
+ endpoint = "api/market-prices/v1/nodes"
71
+ return await self.get(endpoint)
72
+
73
+ async def get_schedule_prices(self, schedule='RTD', market_type='E', nodes=None,
74
+ back=None, forward=None, from_date=None, to_date=None,
75
+ island=None, offset=0):
76
+ """Get prices for a single schedule
77
+ Args:
78
+ schedule: Schedule type (e.g. 'RTD' for Real Time Dispatch)
79
+ market_type: 'E' for energy prices, 'R' for reserve prices
80
+ nodes: List of node IDs to filter by
81
+ back: Number of trading periods to look back (1-48)
82
+ forward: Number of trading periods to look ahead (1-48)
83
+ from_date: Start datetime (RFC3339 format)
84
+ to_date: End datetime (RFC3339 format)
85
+ island: Filter by island ('NI' or 'SI')
86
+ offset: Pagination offset
87
+ """
88
+ endpoint = f"api/market-prices/v1/schedules/{schedule}/prices"
89
+ params = {
90
+ 'marketType': market_type,
91
+ 'offset': offset
92
+ }
93
+ if nodes:
94
+ params['nodes'] = ','.join(nodes) if isinstance(nodes, list) else nodes
95
+ if back:
96
+ params['back'] = min(back, 48)
97
+ if forward:
98
+ params['forward'] = min(forward, 48)
99
+ if from_date:
100
+ params['from'] = from_date
101
+ if to_date:
102
+ params['to'] = to_date
103
+ if island:
104
+ params['island'] = island
105
+ query = '&'.join(f"{k}={v}" for k, v in params.items())
106
+ return await self.get(f"{endpoint}?{query}")
107
+
108
+ async def get_multi_schedule_prices(self, schedules, market_type='E', nodes=None,
109
+ back=None, forward=None, from_date=None,
110
+ to_date=None, island=None, offset=0):
111
+ """Get prices across multiple schedules
112
+ Args:
113
+ schedules: List of schedule types
114
+ market_type: 'E' for energy prices, 'R' for reserve prices
115
+ nodes: List of node IDs to filter by
116
+ back: Number of trading periods to look back (1-48)
117
+ forward: Number of trading periods to look ahead (1-48)
118
+ from_date: Start datetime (RFC3339 format)
119
+ to_date: End datetime (RFC3339 format)
120
+ island: Filter by island ('NI' or 'SI')
121
+ offset: Pagination offset
122
+ """
123
+ endpoint = "api/market-prices/v1/prices"
124
+ params = {
125
+ 'schedules': ','.join(schedules) if isinstance(schedules, list) else schedules,
126
+ 'marketType': market_type,
127
+ 'offset': offset
128
+ }
129
+ if nodes:
130
+ params['nodes'] = ','.join(nodes) if isinstance(nodes, list) else nodes
131
+ if back:
132
+ params['back'] = min(back, 48)
133
+ if forward:
134
+ params['forward'] = min(forward, 48)
135
+ if from_date:
136
+ params['from'] = from_date
137
+ if to_date:
138
+ params['to'] = to_date
139
+ if island:
140
+ params['island'] = island
141
+ query = '&'.join(f"{k}={v}" for k, v in params.items())
142
+ return await self.get(f"{endpoint}?{query}")
143
+
144
+ def parse_prices(self, response):
145
+ """Parse API response into structured price dictionary
146
+ Returns dict in format:
147
+ {node: {trading_time_utc: {schedule: [price, last_run_utc]}}}
148
+ """
149
+ if not response:
150
+ return {}
151
+ prices = {}
152
+ for schedule_data in response:
153
+ schedule = schedule_data['schedule']
154
+ if 'prices' not in schedule_data:
155
+ continue
156
+ for price in schedule_data['prices']:
157
+ node = price['node']
158
+ trading_time = int(datetime.datetime.fromisoformat(
159
+ price['tradingDateTime'].replace('Z', '+00:00')
160
+ ).timestamp())
161
+ last_run = int(datetime.datetime.fromisoformat(
162
+ price['lastRunTime'].replace('Z', '+00:00')
163
+ ).timestamp())
164
+
165
+ if node not in prices:
166
+ prices[node] = {}
167
+ if trading_time not in prices[node]:
168
+ prices[node][trading_time] = {}
169
+ prices[node][trading_time][schedule] = [price['price'], last_run]
170
+
171
+ # Create RTD_forecast schedule
172
+ for node in prices:
173
+ for trading_time in prices[node]:
174
+ if 'RTD' in prices[node][trading_time]:
175
+ prices[node][trading_time]['RTD_forecast'] = prices[node][trading_time]['RTD']
176
+ else:
177
+ # Find most recent schedule by last run time
178
+ latest_schedule = None
179
+ latest_last_run = 0
180
+ for schedule in prices[node][trading_time]:
181
+ if prices[node][trading_time][schedule][1] > latest_last_run:
182
+ latest_last_run = prices[node][trading_time][schedule][1]
183
+ latest_schedule = schedule
184
+ if latest_schedule:
185
+ prices[node][trading_time]['RTD_forecast'] = \
186
+ prices[node][trading_time][latest_schedule]
187
+
188
+ return prices
189
+
190
+ def print_prices(self, prices):
191
+ """Print prices in structured format with time information"""
192
+ now = datetime.datetime.now(datetime.timezone.utc)
193
+ now_ts = now.timestamp()
194
+ for node in sorted(prices.keys()):
195
+ print(f" - {node}:")
196
+ for trading_time in sorted(prices[node].keys()):
197
+ time_diff = trading_time - now_ts
198
+ # For future times on 30 minute boundaries, show half-hour intervals
199
+ if time_diff > 0 and trading_time % 1800 == 0:
200
+ half_hours = int(time_diff / 1800)
201
+ time_str = f"(+{half_hours})"
202
+ else:
203
+ # For past times or non-30min intervals, show actual time
204
+ dt = datetime.datetime.fromtimestamp(trading_time,
205
+ datetime.timezone.utc)
206
+ time_str = f"({dt.strftime('%Y-%m-%d %H:%M:%S')})"
207
+
208
+ print(f" - Trading Time UTC: {trading_time} {time_str}")
209
+ for schedule in sorted(prices[node][trading_time].keys()):
210
+ if schedule in ['RTD', 'PRSS', 'PRSL', 'RTD_forecast']:
211
+ price, last_run = prices[node][trading_time][schedule]
212
+ last_run_dt = datetime.datetime.fromtimestamp(
213
+ last_run, datetime.timezone.utc)
214
+ print(f" {schedule:12} Price: {price:8.2f}, "
215
+ f"Last Run: {last_run_dt.strftime('%H:%M:%S')}")
216
+
217
+
218
+ async def main(config):
219
+ async with WitsAPIClient(url=config['url'],client_id=config['client_id'],
220
+ client_secret=config['client_secret']) as client:
221
+ token = await client.get_token()
222
+ if token:
223
+ multi_prices = await client.get_multi_schedule_prices(
224
+ schedules=config['schedules'],
225
+ nodes=config['gxp_list'],
226
+ back=config['back'],
227
+ forward=config['forward']
228
+ )
229
+ if multi_prices:
230
+ prices_dict = client.parse_prices(multi_prices)
231
+ client.print_prices(prices_dict)
232
+ await asyncio.sleep(1)
233
+
234
+
235
+ CONFIG = {
236
+ 'url': 'https://api.electricityinfo.co.nz',
237
+ 'gxp_list': ['MAT1101', 'CYD2201', 'BEN2201'],
238
+ 'schedules': ['RTD', 'PRSS', 'PRSL'],
239
+ 'back': 2,
240
+ 'forward': 72,
241
+ 'client_id': 'xx',
242
+ 'client_secret': 'xx'
243
+ }
244
+
245
+ if __name__ == "__main__":
246
+ asyncio.run(main(CONFIG))
pymscada/module_config.py CHANGED
@@ -150,6 +150,18 @@ def create_module_registry():
150
150
  module_class='pymscada.iodrivers.snmp_client:SnmpClient',
151
151
  tags=False
152
152
  ),
153
+ ModuleDefinition(
154
+ name='witsapi',
155
+ help='poll WITS GXP pricing real time dispatch and forecast',
156
+ module_class='pymscada.iodrivers.witsapi:WitsAPIClient',
157
+ tags=False,
158
+ epilog=dedent("""
159
+ WITS_CLIENT_ID and WITS_CLIENT_SECRET can be set in the wits.yaml
160
+ or as environment variables:
161
+ vi ~/.bashrc
162
+ export WITS_CLIENT_ID='your_client_id'
163
+ export WITS_CLIENT_SECRET='your_client_secret'""")
164
+ ),
153
165
  ModuleDefinition(
154
166
  name='console',
155
167
  help='interactive bus console',
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pymscada
3
- Version: 0.2.0rc4
3
+ Version: 0.2.0rc7
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
@@ -21,6 +21,7 @@ Requires-Dist: pymscada-html==0.2.0rc4
21
21
  Requires-Dist: cerberus>=1.3.5
22
22
  Requires-Dist: pycomm3>=1.2.14
23
23
  Requires-Dist: pysnmplib>=5.0.24
24
+ Dynamic: license-file
24
25
 
25
26
  # pymscada
26
27
  #### [Docs](https://github.com/jamie0walton/pymscada/blob/main/docs/README.md)
@@ -1,16 +1,17 @@
1
1
  pymscada/__init__.py,sha256=NV_cIIwe66Ugp8ns426rtfJIIyskWbqwImD9p_5p0bQ,739
2
2
  pymscada/__main__.py,sha256=WcyVlrYOoDdktJhOoyubTOycMwpayksFdxwelRU5xpQ,272
3
- pymscada/alarms.py,sha256=KiGws42xu8Bcmy_hhZkKoYhfNzUzICSV7f8xQcY7M8c,13715
4
- pymscada/bus_client.py,sha256=ROShMcR2-y_i5CIvPxRdCRr-NpnMANjKFdLjKjMTRwo,9117
3
+ pymscada/alarms.py,sha256=eo-9-DSwV825ihuHApaYkADPpLouKbNLHyh0z4bdPoU,12936
4
+ pymscada/bus_client.py,sha256=eRGvHQ4sFM_n3rKvKDWZQJPQJqvKMWvxbH3MjtT2WBo,9131
5
5
  pymscada/bus_server.py,sha256=k7ht2SAr24Oab0hBOPeW4NRDF_RK-F46iE0cMzh7K4w,12323
6
+ pymscada/callout.py,sha256=o_GCB-3IFG-QIcHfrmW-N9CTTlsIu9nMLC-F1jQu1hQ,7274
6
7
  pymscada/checkout.py,sha256=RLuCMTEuUI7pp1hIRAUPbo8xYFta8TjArelx0SD4gOY,3897
7
8
  pymscada/config.py,sha256=vwGxieaJBYXiHNQEOYVDFaPuGmnUlCnbNm_W9bugKlc,1851
8
- pymscada/console.py,sha256=zM6TNXJY8ROcVzO3UBzi1qDSiit7mfLr2YprRoxv2oQ,8824
9
+ pymscada/console.py,sha256=EEsJLCvn8AFimN8qGNilX0ks6t3OFcGW5nw6OVAXfac,8850
9
10
  pymscada/files.py,sha256=iouEOPfEkVI0Qbbf1p-L324Y04zSrynVypLW0-1MThA,2499
10
11
  pymscada/history.py,sha256=7UEOeMnlSMv0LoWTqLWx7QwOW1FZZ4wAvzH6v6b0_vI,11592
11
12
  pymscada/main.py,sha256=d6EnK4-tEcvM5AqMHYhvqlnSh-E_wd0Tuxk-kXYSiDw,1854
12
13
  pymscada/misc.py,sha256=0Cj6OFhQonyhyk9x0BG5MiS-6EPk_w6zvavt8o_Hlf0,622
13
- pymscada/module_config.py,sha256=r1JBjOXtMk7n09kvlvnmA3d_BySEmJpdefFhRNKPdAY,8824
14
+ pymscada/module_config.py,sha256=sEoLUhMUFJfalH3CbhNPIqQd1bAL7bWCyPSMUKs6HJ4,9370
14
15
  pymscada/opnotes.py,sha256=MKM51IrB93B2-kgoTzlpOLpaMYs-7rPQHWmRLME-hQQ,7952
15
16
  pymscada/periodic.py,sha256=MLlL93VLvFqBBgjO1Us1t0aLHTZ5BFdW0B__G02T1nQ,1235
16
17
  pymscada/protocol_constants.py,sha256=lPJ4JEgFJ_puJjTym83EJIOw3UTUFbuFMwg3ohyUAGY,2414
@@ -23,6 +24,7 @@ pymscada/demo/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
23
24
  pymscada/demo/accuweather.yaml,sha256=Fk4rV0S8jCau0173QCzKW8TdUbc4crYVi0aD8fPLNgU,322
24
25
  pymscada/demo/alarms.yaml,sha256=Ea8tLZ0aEiyKM_m5MN1TF6xS-lI5ReXiz2oUPO8GvmQ,110
25
26
  pymscada/demo/bus.yaml,sha256=zde5JDo2Yv5s7NvJ569gAEoTDvsvgBwRPxfrYhsxj3w,26
27
+ pymscada/demo/callout.yaml,sha256=ze2UfAymU05lzVp-t8wSa9hYJMGSkhT4FwL4rVJQG4o,270
26
28
  pymscada/demo/files.yaml,sha256=XWtmGDJxtD4qdl2h7miUfJYkDKsvwNTgQjlGpR6LQNs,163
27
29
  pymscada/demo/history.yaml,sha256=c0OuYe8LbTeZqJGU2WKGgTEkOA0IYAjO3e046ddQB8E,55
28
30
  pymscada/demo/logixclient.yaml,sha256=G_NlJhBYwT1a9ceHDgO6fCNKFmBM2pVO_t9Xa1NqlRY,912
@@ -43,10 +45,12 @@ pymscada/demo/pymscada-io-modbusserver.service,sha256=g7Rzm6zGLq_qvTJRL_pcLl4Ps7
43
45
  pymscada/demo/pymscada-io-openweather.service,sha256=SQnZ-cq1V3qvZY7EgR_Vx36vCOw1ipfGoLoutHsxtNk,359
44
46
  pymscada/demo/pymscada-io-ping.service,sha256=Fm8qR4IVq0NupEvHLGONXGwjjQsx5VqaBYPewhg7-k4,329
45
47
  pymscada/demo/pymscada-io-snmpclient.service,sha256=Rsm8uiwnoGx-1MkXqYgtj4UP9-r7AEEeB9yoR0y0oVA,343
48
+ pymscada/demo/pymscada-io-witsapi.service,sha256=ZjNwUnZg7WZsCaBFk8aNibnCbwqtbhl1i9D8tdUGXiQ,343
46
49
  pymscada/demo/pymscada-opnotes.service,sha256=TlrTRgP3rzrlXT8isAGT_Wy38ScDjT1VvnlgW84XiS8,354
47
50
  pymscada/demo/pymscada-wwwserver.service,sha256=7Qy2wsMmFEsQn-b5mgAcsrAQZgXynkv8SpHD6hLvRGc,370
48
51
  pymscada/demo/snmpclient.yaml,sha256=z8iACrFvMftYUtqGrRjPZYZTpn7aOXI-Kp675NAM8cU,2013
49
- pymscada/demo/tags.yaml,sha256=9xydsQriKT0lNAW533rz-FMVgoedn6Lwc50AnNig7-k,2733
52
+ pymscada/demo/tags.yaml,sha256=1HH9SqevBE0P0NXHK0Slfu68gwx5iKpgyirClmAcXGY,2814
53
+ pymscada/demo/witsapi.yaml,sha256=B8F136jvLIYU8t-pOdsEU_j97qMo3RgGQ1Rs4ExhmeE,289
50
54
  pymscada/demo/wwwserver.yaml,sha256=mmwvSLUXUDCIPaHeCJdCETAp9Cc4wb5CuK_aGv01KWk,2759
51
55
  pymscada/demo/__pycache__/__init__.cpython-311.pyc,sha256=tpxZoW429YA-2mbwzOlhBmbSTcbvTJqgKCfDRMrhEJE,195
52
56
  pymscada/iodrivers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -61,15 +65,17 @@ pymscada/iodrivers/ping_client.py,sha256=UOQgUfoIcYqy5VvKyJ8XGHHjeSRTfjmrhyWEojh
61
65
  pymscada/iodrivers/ping_map.py,sha256=EbOteqfEYKIOMqPymROJ4now2If-ekEj6jnM5hthoSA,1403
62
66
  pymscada/iodrivers/snmp_client.py,sha256=66-IDzddeKcSnqOzNXIZ8wuuAqhIxZjyLNrDwDvHCvw,2708
63
67
  pymscada/iodrivers/snmp_map.py,sha256=sDdIR5ZPAETpozDfBt_XQiZ-f4t99UCPlzj7BxFxQyM,2369
68
+ pymscada/iodrivers/witsapi.py,sha256=Ga6JpEQRUciT_LxWW36LsVGkUeWjModtzPoWYIzyzHs,8381
69
+ pymscada/iodrivers/witsapi_POC.py,sha256=dQcR2k1wsLb_cnNqvAB4kJ7FdY0BlcnxiMoepr28Ars,10132
64
70
  pymscada/pdf/__init__.py,sha256=WsDDgkWnZBJbt2-cJCdc2NvRAv_T4a7WOC1Q0k_l0gI,29
65
71
  pymscada/pdf/one.pdf,sha256=eoJ45DrAjVZrwmwdA_EAz1fwmT44eRnt_tkc2pmMrKY,1488
66
72
  pymscada/pdf/two.pdf,sha256=TAuW5yLU1_wfmTH_I5ezHwY0pxhCVuZh3ixu0kwmJwE,1516
67
73
  pymscada/pdf/__pycache__/__init__.cpython-311.pyc,sha256=4KTfXrV9bGDbTIEv-zgIj_LvzLbVTj77lEC1wzMh9e0,194
68
74
  pymscada/tools/snmp_client2.py,sha256=pdn5dYyEv4q-ubA0zQ8X-3tQDYxGC7f7Xexa7QPaL40,1675
69
75
  pymscada/tools/walk.py,sha256=OgpprUbKLhEWMvJGfU1ckUt_PFEpwZVOD8HucCgzmOc,1625
70
- pymscada-0.2.0rc4.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
- pymscada-0.2.0rc4.dist-info/METADATA,sha256=F9_ab9hHNXQhJaoVfC7CtuEPKgMh5eDAaZOt8GdIr1M,2371
72
- pymscada-0.2.0rc4.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
73
- pymscada-0.2.0rc4.dist-info/entry_points.txt,sha256=2UJBi8jrqujnerrcXcq4F8GHJYVDt26sacXl94t3sd8,56
74
- pymscada-0.2.0rc4.dist-info/top_level.txt,sha256=LxIB-zrtgObJg0fgdGZXBkmNKLDYHfaH1Hw2YP2ZMms,9
75
- pymscada-0.2.0rc4.dist-info/RECORD,,
76
+ pymscada-0.2.0rc7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
77
+ pymscada-0.2.0rc7.dist-info/METADATA,sha256=fSgmvdFEaztKuACzjhu9ya2By5ufTzj1PAYfM4z0dVk,2393
78
+ pymscada-0.2.0rc7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
+ pymscada-0.2.0rc7.dist-info/entry_points.txt,sha256=2UJBi8jrqujnerrcXcq4F8GHJYVDt26sacXl94t3sd8,56
80
+ pymscada-0.2.0rc7.dist-info/top_level.txt,sha256=LxIB-zrtgObJg0fgdGZXBkmNKLDYHfaH1Hw2YP2ZMms,9
81
+ pymscada-0.2.0rc7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.7.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5