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.
@@ -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)