redbacktechpy 0.3.1__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.
- redbacktechpy/__init__.py +19 -0
- redbacktechpy/constants.py +62 -0
- redbacktechpy/exceptions.py +17 -0
- redbacktechpy/model.py +42 -0
- redbacktechpy/redbacktech_client.py +543 -0
- redbacktechpy/str_enum.py +23 -0
- redbacktechpy-0.3.1.dist-info/LICENSE +674 -0
- redbacktechpy-0.3.1.dist-info/METADATA +17 -0
- redbacktechpy-0.3.1.dist-info/RECORD +11 -0
- redbacktechpy-0.3.1.dist-info/WHEEL +5 -0
- redbacktechpy-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""Python API for Redback Tech Systems"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from aiohttp import ClientResponse, ClientSession
|
|
4
|
+
from typing import Any
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from constants import (
|
|
11
|
+
BaseUrl,
|
|
12
|
+
Endpoint,
|
|
13
|
+
Header,
|
|
14
|
+
InverterOperationType,
|
|
15
|
+
TIMEOUT,
|
|
16
|
+
AUTH_ERROR_CODES,
|
|
17
|
+
DEVICEINFOREFRESH,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from model import (
|
|
21
|
+
Inverters,
|
|
22
|
+
Batterys,
|
|
23
|
+
)
|
|
24
|
+
from exceptions import (
|
|
25
|
+
AuthError,
|
|
26
|
+
RedbackTechClientError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
LOGGER = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
class RedbackTechClient:
|
|
32
|
+
"""Redback Tech Client"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client_id: str, client_secret:str, portal_email: str, portal_password: str, session1: ClientSession | None = None, session2: ClientSession | None = None, timeout: int = TIMEOUT) -> None:
|
|
35
|
+
self.client_id: str = client_id
|
|
36
|
+
self.client_secret: str = client_secret
|
|
37
|
+
self.portal_email: str = portal_email
|
|
38
|
+
self.portal_password: str = portal_password
|
|
39
|
+
self.timeout: int = timeout
|
|
40
|
+
self.serial_numbers: list[str] | None = None
|
|
41
|
+
self._session1: ClientSession = session1 if session1 else ClientSession()
|
|
42
|
+
self._session2: ClientSession = session2 if session2 else ClientSession()
|
|
43
|
+
self.token: str | None = None
|
|
44
|
+
self.token_type: str | None = None
|
|
45
|
+
self.token_expiration: datetime | None = None
|
|
46
|
+
self._GAFToken: str | None = None
|
|
47
|
+
self._device_info_refresh_time: datetime | None = None
|
|
48
|
+
self._flatInverters = []
|
|
49
|
+
self._flatBatterys = []
|
|
50
|
+
self._redback_devices = []
|
|
51
|
+
|
|
52
|
+
async def get_redback_data(self):
|
|
53
|
+
"""Get Redback Data."""
|
|
54
|
+
#Check if we need to get a new device list
|
|
55
|
+
await self.create_device_info()
|
|
56
|
+
|
|
57
|
+
inverter_data: dict[str, Inverters] = {}
|
|
58
|
+
battery_data: dict[str, Batterys] = {}
|
|
59
|
+
|
|
60
|
+
if self._redback_devices is not None:
|
|
61
|
+
for device in self._redback_devices:
|
|
62
|
+
if device['device_type'] == 'inverter':
|
|
63
|
+
in_instance, in_id = await self._handle_inverter(device)
|
|
64
|
+
inverter_data[in_id] = in_instance
|
|
65
|
+
|
|
66
|
+
if device['device_type'] == 'battery':
|
|
67
|
+
bat_instance, bat_id = await self._handle_battery(device)
|
|
68
|
+
battery_data[bat_id] = bat_instance
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
async def api_login(self) -> None:
|
|
74
|
+
"""Login to Redback API and obtain token."""
|
|
75
|
+
login_url = f'{BaseUrl.API}{Endpoint.API_AUTH}'
|
|
76
|
+
|
|
77
|
+
headers = {
|
|
78
|
+
'Content-Type': Header.CONTENT_TYPE,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
data = b'client_id=' + self.client_id.encode() + b'&client_secret=' + self.client_secret.encode()
|
|
82
|
+
|
|
83
|
+
response = await self._api_post(login_url, headers, data)
|
|
84
|
+
self.token = response['token_type'] + ' '+ response['access_token']
|
|
85
|
+
self.token_type = ['token_type']
|
|
86
|
+
self.token_expiration = datetime.now() + timedelta(seconds=response['expires_in'])
|
|
87
|
+
|
|
88
|
+
async def portal_login(self) -> None:
|
|
89
|
+
"""Login to Redback Portal and obtain token."""
|
|
90
|
+
self._session2.cookie_jar.clear()
|
|
91
|
+
login_url = f'{BaseUrl.PORTAL}{Endpoint.PORTAL_LOGIN}'
|
|
92
|
+
response = await self._portal_get(login_url, {}, {})
|
|
93
|
+
await self._get_portal_token(response, 1)
|
|
94
|
+
data={
|
|
95
|
+
"Email": self.portal_email,
|
|
96
|
+
"Password": self.portal_password,
|
|
97
|
+
"__RequestVerificationToken": self._GAFToken
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
headers = {
|
|
101
|
+
'Referer': Header.REFERER_UI,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
response = await self._portal_post(login_url, headers, data)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
async def test_api_connection(self) -> dict[str, Any]:
|
|
108
|
+
"""Test API connection."""
|
|
109
|
+
await self._check_token()
|
|
110
|
+
if self.token is not None:
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
async def test_portal_connection(self) -> dict[str, Any]:
|
|
115
|
+
"""Test Portal connection."""
|
|
116
|
+
self._GAFToken = None
|
|
117
|
+
await self.portal_login()
|
|
118
|
+
if self._GAFToken is not None:
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
async def set_inverter_mode(self, serial_number: str, mode: str, power: int, ross_version: str) -> dict[str, Any]:
|
|
123
|
+
"""Set inverter mode."""
|
|
124
|
+
self.serial_number = serial_number
|
|
125
|
+
self.mode = mode
|
|
126
|
+
self.power = power
|
|
127
|
+
self.ross_version = ross_version
|
|
128
|
+
|
|
129
|
+
await self.portal_login()
|
|
130
|
+
|
|
131
|
+
full_url = f'{BaseUrl.PORTAL}{Endpoint.PORTAL_CONFIGURE}{self.serial_number}'
|
|
132
|
+
response = await self._portal_get(full_url, {}, {})
|
|
133
|
+
await self._get_portal_token(response, 2)
|
|
134
|
+
headers = {
|
|
135
|
+
'X-Requested-With': Header.X_REQUESTED_WITH,
|
|
136
|
+
'Content-Type': Header.CONTENT_TYPE,
|
|
137
|
+
'Referer': full_url
|
|
138
|
+
}
|
|
139
|
+
data = {
|
|
140
|
+
'SerialNumber':self.serial_number,
|
|
141
|
+
'AppliedTariffId':'',
|
|
142
|
+
'InverterOperation[Type]':InverterOperationType.SET,
|
|
143
|
+
'InverterOperation[Mode]':self.mode,
|
|
144
|
+
'InverterOperation[PowerInWatts]':self.power,
|
|
145
|
+
'InverterOperation[AppliedTarrifId]':'',
|
|
146
|
+
'ProductModelName': '',
|
|
147
|
+
'RossVersion':self.ross_version,
|
|
148
|
+
'__RequestVerificationToken':self._GAFToken
|
|
149
|
+
}
|
|
150
|
+
full_url = f'{BaseUrl.PORTAL}{Endpoint.PORTAL_INVERTER_SET}'
|
|
151
|
+
await self._portal_post(full_url, headers, data)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
async def get_inverter_list(self) -> dict[str, Any]:
|
|
155
|
+
self.serial_numbers = []
|
|
156
|
+
await self._check_token()
|
|
157
|
+
|
|
158
|
+
headers = {
|
|
159
|
+
'Authorization': self.token
|
|
160
|
+
}
|
|
161
|
+
full_url = f'{BaseUrl.API}{Endpoint.API_NODES}'
|
|
162
|
+
response = await self._api_get(full_url, headers, {})
|
|
163
|
+
|
|
164
|
+
for site in response['Data']:
|
|
165
|
+
for node in site['Nodes']:
|
|
166
|
+
if node['Type'] == 'Inverter':
|
|
167
|
+
self.serial_numbers.append(node['SerialNumber'])
|
|
168
|
+
return self.serial_numbers
|
|
169
|
+
|
|
170
|
+
async def get_dynamic_by_serial(self, serial_number: str) -> dict[str, Any]:
|
|
171
|
+
"""/Api/v2.21/EnergyData/Dynamic/BySerialNumber/{serialNumber}"""
|
|
172
|
+
self.serial_number = serial_number
|
|
173
|
+
await self._check_token()
|
|
174
|
+
|
|
175
|
+
headers = {
|
|
176
|
+
'Authorization': self.token,
|
|
177
|
+
'Content_type': 'text/json',
|
|
178
|
+
'accept': 'text/plain'
|
|
179
|
+
}
|
|
180
|
+
full_url = f'{BaseUrl.API}{Endpoint.API_ENERGY_DYNAMIC_BY_SERIAL}{self.serial_number}'
|
|
181
|
+
response = await self._api_get(full_url, headers, {})
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
async def get_config_by_serial(self, serial_number: str) -> dict[str, Any]:
|
|
185
|
+
"""/Api/v2/Configuration/Configuration/BySerialNumber/{serialNumber}"""
|
|
186
|
+
self.serial_number = serial_number
|
|
187
|
+
|
|
188
|
+
headers = {
|
|
189
|
+
'Authorization': self.token,
|
|
190
|
+
'Content_type': 'text/json',
|
|
191
|
+
'accept': 'text/plain'
|
|
192
|
+
}
|
|
193
|
+
full_url = f'{BaseUrl.API}{Endpoint.API_CONFIG_BY_SERIAL}{self.serial_number}'
|
|
194
|
+
response = await self._api_get(full_url, headers, {})
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
async def get_static_by_serial(self, serial_number: str) -> dict[str, Any]:
|
|
198
|
+
"""/Api/v2/EnergyData/Static/BySerialNumber/{serialNumber}"""
|
|
199
|
+
self.serial_number = serial_number
|
|
200
|
+
await self._check_token()
|
|
201
|
+
|
|
202
|
+
headers = {
|
|
203
|
+
'Authorization': self.token,
|
|
204
|
+
'Content_type': 'text/json',
|
|
205
|
+
'accept': 'text/plain'
|
|
206
|
+
}
|
|
207
|
+
full_url = f'{BaseUrl.API}{Endpoint.API_STATIC_BY_SERIAL}{self.serial_number}'
|
|
208
|
+
response = await self._api_get(full_url, headers, {})
|
|
209
|
+
return response
|
|
210
|
+
|
|
211
|
+
async def get_config_by_multiple_serial(self, serial_numbers: str | None=None) -> dict[str, Any]:
|
|
212
|
+
""""""
|
|
213
|
+
self._serial_numbers: str = serial_numbers if serial_numbers else self.serial_numbers
|
|
214
|
+
|
|
215
|
+
if self._serial_numbers is None:
|
|
216
|
+
self._serial_numbers = await self.get_inverter_list()
|
|
217
|
+
|
|
218
|
+
await self._check_token()
|
|
219
|
+
|
|
220
|
+
headers = {
|
|
221
|
+
'Authorization': self.token,
|
|
222
|
+
'Content_type': 'text/json',
|
|
223
|
+
'accept': 'text/plain'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
full_url = f'{BaseUrl.API}{Endpoint.API_STATIC_MULTIPLE_BY_SERIAL}'
|
|
227
|
+
response = await self._api_post_json(full_url, headers, self._serial_numbers)
|
|
228
|
+
|
|
229
|
+
return response
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async def get_site_list(self) -> dict[str, Any]:
|
|
233
|
+
self.site_ids = []
|
|
234
|
+
await self._check_token()
|
|
235
|
+
|
|
236
|
+
headers = {
|
|
237
|
+
'Authorization': self.token
|
|
238
|
+
}
|
|
239
|
+
full_url = f'{BaseUrl.API}{Endpoint.API_SITES}'
|
|
240
|
+
response = await self._api_get(full_url, headers, {})
|
|
241
|
+
|
|
242
|
+
for site in response['Data']:
|
|
243
|
+
self.site_ids.append(site)
|
|
244
|
+
return self.site_ids
|
|
245
|
+
|
|
246
|
+
async def close_sessions(self) -> None:
|
|
247
|
+
"""Close sessions."""
|
|
248
|
+
await self._session1.close()
|
|
249
|
+
await self._session2.close()
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
async def create_device_info(self) -> None:
|
|
253
|
+
device_info = True
|
|
254
|
+
if not await self._check_device_info_refresh():
|
|
255
|
+
#Get the device info if it needs to be refreshed
|
|
256
|
+
self._serial_numbers = await self.get_inverter_list()
|
|
257
|
+
device_info = False
|
|
258
|
+
|
|
259
|
+
for serial_number in self._serial_numbers:
|
|
260
|
+
response1 = await self.get_static_by_serial(serial_number)
|
|
261
|
+
response2 = await self.get_dynamic_by_serial(serial_number)
|
|
262
|
+
self._flatInverters = await self._convert_static_by_serial_to_inverter_list(response1, response2)
|
|
263
|
+
self._redback_devices.append(self._flatInverters)
|
|
264
|
+
#print(self._flatInverters['serial_number'])
|
|
265
|
+
#response = await self.create_dynamic_info()
|
|
266
|
+
if response1['Data']['Nodes'][0]['StaticData']['BatteryCount'] > 0:
|
|
267
|
+
soc_data = await self.get_config_by_serial(response1['Data']['Nodes'][0]['StaticData']['Id'])
|
|
268
|
+
self._flatBatterys = await self._convert_static_by_serial_to_battery_list(response1, response2, soc_data)
|
|
269
|
+
self._redback_devices.append(self._flatBatterys)
|
|
270
|
+
self._device_info_refresh_time = datetime.now() + timedelta(seconds=DEVICEINFOREFRESH)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
async def create_dynamic_info(self) -> None:
|
|
274
|
+
|
|
275
|
+
self._serial_numbers = await self.get_inverter_list()
|
|
276
|
+
self._dynamic_data = []
|
|
277
|
+
for serial_number in self._serial_numbers:
|
|
278
|
+
response = await self.get_dynamic_by_serial(serial_number)
|
|
279
|
+
self._dynamic_data.append(response)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
async def _convert_static_by_serial_to_inverter_list(self, data, data2) -> list[str]:
|
|
283
|
+
"""Convert static by serial to list."""
|
|
284
|
+
|
|
285
|
+
dataDict = {}
|
|
286
|
+
pvId =1
|
|
287
|
+
#static
|
|
288
|
+
dataDict['device_type'] = 'inverter'
|
|
289
|
+
dataDict['model'] = data['Data']['Nodes'][0]['StaticData']['ModelName']
|
|
290
|
+
dataDict['sw_version'] = data['Data']['Nodes'][0]['StaticData']['SoftwareVersion']
|
|
291
|
+
dataDict['hw_version'] = data['Data']['Nodes'][0]['StaticData']['FirmwareVersion']
|
|
292
|
+
dataDict['serial_number'] = data['Data']['Nodes'][0]['StaticData']['Id']
|
|
293
|
+
dataDict['latitude'] = data['Data']['StaticData']['Location']['Latitude']
|
|
294
|
+
dataDict['longitude'] = data['Data']['StaticData']['Location']['Longitude']
|
|
295
|
+
dataDict['network_connection'] = data['Data']['StaticData']['RemoteAccessConnection']['Type']
|
|
296
|
+
dataDict['approved_capacity'] = data['Data']['StaticData']['ApprovedCapacityW']
|
|
297
|
+
dataDict['generation_hard_limit_va'] = data['Data']['StaticData']['SiteDetails']['GenerationHardLimitVA']
|
|
298
|
+
dataDict['generation_soft_limit_va'] = data['Data']['StaticData']['SiteDetails']['GenerationSoftLimitVA']
|
|
299
|
+
dataDict['export_hard_limit_kw'] = data['Data']['StaticData']['SiteDetails']['ExportHardLimitkW']
|
|
300
|
+
dataDict['export_soft_limit_kw'] = data['Data']['StaticData']['SiteDetails']['ExportSoftLimitkW']
|
|
301
|
+
dataDict['site_export_limit_kw'] = data['Data']['StaticData']['SiteDetails']['SiteExportLimitkW']
|
|
302
|
+
dataDict['pv_panel_model'] = data['Data']['StaticData']['SiteDetails']['PanelModel']
|
|
303
|
+
dataDict['pv_panel_size_kw'] = data['Data']['StaticData']['SiteDetails']['PanelSizekW']
|
|
304
|
+
dataDict['system_type'] = data['Data']['StaticData']['SiteDetails']['SystemType']
|
|
305
|
+
dataDict['inverter_max_export_power_kw'] = data['Data']['StaticData']['SiteDetails']['InverterMaxExportPowerkW']
|
|
306
|
+
dataDict['inverter_max_import_power_kw'] = data['Data']['StaticData']['SiteDetails']['InverterMaxImportPowerkW']
|
|
307
|
+
dataDict['commissioning_date'] = data['Data']['StaticData']['CommissioningDate']
|
|
308
|
+
dataDict['model_name'] = data['Data']['Nodes'][0]['StaticData']['ModelName']
|
|
309
|
+
dataDict['nmi'] = data['Data']['StaticData']['NMI']
|
|
310
|
+
dataDict['site_id'] = data['Data']['StaticData']['Id']
|
|
311
|
+
dataDict['inverter_site_type'] = data['Data']['StaticData']['Type']
|
|
312
|
+
dataDict['battery_count'] = data['Data']['Nodes'][0]['StaticData']['BatteryCount']
|
|
313
|
+
dataDict['software_version'] = data['Data']['Nodes'][0]['StaticData']['SoftwareVersion']
|
|
314
|
+
dataDict['firmware_version'] = data['Data']['Nodes'][0]['StaticData']['FirmwareVersion']
|
|
315
|
+
dataDict['inverter_serial_number'] = data['Data']['Nodes'][0]['StaticData']['Id']
|
|
316
|
+
#dynamic
|
|
317
|
+
dataDict['timestamp_utc'] = data2['Data']['TimestampUtc']
|
|
318
|
+
dataDict['frequency_instantaneous'] = data2['Data']['FrequencyInstantaneousHz']
|
|
319
|
+
dataDict['pv_power_instantaneous_kw'] = data2['Data']['PvPowerInstantaneouskW']
|
|
320
|
+
dataDict['inverter_temperature_c'] = data2['Data']['InverterTemperatureC']
|
|
321
|
+
dataDict['pv_all_time_energy_kwh'] = data2['Data']['PvAllTimeEnergykWh']
|
|
322
|
+
dataDict['export_all_time_energy_kwh'] = data2['Data']['ExportAllTimeEnergykWh']
|
|
323
|
+
dataDict['import_all_time_energy_kwh'] = data2['Data']['ImportAllTimeEnergykWh']
|
|
324
|
+
dataDict['load_all_time_energy_kwh'] = data2['Data']['LoadAllTimeEnergykWh']
|
|
325
|
+
dataDict['status'] = data2['Data']['Status']
|
|
326
|
+
dataDict['power_mode_inverter_mode'] = data2['Data']['Inverters'][0]['PowerMode']['InverterMode']
|
|
327
|
+
dataDict['power_mode_power_w'] = data2['Data']['Inverters'][0]['PowerMode']['PowerW']
|
|
328
|
+
for pv in data2['Data']['PVs']:
|
|
329
|
+
dataDict[f'pv_{pvId}_current_a'] = pv['CurrentA']
|
|
330
|
+
dataDict[f'pv_{pvId}_voltage_v'] = pv['VoltageV']
|
|
331
|
+
dataDict[f'pv_{pvId}_power_kw'] = pv['PowerkW']
|
|
332
|
+
pvId += 1
|
|
333
|
+
for phase in data2['Data']['Phases']:
|
|
334
|
+
phaseAlpha=phase['Id']
|
|
335
|
+
dataDict[f'inverter_phase_{phaseAlpha}_active_exported_power_instantaneous_kw'] = phase['ActiveExportedPowerInstantaneouskW']
|
|
336
|
+
dataDict[f'inverter_phase_{phaseAlpha}_active_imported_power_instantaneous_kw'] = phase['ActiveImportedPowerInstantaneouskW']
|
|
337
|
+
dataDict[f'inverter_phase_{phaseAlpha}_voltage_instantaneous_v'] = phase['VoltageInstantaneousV']
|
|
338
|
+
dataDict[f'inverter_phase_{phaseAlpha}_current_instantaneous_a'] = phase['CurrentInstantaneousA']
|
|
339
|
+
dataDict[f'inverter_phase_{phaseAlpha}_power_factor_instantaneous_minus_1to1'] = phase['PowerFactorInstantaneousMinus1to1']
|
|
340
|
+
return dataDict
|
|
341
|
+
|
|
342
|
+
async def _convert_static_by_serial_to_battery_list(self, data, data2, soc_data) -> list[str]:
|
|
343
|
+
dataDict = {}
|
|
344
|
+
batteryName = ''
|
|
345
|
+
batteryId = 1
|
|
346
|
+
cabinetId = 1
|
|
347
|
+
dataDict['device_type'] = 'battery'
|
|
348
|
+
dataDict['model'] = data['Data']['Nodes'][0]['StaticData']['ModelName']
|
|
349
|
+
dataDict['sw_version'] = data['Data']['Nodes'][0]['StaticData']['SoftwareVersion']
|
|
350
|
+
dataDict['hw_version'] = data['Data']['Nodes'][0]['StaticData']['FirmwareVersion']
|
|
351
|
+
dataDict['serial_number'] = data['Data']['Nodes'][0]['StaticData']['Id']
|
|
352
|
+
|
|
353
|
+
dataDict['model'] = data['Data']['Nodes'][0]['StaticData']['ModelName']
|
|
354
|
+
dataDict['sw_version'] = data['Data']['Nodes'][0]['StaticData']['SoftwareVersion']
|
|
355
|
+
dataDict['hw_version'] = data['Data']['Nodes'][0]['StaticData']['FirmwareVersion']
|
|
356
|
+
dataDict['serial_number'] = data['Data']['Nodes'][0]['StaticData']['Id']
|
|
357
|
+
dataDict['min_soc_0_to_1'] = soc_data['Data']['MinSoC0to1']
|
|
358
|
+
dataDict['min_Offgrid_soc_0_to_1'] = soc_data['Data']['MinOffgridSoC0to1']
|
|
359
|
+
dataDict['latitude'] = data['Data']['StaticData']['Location']['Latitude']
|
|
360
|
+
dataDict['longitude'] = data['Data']['StaticData']['Location']['Longitude']
|
|
361
|
+
dataDict['battery_max_charge_power_kw'] = data['Data']['StaticData']['SiteDetails']['BatteryMaxChargePowerkW']
|
|
362
|
+
dataDict['battery_max_discharge_power_kw'] = data['Data']['StaticData']['SiteDetails']['BatteryMaxDischargePowerkW']
|
|
363
|
+
dataDict['battery_capacity_kwh'] = data['Data']['StaticData']['SiteDetails']['BatteryCapacitykWh']
|
|
364
|
+
dataDict['battery_usable_capacity_kwh'] = data['Data']['StaticData']['SiteDetails']['UsableBatteryCapacitykWh']
|
|
365
|
+
dataDict['system_type'] = data['Data']['StaticData']['SiteDetails']['SystemType']
|
|
366
|
+
dataDict['commissioning_date'] = data['Data']['StaticData']['CommissioningDate']
|
|
367
|
+
dataDict['site_id'] = data['Data']['StaticData']['Id']
|
|
368
|
+
dataDict['inverter_site_type'] = data['Data']['StaticData']['Type']
|
|
369
|
+
dataDict['model_name'] = data['Data']['Nodes'][0]['StaticData']['ModelName']
|
|
370
|
+
dataDict['battery_count'] = data['Data']['Nodes'][0]['StaticData']['BatteryCount']
|
|
371
|
+
dataDict['software_version'] = data['Data']['Nodes'][0]['StaticData']['SoftwareVersion']
|
|
372
|
+
dataDict['firmware_version'] = data['Data']['Nodes'][0]['StaticData']['FirmwareVersion']
|
|
373
|
+
for battery in data['Data']['Nodes'][0]['StaticData']['BatteryModels']:
|
|
374
|
+
if battery != 'Unknown':
|
|
375
|
+
batteryName = battery
|
|
376
|
+
dataDict[f'battery_{batteryId}_model'] = batteryName
|
|
377
|
+
else:
|
|
378
|
+
dataDict[f'battery_{batteryId}_model'] = batteryName
|
|
379
|
+
dataDict[f'battery_{batteryId}_current_negative_in_charging_a'] = data2['Data']['Battery']['Modules'][batteryId-1]['CurrentNegativeIsChargingA']
|
|
380
|
+
dataDict[f'battery_{batteryId}_voltage_v'] = data2['Data']['Battery']['Modules'][batteryId-1]['VoltageV']
|
|
381
|
+
dataDict[f'battery_{batteryId}_power_negative_is_charging_kw'] = data2['Data']['Battery']['Modules'][batteryId-1]['PowerNegativeIsChargingkW']
|
|
382
|
+
dataDict[f'battery_{batteryId}_soc_0to1'] = data2['Data']['Battery']['Modules'][batteryId-1]['SoC0To1']
|
|
383
|
+
batteryId += 1
|
|
384
|
+
for cabinet in data2['Data']['Battery']['Cabinets']:
|
|
385
|
+
dataDict[f'battery_cabinet_{cabinetId}_temperature_c'] = cabinet['TemperatureC']
|
|
386
|
+
dataDict[f'battery_cabinet_{cabinetId}_fan_state'] = cabinet['FanState']
|
|
387
|
+
cabinetId += 1
|
|
388
|
+
dataDict['inverter_serial_number'] = data['Data']['Nodes'][0]['StaticData']['Id']
|
|
389
|
+
dataDict['timestamp_utc'] = data2['Data']['TimestampUtc']
|
|
390
|
+
dataDict['battery_soc_instantaneous_0to1'] = data2['Data']['BatterySoCInstantaneous0to1']
|
|
391
|
+
dataDict['battery_power_negative_is_charging_kw'] = data2['Data']['BatteryPowerNegativeIsChargingkW']
|
|
392
|
+
dataDict['battery_charge_all_time_energy_kwh'] = data2['Data']['BatteryChargeAllTimeEnergykWh']
|
|
393
|
+
dataDict['battery_discharge_all_time_energy_kwh'] = data2['Data']['BatteryDischargeAllTimeEnergykWh']
|
|
394
|
+
dataDict['status'] = data2['Data']['Status']
|
|
395
|
+
dataDict['battery_current_negative_in_charging_a'] = data2['Data']['Battery']['CurrentNegativeIsChargingA']
|
|
396
|
+
dataDict['battery_voltage_v'] = data2['Data']['Battery']['VoltageV']
|
|
397
|
+
dataDict['battery_voltage_type'] = data2['Data']['Battery']['VoltageType']
|
|
398
|
+
dataDict['battery_no_of_modules'] = data2['Data']['Battery']['NumberOfModules']
|
|
399
|
+
|
|
400
|
+
dataDict['battery_currently_stored_kwh'] = (data['Data']['StaticData']['SiteDetails']['BatteryCapacitykWh'] * data2['Data']['BatterySoCInstantaneous0to1'] )
|
|
401
|
+
dataDict['battery_currently_usable_kwh'] = round(data['Data']['StaticData']['SiteDetails']['BatteryCapacitykWh'] * (data2['Data']['BatterySoCInstantaneous0to1']- soc_data['Data']['MinSoC0to1']),2)
|
|
402
|
+
return dataDict
|
|
403
|
+
|
|
404
|
+
async def _handle_inverter(self, device: dict[str, Any]) -> (Inverters, str):
|
|
405
|
+
"""Handle inverter data."""
|
|
406
|
+
|
|
407
|
+
device_type: str = device['device_type'].lower()
|
|
408
|
+
|
|
409
|
+
data = {
|
|
410
|
+
'id': device['serial_number'] + device_type
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
inverter_instance = Inverters(
|
|
415
|
+
id=data['id'],
|
|
416
|
+
device_serial_number=device['serial_number'],
|
|
417
|
+
data=device,
|
|
418
|
+
type=device_type
|
|
419
|
+
)
|
|
420
|
+
return inverter_instance, data['id']
|
|
421
|
+
|
|
422
|
+
async def _handle_battery(self, device: dict[str, Any]) -> (Batterys, str):
|
|
423
|
+
"""Handle inverter data."""
|
|
424
|
+
|
|
425
|
+
device_type: str = device['device_type'].lower()
|
|
426
|
+
#inverter_data: dict[str, Any] = {}
|
|
427
|
+
data = {
|
|
428
|
+
'id': device['serial_number'] + device_type
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
battery_instance = Batterys(
|
|
432
|
+
id=data['id'],
|
|
433
|
+
device_serial_number=device['serial_number'],
|
|
434
|
+
data=device,
|
|
435
|
+
type=device_type
|
|
436
|
+
)
|
|
437
|
+
return battery_instance, data['id']
|
|
438
|
+
|
|
439
|
+
async def _api_post(self, url: str, headers: dict[str, Any], data ) -> dict[str, Any]:
|
|
440
|
+
"""Make POST API call."""
|
|
441
|
+
|
|
442
|
+
async with self._session1.post(url, headers=headers, data=data, timeout=self.timeout) as resp:
|
|
443
|
+
return await self._api_response(resp)
|
|
444
|
+
|
|
445
|
+
async def _api_post_json(self, url: str, headers: dict[str, Any], data ) -> dict[str, Any]:
|
|
446
|
+
"""Make POST API call."""
|
|
447
|
+
|
|
448
|
+
async with self._session1.post(url, headers=headers, json=data, timeout=self.timeout) as resp:
|
|
449
|
+
return await self._api_response(resp)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
async def _api_get(self, url: str, headers: dict[str, Any], data: dict[str, Any]) -> dict[str, Any]:
|
|
453
|
+
"""Make GET API call."""
|
|
454
|
+
|
|
455
|
+
async with self._session1.get(url, headers=headers, data=data, timeout=self.timeout) as resp:
|
|
456
|
+
return await self._api_response(resp)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
async def _api_response(resp: ClientResponse): # -> dict[str, Any]:
|
|
460
|
+
"""Return response from API call."""
|
|
461
|
+
|
|
462
|
+
if resp.status != 200:
|
|
463
|
+
error = await resp.text()
|
|
464
|
+
raise RedbackTechClientError(f'RedbackTech API Error Encountered. Status: {resp.status}; Error: {error}')
|
|
465
|
+
try:
|
|
466
|
+
response: dict[str, Any] = await resp.json()
|
|
467
|
+
except Exception as error:
|
|
468
|
+
raise RedbackTechClientError(f'Could not return json {error}') from error
|
|
469
|
+
if 'error' in response:
|
|
470
|
+
code = response['error'] #['code']
|
|
471
|
+
if code in AUTH_ERROR_CODES:
|
|
472
|
+
raise AuthError(f'Redback API Error: {code}')
|
|
473
|
+
# elif code in SERVER_ERROR_CODES:
|
|
474
|
+
# raise ServerError(f'PetKit Error {code}: {SERVER_ERROR_CODES[code]}')
|
|
475
|
+
else:
|
|
476
|
+
raise RedbackTechClientError(f'RedbackTech API Error: {code}')
|
|
477
|
+
return response
|
|
478
|
+
|
|
479
|
+
async def _check_device_info_refresh(self) -> None:
|
|
480
|
+
"""Check to see if device info is about to expire.
|
|
481
|
+
If there is no device info, a new device info is obtained. In addition,
|
|
482
|
+
if the current device info is about to expire within 30 minutes
|
|
483
|
+
or has already expired, a new device info is obtained.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
current_dt = datetime.now()
|
|
487
|
+
if self._device_info_refresh_time is None:
|
|
488
|
+
True
|
|
489
|
+
elif (self._device_info_refresh_time-current_dt).total_seconds() < 10:
|
|
490
|
+
True
|
|
491
|
+
else:
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
async def _check_token(self) -> None:
|
|
495
|
+
"""Check to see if there is a valid token or if token is about to expire.
|
|
496
|
+
If there is no token, a new token is obtained. In addition,
|
|
497
|
+
if the current token is about to expire within 60 minutes
|
|
498
|
+
or has already expired, a new token is obtained.
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
current_dt = datetime.now()
|
|
502
|
+
if (self.token or self.token_expiration) is None:
|
|
503
|
+
await self.api_login()
|
|
504
|
+
elif (self.token_expiration-current_dt).total_seconds() < 3600:
|
|
505
|
+
await self.api_login()
|
|
506
|
+
else:
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
async def _get_portal_token(self, response, type):
|
|
510
|
+
soup = BeautifulSoup(response , features="html.parser")
|
|
511
|
+
if type == 1: #LOGINURL:
|
|
512
|
+
form = soup.find("form", class_="login-form")
|
|
513
|
+
else:
|
|
514
|
+
form = soup.find('form', id='GlobalAntiForgeryToken')
|
|
515
|
+
hidden_input = form.find("input", type="hidden")
|
|
516
|
+
self._GAFToken = hidden_input.attrs['value']
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
async def _portal_post(self, url: str, headers: dict[str, Any], data ) -> dict[str, Any]:
|
|
520
|
+
"""Make POST Portal call."""
|
|
521
|
+
|
|
522
|
+
async with self._session2.post(url, headers=headers, data=data, timeout=self.timeout) as resp:
|
|
523
|
+
return await self._portal_response(resp)
|
|
524
|
+
|
|
525
|
+
async def _portal_get(self, url: str, headers: dict[str, Any], data: dict[str, Any]) -> dict[str, Any]:
|
|
526
|
+
"""Make GET Portal call."""
|
|
527
|
+
|
|
528
|
+
async with self._session2.get(url, headers=headers, data=data, timeout=self.timeout) as resp:
|
|
529
|
+
return await self._portal_response(resp)
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
async def _portal_response(resp: ClientResponse): # -> dict[str, Any]:
|
|
533
|
+
"""Return response from Portal call."""
|
|
534
|
+
|
|
535
|
+
if resp.status != 200:
|
|
536
|
+
error = await resp.text()
|
|
537
|
+
raise RedbackTechClientError(f'RedbackTech API Error Encountered. Status: {resp.status}; Error: {error}')
|
|
538
|
+
try:
|
|
539
|
+
response: dict[str, Any] = await resp.text()
|
|
540
|
+
except Exception as error:
|
|
541
|
+
raise RedbackTechClientError(f'Could not return text {error}') from error
|
|
542
|
+
|
|
543
|
+
return response
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Enum backports from standard lib."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
_StrEnumSelfT = TypeVar("_StrEnumSelfT", bound="StrEnum")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StrEnum(str, Enum):
|
|
11
|
+
"""Partial backport of Python 3.11's StrEnum."""
|
|
12
|
+
|
|
13
|
+
def __new__(
|
|
14
|
+
cls: type[_StrEnumSelfT], value: str, *args: Any, **kwargs: Any
|
|
15
|
+
) -> _StrEnumSelfT:
|
|
16
|
+
"""Create a new StrEnum instance."""
|
|
17
|
+
if not isinstance(value, str):
|
|
18
|
+
raise TypeError(f"{value!r} is not a string")
|
|
19
|
+
return super().__new__(cls, value, *args, **kwargs)
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
"""Return self.value."""
|
|
23
|
+
return str(self.value)
|