pymscada 0.1.11b10__py3-none-any.whl → 0.2.0__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.
- pymscada/alarms.py +353 -0
- pymscada/bus_client.py +6 -5
- pymscada/bus_server.py +14 -2
- pymscada/callout.py +206 -0
- pymscada/checkout.py +87 -89
- pymscada/console.py +4 -3
- pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/demo/alarms.yaml +5 -0
- pymscada/demo/callout.yaml +17 -0
- pymscada/demo/openweather.yaml +1 -1
- pymscada/demo/pymscada-alarms.service +16 -0
- pymscada/demo/pymscada-callout.service +16 -0
- pymscada/demo/pymscada-io-openweather.service +15 -0
- pymscada/demo/{pymscada-io-accuweather.service → pymscada-io-witsapi.service} +2 -2
- pymscada/demo/tags.yaml +4 -0
- pymscada/demo/witsapi.yaml +17 -0
- pymscada/files.py +3 -3
- pymscada/history.py +64 -8
- pymscada/iodrivers/openweather.py +131 -50
- pymscada/iodrivers/witsapi.py +217 -0
- pymscada/iodrivers/witsapi_POC.py +246 -0
- pymscada/main.py +1 -1
- pymscada/module_config.py +40 -14
- pymscada/opnotes.py +81 -16
- pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/protocol_constants.py +51 -33
- pymscada/tag.py +0 -22
- pymscada/www_server.py +72 -17
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/METADATA +9 -7
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/RECORD +38 -25
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/WHEEL +2 -1
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/entry_points.txt +0 -3
- pymscada-0.2.0.dist-info/top_level.txt +1 -0
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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__(
|
|
14
|
-
|
|
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')
|
|
@@ -41,82 +60,144 @@ class OpenWeatherClient:
|
|
|
41
60
|
|
|
42
61
|
def update_tags(self, location, data, suffix):
|
|
43
62
|
"""Update tags for forecast weather."""
|
|
63
|
+
if 'dt' not in data:
|
|
64
|
+
logging.error(f'No timestamp in data for {location}, skipping update')
|
|
65
|
+
return
|
|
44
66
|
for parameter in self.parameters:
|
|
45
67
|
tagname = f"{location}_{parameter}{suffix}"
|
|
46
|
-
if parameter == 'Temp':
|
|
47
|
-
value = data['main']['temp']
|
|
48
|
-
elif parameter == 'WindSpeed':
|
|
49
|
-
value = data['wind']['speed']
|
|
50
|
-
elif parameter == 'WindDir':
|
|
51
|
-
value = data['wind'].get('deg', 0)
|
|
52
|
-
elif parameter == 'Rain':
|
|
53
|
-
value = data.get('rain', {}).get('1h', 0)
|
|
54
68
|
try:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
if parameter == 'Temp':
|
|
70
|
+
main_data = data.get('main', {})
|
|
71
|
+
value = main_data.get('temp', 0)
|
|
72
|
+
elif parameter == 'WindSpeed':
|
|
73
|
+
wind_data = data.get('wind', {})
|
|
74
|
+
value = wind_data.get('speed', 0)
|
|
75
|
+
elif parameter == 'WindDir':
|
|
76
|
+
wind_data = data.get('wind', {})
|
|
77
|
+
value = wind_data.get('deg', 0)
|
|
78
|
+
elif parameter == 'Rain':
|
|
79
|
+
rain_data = data.get('rain', {})
|
|
80
|
+
value = rain_data.get('1h', 0)
|
|
81
|
+
else:
|
|
82
|
+
logging.warning(f'Unknown parameter {parameter} for {tagname}')
|
|
83
|
+
continue
|
|
84
|
+
time_us = int(data['dt'] * 1_000_000)
|
|
85
|
+
self.tags[tagname].value = value, time_us, self.map_bus
|
|
86
|
+
logging.debug(f'Updated {tagname} = {value} at timestamp {data["dt"]}')
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logging.error(
|
|
89
|
+
f'Error updating {tagname}: {type(e).__name__} - {str(e)}'
|
|
90
|
+
)
|
|
58
91
|
|
|
59
92
|
async def handle_response(self):
|
|
60
93
|
"""Handle responses from the API."""
|
|
61
94
|
while True:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
try:
|
|
96
|
+
location, data = await self.queue.get()
|
|
97
|
+
logging.debug(f'Processing data for {location}')
|
|
98
|
+
|
|
99
|
+
if 'dt' in data: # Current weather data
|
|
100
|
+
self.update_tags(location, data, '')
|
|
101
|
+
elif 'list' in data: # Forecast data
|
|
102
|
+
now = int(time())
|
|
103
|
+
for forecast in data['list']:
|
|
104
|
+
hours_ahead = int((forecast['dt'] - now) / 3600)
|
|
105
|
+
if hours_ahead in self.times:
|
|
106
|
+
suffix = f'_{hours_ahead:02d}'
|
|
107
|
+
self.update_tags(location, forecast, suffix)
|
|
108
|
+
|
|
109
|
+
self.queue.task_done()
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logging.error(f'Error handling response: {type(e).__name__} - {str(e)}')
|
|
73
112
|
|
|
74
113
|
async def fetch_current_data(self):
|
|
75
114
|
"""Fetch current weather data for all locations."""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
115
|
+
try:
|
|
116
|
+
if self.session is None:
|
|
117
|
+
self.session = aiohttp.ClientSession()
|
|
118
|
+
|
|
119
|
+
for location, coords in self.locations.items():
|
|
120
|
+
base_params = {
|
|
121
|
+
'lat': coords.get('lat'),
|
|
122
|
+
'lon': coords.get('lon'),
|
|
123
|
+
'appid': self.api_key,
|
|
124
|
+
'units': self.units
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Validate required parameters
|
|
128
|
+
if not all(base_params.values()):
|
|
129
|
+
logging.error(
|
|
130
|
+
f'Missing required parameters for {location}: '
|
|
131
|
+
f'{[k for k, v in base_params.items() if not v]}'
|
|
132
|
+
)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
async with self.session.get(
|
|
137
|
+
self.current_url,
|
|
138
|
+
params=base_params,
|
|
139
|
+
proxy=self.proxy,
|
|
140
|
+
timeout=30 # Add timeout
|
|
141
|
+
) as resp:
|
|
142
|
+
if resp.status == 200:
|
|
143
|
+
data = await resp.json()
|
|
144
|
+
logging.debug(
|
|
145
|
+
f'Received current weather data for {location}'
|
|
146
|
+
)
|
|
147
|
+
await self.queue.put((location, data))
|
|
148
|
+
else:
|
|
149
|
+
error_text = await resp.text()
|
|
150
|
+
logging.error(
|
|
151
|
+
f'OpenWeather API error for {location}: '
|
|
152
|
+
f'Status: {resp.status}, Response: {error_text[:200]}'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
except asyncio.TimeoutError:
|
|
156
|
+
logging.error(f'Timeout fetching data for {location}')
|
|
157
|
+
except aiohttp.ClientError as e:
|
|
158
|
+
logging.error(
|
|
159
|
+
f'Network error for {location}: {type(e).__name__} - {str(e)}'
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logging.error(
|
|
163
|
+
f'Unexpected error for {location}: {type(e).__name__} - {str(e)}'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logging.error(f'Fatal error in fetch_current_data: {type(e).__name__} - {str(e)}')
|
|
93
168
|
|
|
94
169
|
async def fetch_forecast_data(self):
|
|
95
170
|
"""Fetch forecast weather data for all locations."""
|
|
96
|
-
logging.info('fetching forecast')
|
|
97
171
|
if self.session is None:
|
|
98
172
|
self.session = aiohttp.ClientSession()
|
|
99
173
|
for location, coords in self.locations.items():
|
|
100
|
-
base_params = {
|
|
101
|
-
'
|
|
174
|
+
base_params = {
|
|
175
|
+
'lat': coords['lat'],
|
|
176
|
+
'lon': coords['lon'],
|
|
177
|
+
'appid': self.api_key,
|
|
178
|
+
'units': self.units
|
|
179
|
+
}
|
|
102
180
|
try:
|
|
103
181
|
async with self.session.get(self.forecast_url,
|
|
104
182
|
params=base_params, proxy=self.proxy) as resp:
|
|
105
183
|
if resp.status == 200:
|
|
106
|
-
|
|
184
|
+
data = await resp.json()
|
|
185
|
+
logging.info(f'Queue forecast {location} {data}')
|
|
186
|
+
await self.queue.put((location, data))
|
|
107
187
|
else:
|
|
108
|
-
logging.
|
|
109
|
-
f'
|
|
188
|
+
logging.error(f'OpenWeather forecast API error for '
|
|
189
|
+
f'{location}: Status:{resp.status}, '
|
|
190
|
+
f'Response:{await resp.text()}')
|
|
110
191
|
except Exception as e:
|
|
111
|
-
logging.
|
|
112
|
-
f'{
|
|
192
|
+
logging.error(f'OpenWeather forecast API error for {location}: '
|
|
193
|
+
f'Exception:{type(e).__name__}, Message:{str(e)}')
|
|
113
194
|
|
|
114
195
|
async def poll(self):
|
|
115
196
|
"""Poll OpenWeather APIs every 10 minutes."""
|
|
116
197
|
now = int(time())
|
|
117
198
|
if now % 600 == 0: # Every 10 minutes
|
|
118
199
|
asyncio.create_task(self.fetch_current_data())
|
|
119
|
-
if now %
|
|
200
|
+
if now % 3600 == 60: # Every 3 hours, offset by 1 minute
|
|
120
201
|
asyncio.create_task(self.fetch_forecast_data())
|
|
121
202
|
|
|
122
203
|
async def start(self):
|
|
@@ -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
|
+
|