pylxpweb 0.1.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.
- pylxpweb/__init__.py +39 -0
- pylxpweb/client.py +417 -0
- pylxpweb/constants.py +1183 -0
- pylxpweb/endpoints/__init__.py +27 -0
- pylxpweb/endpoints/analytics.py +446 -0
- pylxpweb/endpoints/base.py +43 -0
- pylxpweb/endpoints/control.py +306 -0
- pylxpweb/endpoints/devices.py +250 -0
- pylxpweb/endpoints/export.py +86 -0
- pylxpweb/endpoints/firmware.py +235 -0
- pylxpweb/endpoints/forecasting.py +109 -0
- pylxpweb/endpoints/plants.py +470 -0
- pylxpweb/exceptions.py +23 -0
- pylxpweb/models.py +765 -0
- pylxpweb/py.typed +0 -0
- pylxpweb/registers.py +511 -0
- pylxpweb-0.1.0.dist-info/METADATA +433 -0
- pylxpweb-0.1.0.dist-info/RECORD +19 -0
- pylxpweb-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Plant/Station endpoints for the Luxpower API.
|
|
2
|
+
|
|
3
|
+
This module provides plant/station functionality including:
|
|
4
|
+
- Plant discovery and listing
|
|
5
|
+
- Plant configuration management
|
|
6
|
+
- Daylight Saving Time control
|
|
7
|
+
- Plant overview with real-time metrics
|
|
8
|
+
- Inverter overview across plants
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from pylxpweb.endpoints.base import BaseEndpoint
|
|
16
|
+
from pylxpweb.models import PlantListResponse
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pylxpweb.client import LuxpowerClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PlantEndpoints(BaseEndpoint):
|
|
23
|
+
"""Plant/Station endpoints for discovery, configuration, and overview."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, client: LuxpowerClient) -> None:
|
|
26
|
+
"""Initialize plant endpoints.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
client: The parent LuxpowerClient instance
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(client)
|
|
32
|
+
|
|
33
|
+
async def get_plants(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
sort: str = "createDate",
|
|
37
|
+
order: str = "desc",
|
|
38
|
+
search_text: str = "",
|
|
39
|
+
page: int = 1,
|
|
40
|
+
rows: int = 20,
|
|
41
|
+
) -> PlantListResponse:
|
|
42
|
+
"""Get list of available plants/stations.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
sort: Sort field (default: createDate)
|
|
46
|
+
order: Sort order (asc/desc, default: desc)
|
|
47
|
+
search_text: Search filter text
|
|
48
|
+
page: Page number for pagination
|
|
49
|
+
rows: Number of rows per page
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
PlantListResponse: List of plants with metadata
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
plants = await client.plants.get_plants()
|
|
56
|
+
for plant in plants.rows:
|
|
57
|
+
print(f"Plant: {plant.name}, ID: {plant.plantId}")
|
|
58
|
+
"""
|
|
59
|
+
await self.client._ensure_authenticated()
|
|
60
|
+
|
|
61
|
+
data = {
|
|
62
|
+
"sort": sort,
|
|
63
|
+
"order": order,
|
|
64
|
+
"searchText": search_text,
|
|
65
|
+
"page": page,
|
|
66
|
+
"rows": rows,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
response = await self.client._request(
|
|
70
|
+
"POST", "/WManage/web/config/plant/list/viewer", data=data
|
|
71
|
+
)
|
|
72
|
+
return PlantListResponse.model_validate(response)
|
|
73
|
+
|
|
74
|
+
async def get_plant_details(self, plant_id: int | str) -> dict[str, Any]:
|
|
75
|
+
"""Get detailed plant/station configuration information.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
plant_id: The plant/station ID
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict containing plant configuration including:
|
|
82
|
+
- plantId: Plant identifier
|
|
83
|
+
- name: Station name
|
|
84
|
+
- nominalPower: Solar PV power rating (W)
|
|
85
|
+
- timezone: Timezone string (e.g., "GMT -8")
|
|
86
|
+
- daylightSavingTime: DST enabled (boolean)
|
|
87
|
+
- continent: Continent enum value
|
|
88
|
+
- region: Region enum value
|
|
89
|
+
- country: Country enum value
|
|
90
|
+
- longitude: Geographic coordinate
|
|
91
|
+
- latitude: Geographic coordinate
|
|
92
|
+
- createDate: Plant creation date
|
|
93
|
+
- address: Physical address
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
LuxpowerAPIError: If plant not found or API error occurs
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
details = await client.plants.get_plant_details("12345")
|
|
100
|
+
print(f"Plant: {details['name']}")
|
|
101
|
+
print(f"Timezone: {details['timezone']}")
|
|
102
|
+
print(f"DST Enabled: {details['daylightSavingTime']}")
|
|
103
|
+
"""
|
|
104
|
+
await self.client._ensure_authenticated()
|
|
105
|
+
|
|
106
|
+
data = {
|
|
107
|
+
"page": 1,
|
|
108
|
+
"rows": 20,
|
|
109
|
+
"searchText": "",
|
|
110
|
+
"targetPlantId": str(plant_id),
|
|
111
|
+
"sort": "createDate",
|
|
112
|
+
"order": "desc",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
response = await self.client._request(
|
|
116
|
+
"POST", "/WManage/web/config/plant/list/viewer", data=data
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if isinstance(response, dict) and response.get("rows"):
|
|
120
|
+
from logging import getLogger
|
|
121
|
+
|
|
122
|
+
plant_data = response["rows"][0]
|
|
123
|
+
getLogger(__name__).debug(
|
|
124
|
+
"Retrieved plant details for plant %s: %s",
|
|
125
|
+
plant_id,
|
|
126
|
+
plant_data.get("name"),
|
|
127
|
+
)
|
|
128
|
+
return dict(plant_data)
|
|
129
|
+
|
|
130
|
+
from pylxpweb.exceptions import LuxpowerAPIError
|
|
131
|
+
|
|
132
|
+
raise LuxpowerAPIError(f"Plant {plant_id} not found in API response")
|
|
133
|
+
|
|
134
|
+
async def _fetch_country_location_from_api(self, country_human: str) -> tuple[str, str]:
|
|
135
|
+
"""Fetch continent and region for a country from locale API.
|
|
136
|
+
|
|
137
|
+
This is the fallback method when country is not in static mapping.
|
|
138
|
+
Queries the locale API to discover the continent and region dynamically.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
country_human: Human-readable country name from API
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Tuple of (continent_enum, region_enum)
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
LuxpowerAPIError: If country cannot be found in locale API
|
|
148
|
+
"""
|
|
149
|
+
import json
|
|
150
|
+
from logging import getLogger
|
|
151
|
+
|
|
152
|
+
_LOGGER = getLogger(__name__)
|
|
153
|
+
_LOGGER.info(
|
|
154
|
+
"Country '%s' not in static mapping, fetching from locale API",
|
|
155
|
+
country_human,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Get all continents from constants
|
|
159
|
+
from pylxpweb.constants import CONTINENT_MAP
|
|
160
|
+
|
|
161
|
+
session = await self.client._get_session()
|
|
162
|
+
|
|
163
|
+
# Search through all continents and regions
|
|
164
|
+
for continent_enum in CONTINENT_MAP.values():
|
|
165
|
+
# Get regions for this continent
|
|
166
|
+
async with session.post(
|
|
167
|
+
f"{self.client.base_url}/WManage/locale/region",
|
|
168
|
+
data=f"continent={continent_enum}",
|
|
169
|
+
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
|
170
|
+
) as resp:
|
|
171
|
+
if resp.status != 200:
|
|
172
|
+
continue
|
|
173
|
+
regions_text = await resp.text()
|
|
174
|
+
regions = json.loads(regions_text)
|
|
175
|
+
|
|
176
|
+
# Check each region for our country
|
|
177
|
+
for region in regions:
|
|
178
|
+
region_value = region["value"]
|
|
179
|
+
|
|
180
|
+
async with session.post(
|
|
181
|
+
f"{self.client.base_url}/WManage/locale/country",
|
|
182
|
+
data=f"region={region_value}",
|
|
183
|
+
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
|
184
|
+
) as resp:
|
|
185
|
+
if resp.status != 200:
|
|
186
|
+
continue
|
|
187
|
+
countries_text = await resp.text()
|
|
188
|
+
countries = json.loads(countries_text)
|
|
189
|
+
|
|
190
|
+
# Check if our country is in this region
|
|
191
|
+
for country in countries:
|
|
192
|
+
if country["text"] == country_human:
|
|
193
|
+
_LOGGER.info(
|
|
194
|
+
"Found country '%s' in locale API: continent=%s, region=%s",
|
|
195
|
+
country_human,
|
|
196
|
+
continent_enum,
|
|
197
|
+
region_value,
|
|
198
|
+
)
|
|
199
|
+
return (continent_enum, region_value)
|
|
200
|
+
|
|
201
|
+
# Country not found anywhere
|
|
202
|
+
from pylxpweb.exceptions import LuxpowerAPIError
|
|
203
|
+
|
|
204
|
+
raise LuxpowerAPIError(
|
|
205
|
+
f"Country '{country_human}' not found in locale API. "
|
|
206
|
+
"This country may not be supported by the Luxpower platform."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def _prepare_plant_update_data(
|
|
210
|
+
self, plant_details: dict[str, Any], **overrides: Any
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
"""Prepare data for plant configuration update POST request.
|
|
213
|
+
|
|
214
|
+
Converts API response values to the enum format required by the POST endpoint.
|
|
215
|
+
Uses hybrid approach: static mapping for common countries (fast), dynamic
|
|
216
|
+
fetching from locale API for unknown countries (comprehensive).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
plant_details: Plant details from get_plant_details()
|
|
220
|
+
**overrides: Fields to override (e.g., daylightSavingTime=True)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dictionary ready for POST to /WManage/web/config/plant/edit
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If unable to map required fields
|
|
227
|
+
LuxpowerAPIError: If dynamic fetch fails
|
|
228
|
+
"""
|
|
229
|
+
from logging import getLogger
|
|
230
|
+
|
|
231
|
+
from pylxpweb.constants import (
|
|
232
|
+
COUNTRY_MAP,
|
|
233
|
+
TIMEZONE_MAP,
|
|
234
|
+
get_continent_region_from_country,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
_LOGGER = getLogger(__name__)
|
|
238
|
+
|
|
239
|
+
# Required fields for POST
|
|
240
|
+
data: dict[str, Any] = {
|
|
241
|
+
"plantId": str(plant_details["plantId"]),
|
|
242
|
+
"name": plant_details["name"],
|
|
243
|
+
"createDate": plant_details["createDate"],
|
|
244
|
+
"daylightSavingTime": plant_details["daylightSavingTime"],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Map timezone: "GMT -8" → "WEST8"
|
|
248
|
+
timezone_human = plant_details["timezone"]
|
|
249
|
+
if timezone_human not in TIMEZONE_MAP:
|
|
250
|
+
raise ValueError(f"Unknown timezone: {timezone_human}")
|
|
251
|
+
data["timezone"] = TIMEZONE_MAP[timezone_human]
|
|
252
|
+
|
|
253
|
+
# Map country: "United States of America" → "UNITED_STATES_OF_AMERICA"
|
|
254
|
+
country_human = plant_details["country"]
|
|
255
|
+
if country_human not in COUNTRY_MAP:
|
|
256
|
+
raise ValueError(f"Unknown country: {country_human}")
|
|
257
|
+
data["country"] = COUNTRY_MAP[country_human]
|
|
258
|
+
|
|
259
|
+
# Hybrid approach: Try static mapping first, fall back to dynamic fetch
|
|
260
|
+
try:
|
|
261
|
+
# Fast path: static mapping
|
|
262
|
+
continent_enum, region_enum = get_continent_region_from_country(country_human)
|
|
263
|
+
_LOGGER.debug(
|
|
264
|
+
"Used static mapping for country '%s': %s/%s",
|
|
265
|
+
country_human,
|
|
266
|
+
continent_enum,
|
|
267
|
+
region_enum,
|
|
268
|
+
)
|
|
269
|
+
except ValueError:
|
|
270
|
+
# Slow path: dynamic fetch from locale API
|
|
271
|
+
_LOGGER.info(
|
|
272
|
+
"Country '%s' not in static mapping, fetching from locale API",
|
|
273
|
+
country_human,
|
|
274
|
+
)
|
|
275
|
+
continent_enum, region_enum = await self._fetch_country_location_from_api(country_human)
|
|
276
|
+
|
|
277
|
+
data["continent"] = continent_enum
|
|
278
|
+
data["region"] = region_enum
|
|
279
|
+
|
|
280
|
+
# Include nominalPower if present and not blank
|
|
281
|
+
if plant_details.get("nominalPower"):
|
|
282
|
+
data["nominalPower"] = plant_details["nominalPower"]
|
|
283
|
+
|
|
284
|
+
# Apply any overrides
|
|
285
|
+
data.update(overrides)
|
|
286
|
+
|
|
287
|
+
_LOGGER.info(
|
|
288
|
+
"Prepared plant update data for plant %s: timezone=%s, country=%s, "
|
|
289
|
+
"continent=%s, region=%s, dst=%s",
|
|
290
|
+
plant_details["plantId"],
|
|
291
|
+
data["timezone"],
|
|
292
|
+
data["country"],
|
|
293
|
+
data["continent"],
|
|
294
|
+
data["region"],
|
|
295
|
+
data["daylightSavingTime"],
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return data
|
|
299
|
+
|
|
300
|
+
async def update_plant_config(self, plant_id: int | str, **kwargs: Any) -> dict[str, Any]:
|
|
301
|
+
"""Update plant/station configuration.
|
|
302
|
+
|
|
303
|
+
Uses API-only data with mapping tables to convert human-readable values
|
|
304
|
+
to the enum format required by the POST endpoint. No HTML parsing needed.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
plant_id: The plant/station ID
|
|
308
|
+
**kwargs: Configuration parameters to update:
|
|
309
|
+
- name (str): Station name
|
|
310
|
+
- nominalPower (int): Solar PV power rating in Watts
|
|
311
|
+
- daylightSavingTime (bool): DST enabled
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dict containing API response (success status and message)
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
LuxpowerAPIError: If update fails or validation error occurs
|
|
318
|
+
ValueError: If unable to map timezone or country values
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
# Toggle DST
|
|
322
|
+
await client.plants.update_plant_config(
|
|
323
|
+
"12345",
|
|
324
|
+
daylightSavingTime=True
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Update power rating
|
|
328
|
+
await client.plants.update_plant_config(
|
|
329
|
+
"12345",
|
|
330
|
+
nominalPower=20000
|
|
331
|
+
)
|
|
332
|
+
"""
|
|
333
|
+
from logging import getLogger
|
|
334
|
+
|
|
335
|
+
_LOGGER = getLogger(__name__)
|
|
336
|
+
await self.client._ensure_authenticated()
|
|
337
|
+
|
|
338
|
+
# Get current configuration from API (human-readable values)
|
|
339
|
+
_LOGGER.info("Fetching plant details for plant %s", plant_id)
|
|
340
|
+
plant_details = await self.get_plant_details(plant_id)
|
|
341
|
+
|
|
342
|
+
# Prepare POST data using hybrid approach (static + dynamic mapping)
|
|
343
|
+
data = await self._prepare_plant_update_data(plant_details, **kwargs)
|
|
344
|
+
|
|
345
|
+
_LOGGER.info(
|
|
346
|
+
"Updating plant %s configuration: %s",
|
|
347
|
+
plant_id,
|
|
348
|
+
dict(kwargs),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
response = await self.client._request("POST", "/WManage/web/config/plant/edit", data=data)
|
|
352
|
+
|
|
353
|
+
_LOGGER.info("Plant %s configuration updated successfully", plant_id)
|
|
354
|
+
return response
|
|
355
|
+
|
|
356
|
+
async def set_daylight_saving_time(self, plant_id: int | str, enabled: bool) -> dict[str, Any]:
|
|
357
|
+
"""Set Daylight Saving Time (DST) for a plant/station.
|
|
358
|
+
|
|
359
|
+
Convenience method for toggling DST without affecting other settings.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
plant_id: The plant/station ID
|
|
363
|
+
enabled: True to enable DST, False to disable
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Dict containing API response
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
LuxpowerAPIError: If update fails
|
|
370
|
+
|
|
371
|
+
Example:
|
|
372
|
+
# Enable DST
|
|
373
|
+
await client.plants.set_daylight_saving_time("12345", True)
|
|
374
|
+
|
|
375
|
+
# Disable DST
|
|
376
|
+
await client.plants.set_daylight_saving_time("12345", False)
|
|
377
|
+
"""
|
|
378
|
+
from logging import getLogger
|
|
379
|
+
|
|
380
|
+
_LOGGER = getLogger(__name__)
|
|
381
|
+
_LOGGER.info(
|
|
382
|
+
"Setting Daylight Saving Time to %s for plant %s",
|
|
383
|
+
"enabled" if enabled else "disabled",
|
|
384
|
+
plant_id,
|
|
385
|
+
)
|
|
386
|
+
return await self.update_plant_config(plant_id, daylightSavingTime=enabled)
|
|
387
|
+
|
|
388
|
+
async def get_plant_overview(self, search_text: str = "") -> dict[str, Any]:
|
|
389
|
+
"""Get plant overview with real-time metrics.
|
|
390
|
+
|
|
391
|
+
This endpoint provides plant-level aggregated data including:
|
|
392
|
+
- Real-time power metrics (PV, charge, discharge, consumption)
|
|
393
|
+
- Energy totals (today, total)
|
|
394
|
+
- Inverter details nested within plant data
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
search_text: Optional search filter for plant name/address
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Dict containing:
|
|
401
|
+
- total: Total number of plants matching filter
|
|
402
|
+
- rows: List of plant objects with real-time metrics
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
overview = await client.plants.get_plant_overview()
|
|
406
|
+
for plant in overview["rows"]:
|
|
407
|
+
print(f"{plant['name']}: {plant['ppv']}W PV")
|
|
408
|
+
"""
|
|
409
|
+
data = {"searchText": search_text}
|
|
410
|
+
|
|
411
|
+
response = await self.client._request(
|
|
412
|
+
"POST",
|
|
413
|
+
"/WManage/api/plantOverview/list/viewer",
|
|
414
|
+
data=data,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return dict(response)
|
|
418
|
+
|
|
419
|
+
async def get_inverter_overview(
|
|
420
|
+
self,
|
|
421
|
+
page: int = 1,
|
|
422
|
+
rows: int = 30,
|
|
423
|
+
plant_id: int = -1,
|
|
424
|
+
search_text: str = "",
|
|
425
|
+
status_filter: str = "all",
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""Get paginated list of all inverters with real-time metrics.
|
|
428
|
+
|
|
429
|
+
This endpoint provides per-inverter data across all plants or filtered
|
|
430
|
+
by specific plant. Unlike get_plant_overview which aggregates at plant
|
|
431
|
+
level, this shows individual inverter metrics.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
page: Page number (1-indexed)
|
|
435
|
+
rows: Number of rows per page (default 30)
|
|
436
|
+
plant_id: Plant ID (-1 for all plants, or specific plant ID)
|
|
437
|
+
search_text: Search filter for serial number or device name
|
|
438
|
+
status_filter: Status filter ("all", "normal", "fault", "offline")
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Dict containing:
|
|
442
|
+
- success: Boolean indicating success
|
|
443
|
+
- total: Total number of inverters matching filter
|
|
444
|
+
- rows: List of inverter objects with metrics
|
|
445
|
+
|
|
446
|
+
Example:
|
|
447
|
+
# All inverters across all plants
|
|
448
|
+
overview = await client.plants.get_inverter_overview()
|
|
449
|
+
|
|
450
|
+
# Inverters for specific plant
|
|
451
|
+
overview = await client.plants.get_inverter_overview(plant_id=19147)
|
|
452
|
+
|
|
453
|
+
# Only faulted inverters
|
|
454
|
+
overview = await client.plants.get_inverter_overview(status_filter="fault")
|
|
455
|
+
"""
|
|
456
|
+
data = {
|
|
457
|
+
"page": page,
|
|
458
|
+
"rows": rows,
|
|
459
|
+
"plantId": plant_id,
|
|
460
|
+
"searchText": search_text,
|
|
461
|
+
"statusText": status_filter,
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
response = await self.client._request(
|
|
465
|
+
"POST",
|
|
466
|
+
"/WManage/api/inverterOverview/list",
|
|
467
|
+
data=data,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return dict(response)
|
pylxpweb/exceptions.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Exceptions for Luxpower/EG4 API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LuxpowerError(Exception):
|
|
7
|
+
"""Base exception for all Luxpower errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LuxpowerAuthError(LuxpowerError):
|
|
11
|
+
"""Raised when authentication fails."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LuxpowerConnectionError(LuxpowerError):
|
|
15
|
+
"""Raised when connection to the API fails."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LuxpowerAPIError(LuxpowerError):
|
|
19
|
+
"""Raised when the API returns an error response."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LuxpowerDeviceError(LuxpowerError):
|
|
23
|
+
"""Raised when there's an issue with device operations."""
|