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,306 @@
|
|
|
1
|
+
"""Device control endpoints for the Luxpower API.
|
|
2
|
+
|
|
3
|
+
This module provides device control functionality including:
|
|
4
|
+
- Parameter reading and writing
|
|
5
|
+
- Function enable/disable control
|
|
6
|
+
- Quick charge operations
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from pylxpweb.endpoints.base import BaseEndpoint
|
|
14
|
+
from pylxpweb.models import (
|
|
15
|
+
ParameterReadResponse,
|
|
16
|
+
QuickChargeStatus,
|
|
17
|
+
SuccessResponse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pylxpweb.client import LuxpowerClient
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ControlEndpoints(BaseEndpoint):
|
|
25
|
+
"""Device control endpoints for parameters, functions, and quick charge."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: LuxpowerClient) -> None:
|
|
28
|
+
"""Initialize control endpoints.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
client: The parent LuxpowerClient instance
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(client)
|
|
34
|
+
|
|
35
|
+
async def read_parameters(
|
|
36
|
+
self,
|
|
37
|
+
inverter_sn: str,
|
|
38
|
+
start_register: int = 0,
|
|
39
|
+
point_number: int = 127,
|
|
40
|
+
auto_retry: bool = True,
|
|
41
|
+
) -> ParameterReadResponse:
|
|
42
|
+
"""Read configuration parameters from inverter registers.
|
|
43
|
+
|
|
44
|
+
IMPORTANT: The API returns parameters as FLAT key-value pairs with
|
|
45
|
+
descriptive names (like "HOLD_AC_CHARGE_POWER_CMD"), NOT nested under
|
|
46
|
+
a 'parameters' field or using register numbers as keys!
|
|
47
|
+
|
|
48
|
+
Common register ranges (web interface strategy):
|
|
49
|
+
- 0-126 (startRegister=0, pointNumber=127) - System config, grid protection
|
|
50
|
+
- 127-253 (startRegister=127, pointNumber=127) - Additional config
|
|
51
|
+
- 240-366 (startRegister=240, pointNumber=127) - Extended parameters
|
|
52
|
+
|
|
53
|
+
Critical registers (verified on 18KPV):
|
|
54
|
+
- Register 21: Function enable bit field (27 bits including AC charge, EPS, standby)
|
|
55
|
+
- Register 66: AC charge power command (HOLD_AC_CHARGE_POWER_CMD)
|
|
56
|
+
- Register 67: AC charge SOC limit (HOLD_AC_CHARGE_SOC_LIMIT)
|
|
57
|
+
- Register 70: AC charge schedule start (hour + minute)
|
|
58
|
+
- Register 100: Battery discharge cutoff voltage
|
|
59
|
+
- Register 110: System function bit field (14 bits including microgrid, eco mode)
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> response = await client.control.read_parameters("1234567890", 66, 8)
|
|
63
|
+
>>> # Access parameters directly from response
|
|
64
|
+
>>> response.parameters["HOLD_AC_CHARGE_POWER_CMD"]
|
|
65
|
+
50
|
|
66
|
+
>>> response.parameters["HOLD_AC_CHARGE_SOC_LIMIT"]
|
|
67
|
+
100
|
|
68
|
+
>>> # Or access via model dump (includes all parameter keys at root level)
|
|
69
|
+
>>> data = response.model_dump()
|
|
70
|
+
>>> data["HOLD_AC_CHARGE_POWER_CMD"]
|
|
71
|
+
50
|
|
72
|
+
|
|
73
|
+
>>> # Read function enable register (27 bit fields)
|
|
74
|
+
>>> response = await client.control.read_parameters("1234567890", 21, 1)
|
|
75
|
+
>>> response.parameters["FUNC_AC_CHARGE"]
|
|
76
|
+
True
|
|
77
|
+
>>> response.parameters["FUNC_SET_TO_STANDBY"]
|
|
78
|
+
False
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
inverter_sn: Inverter serial number (e.g., "1234567890")
|
|
82
|
+
start_register: Starting register address
|
|
83
|
+
point_number: Number of registers to read (max 127 in practice)
|
|
84
|
+
auto_retry: Enable automatic retry on failure
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
ParameterReadResponse: Contains inverterSn, deviceType, startRegister,
|
|
88
|
+
pointNumber, and all parameter keys as flat attributes.
|
|
89
|
+
Use .parameters property to get dict of parameter keys.
|
|
90
|
+
|
|
91
|
+
See Also:
|
|
92
|
+
- constants.REGISTER_TO_PARAM_KEYS_18KPV: Verified register→parameter mappings
|
|
93
|
+
- research/REGISTER_NUMBER_MAPPING.md: Complete register documentation
|
|
94
|
+
"""
|
|
95
|
+
await self.client._ensure_authenticated()
|
|
96
|
+
|
|
97
|
+
data = {
|
|
98
|
+
"inverterSn": inverter_sn,
|
|
99
|
+
"startRegister": start_register,
|
|
100
|
+
"pointNumber": point_number,
|
|
101
|
+
"autoRetry": auto_retry,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cache_key = self._get_cache_key(
|
|
105
|
+
"params", sn=inverter_sn, start=start_register, count=point_number
|
|
106
|
+
)
|
|
107
|
+
response = await self.client._request(
|
|
108
|
+
"POST",
|
|
109
|
+
"/WManage/web/maintain/remoteRead/read",
|
|
110
|
+
data=data,
|
|
111
|
+
cache_key=cache_key,
|
|
112
|
+
cache_endpoint="parameter_read",
|
|
113
|
+
)
|
|
114
|
+
return ParameterReadResponse.model_validate(response)
|
|
115
|
+
|
|
116
|
+
async def write_parameter(
|
|
117
|
+
self,
|
|
118
|
+
inverter_sn: str,
|
|
119
|
+
hold_param: str,
|
|
120
|
+
value_text: str,
|
|
121
|
+
client_type: str = "WEB",
|
|
122
|
+
remote_set_type: str = "NORMAL",
|
|
123
|
+
) -> SuccessResponse:
|
|
124
|
+
"""Write a configuration parameter to the inverter.
|
|
125
|
+
|
|
126
|
+
WARNING: This changes device configuration!
|
|
127
|
+
|
|
128
|
+
Common parameters:
|
|
129
|
+
- HOLD_SYSTEM_CHARGE_SOC_LIMIT: Battery charge limit (%)
|
|
130
|
+
- HOLD_SYSTEM_DISCHARGE_SOC_LIMIT: Battery discharge limit (%)
|
|
131
|
+
- HOLD_AC_CHARGE_POWER: AC charge power limit (W)
|
|
132
|
+
- HOLD_AC_DISCHARGE_POWER: AC discharge power limit (W)
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
inverter_sn: Inverter serial number
|
|
136
|
+
hold_param: Parameter name to write
|
|
137
|
+
value_text: Value to write (as string)
|
|
138
|
+
client_type: Client type (WEB/APP)
|
|
139
|
+
remote_set_type: Set type (NORMAL/QUICK)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
SuccessResponse: Operation result
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
# Set battery charge limit to 90%
|
|
146
|
+
await client.control.write_parameter(
|
|
147
|
+
"1234567890",
|
|
148
|
+
"HOLD_SYSTEM_CHARGE_SOC_LIMIT",
|
|
149
|
+
"90"
|
|
150
|
+
)
|
|
151
|
+
"""
|
|
152
|
+
await self.client._ensure_authenticated()
|
|
153
|
+
|
|
154
|
+
data = {
|
|
155
|
+
"inverterSn": inverter_sn,
|
|
156
|
+
"holdParam": hold_param,
|
|
157
|
+
"valueText": value_text,
|
|
158
|
+
"clientType": client_type,
|
|
159
|
+
"remoteSetType": remote_set_type,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
response = await self.client._request(
|
|
163
|
+
"POST", "/WManage/web/maintain/remoteSet/write", data=data
|
|
164
|
+
)
|
|
165
|
+
return SuccessResponse.model_validate(response)
|
|
166
|
+
|
|
167
|
+
async def control_function(
|
|
168
|
+
self,
|
|
169
|
+
inverter_sn: str,
|
|
170
|
+
function_param: str,
|
|
171
|
+
enable: bool,
|
|
172
|
+
client_type: str = "WEB",
|
|
173
|
+
remote_set_type: str = "NORMAL",
|
|
174
|
+
) -> SuccessResponse:
|
|
175
|
+
"""Enable or disable a device function.
|
|
176
|
+
|
|
177
|
+
WARNING: This changes device state!
|
|
178
|
+
|
|
179
|
+
Common functions:
|
|
180
|
+
- FUNC_EPS_EN: Battery backup (EPS) mode
|
|
181
|
+
- FUNC_SET_TO_STANDBY: Standby mode
|
|
182
|
+
- FUNC_GRID_PEAK_SHAVING: Peak shaving mode
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
inverter_sn: Inverter serial number
|
|
186
|
+
function_param: Function parameter name
|
|
187
|
+
enable: Enable or disable the function
|
|
188
|
+
client_type: Client type (WEB/APP)
|
|
189
|
+
remote_set_type: Set type (NORMAL/QUICK)
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
SuccessResponse: Operation result
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
# Enable EPS mode
|
|
196
|
+
await client.control.control_function(
|
|
197
|
+
"1234567890",
|
|
198
|
+
"FUNC_EPS_EN",
|
|
199
|
+
True
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Disable standby mode
|
|
203
|
+
await client.control.control_function(
|
|
204
|
+
"1234567890",
|
|
205
|
+
"FUNC_SET_TO_STANDBY",
|
|
206
|
+
False
|
|
207
|
+
)
|
|
208
|
+
"""
|
|
209
|
+
await self.client._ensure_authenticated()
|
|
210
|
+
|
|
211
|
+
data = {
|
|
212
|
+
"inverterSn": inverter_sn,
|
|
213
|
+
"functionParam": function_param,
|
|
214
|
+
"enable": "true" if enable else "false",
|
|
215
|
+
"clientType": client_type,
|
|
216
|
+
"remoteSetType": remote_set_type,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
response = await self.client._request(
|
|
220
|
+
"POST", "/WManage/web/maintain/remoteSet/functionControl", data=data
|
|
221
|
+
)
|
|
222
|
+
return SuccessResponse.model_validate(response)
|
|
223
|
+
|
|
224
|
+
async def start_quick_charge(
|
|
225
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
226
|
+
) -> SuccessResponse:
|
|
227
|
+
"""Start quick charge operation.
|
|
228
|
+
|
|
229
|
+
WARNING: This starts charging!
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
inverter_sn: Inverter serial number
|
|
233
|
+
client_type: Client type (WEB/APP)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
SuccessResponse: Operation result
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
result = await client.control.start_quick_charge("1234567890")
|
|
240
|
+
if result.success:
|
|
241
|
+
print("Quick charge started successfully")
|
|
242
|
+
"""
|
|
243
|
+
await self.client._ensure_authenticated()
|
|
244
|
+
|
|
245
|
+
data = {"inverterSn": inverter_sn, "clientType": client_type}
|
|
246
|
+
|
|
247
|
+
response = await self.client._request(
|
|
248
|
+
"POST", "/WManage/web/config/quickCharge/start", data=data
|
|
249
|
+
)
|
|
250
|
+
return SuccessResponse.model_validate(response)
|
|
251
|
+
|
|
252
|
+
async def stop_quick_charge(
|
|
253
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
254
|
+
) -> SuccessResponse:
|
|
255
|
+
"""Stop quick charge operation.
|
|
256
|
+
|
|
257
|
+
WARNING: This stops charging!
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
inverter_sn: Inverter serial number
|
|
261
|
+
client_type: Client type (WEB/APP)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
SuccessResponse: Operation result
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
result = await client.control.stop_quick_charge("1234567890")
|
|
268
|
+
if result.success:
|
|
269
|
+
print("Quick charge stopped successfully")
|
|
270
|
+
"""
|
|
271
|
+
await self.client._ensure_authenticated()
|
|
272
|
+
|
|
273
|
+
data = {"inverterSn": inverter_sn, "clientType": client_type}
|
|
274
|
+
|
|
275
|
+
response = await self.client._request(
|
|
276
|
+
"POST", "/WManage/web/config/quickCharge/stop", data=data
|
|
277
|
+
)
|
|
278
|
+
return SuccessResponse.model_validate(response)
|
|
279
|
+
|
|
280
|
+
async def get_quick_charge_status(self, inverter_sn: str) -> QuickChargeStatus:
|
|
281
|
+
"""Get current quick charge operation status.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
inverter_sn: Inverter serial number
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
QuickChargeStatus: Quick charge status
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
status = await client.control.get_quick_charge_status("1234567890")
|
|
291
|
+
if status.is_charging:
|
|
292
|
+
print("Quick charge is active")
|
|
293
|
+
"""
|
|
294
|
+
await self.client._ensure_authenticated()
|
|
295
|
+
|
|
296
|
+
data = {"inverterSn": inverter_sn}
|
|
297
|
+
|
|
298
|
+
cache_key = self._get_cache_key("quick_charge", serialNum=inverter_sn)
|
|
299
|
+
response = await self.client._request(
|
|
300
|
+
"POST",
|
|
301
|
+
"/WManage/web/config/quickCharge/getStatusInfo",
|
|
302
|
+
data=data,
|
|
303
|
+
cache_key=cache_key,
|
|
304
|
+
cache_endpoint="quick_charge_status",
|
|
305
|
+
)
|
|
306
|
+
return QuickChargeStatus.model_validate(response)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Device endpoints for the Luxpower API.
|
|
2
|
+
|
|
3
|
+
This module provides device functionality including:
|
|
4
|
+
- Device discovery and hierarchy
|
|
5
|
+
- Real-time runtime data
|
|
6
|
+
- Energy statistics
|
|
7
|
+
- Battery information
|
|
8
|
+
- GridBOSS/MID device data
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from pylxpweb.endpoints.base import BaseEndpoint
|
|
16
|
+
from pylxpweb.models import (
|
|
17
|
+
BatteryInfo,
|
|
18
|
+
EnergyInfo,
|
|
19
|
+
InverterListResponse,
|
|
20
|
+
InverterRuntime,
|
|
21
|
+
MidboxRuntime,
|
|
22
|
+
ParallelGroupDetailsResponse,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from pylxpweb.client import LuxpowerClient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DeviceEndpoints(BaseEndpoint):
|
|
30
|
+
"""Device endpoints for discovery, runtime data, and device information."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, client: LuxpowerClient) -> None:
|
|
33
|
+
"""Initialize device endpoints.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
client: The parent LuxpowerClient instance
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(client)
|
|
39
|
+
|
|
40
|
+
async def get_parallel_group_details(self, plant_id: int) -> ParallelGroupDetailsResponse:
|
|
41
|
+
"""Get parallel group device hierarchy for a plant.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
plant_id: Plant/station ID
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ParallelGroupDetailsResponse: Parallel group structure
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
groups = await client.devices.get_parallel_group_details(12345)
|
|
51
|
+
for group in groups.rows:
|
|
52
|
+
print(f"Group ID: {group.groupId}")
|
|
53
|
+
print(f"Inverters: {len(group.inverters)}")
|
|
54
|
+
"""
|
|
55
|
+
await self.client._ensure_authenticated()
|
|
56
|
+
|
|
57
|
+
data = {"plantId": plant_id}
|
|
58
|
+
|
|
59
|
+
cache_key = self._get_cache_key("parallel_groups", plantId=plant_id)
|
|
60
|
+
response = await self.client._request(
|
|
61
|
+
"POST",
|
|
62
|
+
"/WManage/api/inverterOverview/getParallelGroupDetails",
|
|
63
|
+
data=data,
|
|
64
|
+
cache_key=cache_key,
|
|
65
|
+
cache_endpoint="device_discovery",
|
|
66
|
+
)
|
|
67
|
+
return ParallelGroupDetailsResponse.model_validate(response)
|
|
68
|
+
|
|
69
|
+
async def get_devices(self, plant_id: int) -> InverterListResponse:
|
|
70
|
+
"""Get list of all devices in a plant.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
plant_id: Plant/station ID
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
InverterListResponse: List of inverters and devices
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
devices = await client.devices.get_devices(12345)
|
|
80
|
+
for device in devices.rows:
|
|
81
|
+
print(f"Device: {device.serialNum} - {device.alias}")
|
|
82
|
+
"""
|
|
83
|
+
await self.client._ensure_authenticated()
|
|
84
|
+
|
|
85
|
+
data = {"plantId": plant_id}
|
|
86
|
+
|
|
87
|
+
cache_key = self._get_cache_key("devices", plantId=plant_id)
|
|
88
|
+
response = await self.client._request(
|
|
89
|
+
"POST",
|
|
90
|
+
"/WManage/api/inverterOverview/list",
|
|
91
|
+
data=data,
|
|
92
|
+
cache_key=cache_key,
|
|
93
|
+
cache_endpoint="device_discovery",
|
|
94
|
+
)
|
|
95
|
+
return InverterListResponse.model_validate(response)
|
|
96
|
+
|
|
97
|
+
async def get_inverter_runtime(self, serial_num: str) -> InverterRuntime:
|
|
98
|
+
"""Get real-time runtime data for an inverter.
|
|
99
|
+
|
|
100
|
+
Note: Many values require scaling:
|
|
101
|
+
- Voltage: divide by 100
|
|
102
|
+
- Current: divide by 100
|
|
103
|
+
- Frequency: divide by 100
|
|
104
|
+
- Power: no scaling (direct watts)
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
serial_num: 10-digit device serial number
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
InverterRuntime: Real-time inverter metrics
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
runtime = await client.devices.get_inverter_runtime("1234567890")
|
|
114
|
+
print(f"PV Power: {runtime.ppv}W")
|
|
115
|
+
print(f"Battery SOC: {runtime.soc}%")
|
|
116
|
+
print(f"Grid Voltage: {runtime.vacr / 100}V")
|
|
117
|
+
"""
|
|
118
|
+
await self.client._ensure_authenticated()
|
|
119
|
+
|
|
120
|
+
data = {"serialNum": serial_num}
|
|
121
|
+
|
|
122
|
+
cache_key = self._get_cache_key("runtime", serialNum=serial_num)
|
|
123
|
+
response = await self.client._request(
|
|
124
|
+
"POST",
|
|
125
|
+
"/WManage/api/inverter/getInverterRuntime",
|
|
126
|
+
data=data,
|
|
127
|
+
cache_key=cache_key,
|
|
128
|
+
cache_endpoint="inverter_runtime",
|
|
129
|
+
)
|
|
130
|
+
return InverterRuntime.model_validate(response)
|
|
131
|
+
|
|
132
|
+
async def get_inverter_energy(self, serial_num: str) -> EnergyInfo:
|
|
133
|
+
"""Get energy statistics for an inverter.
|
|
134
|
+
|
|
135
|
+
All energy values are in Wh (divide by 1000 for kWh).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
serial_num: 10-digit device serial number
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
EnergyInfo: Energy production and consumption statistics
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
energy = await client.devices.get_inverter_energy("1234567890")
|
|
145
|
+
print(f"Today's Production: {energy.eInvDay / 1000}kWh")
|
|
146
|
+
print(f"Total Production: {energy.eInvAll / 1000}kWh")
|
|
147
|
+
"""
|
|
148
|
+
await self.client._ensure_authenticated()
|
|
149
|
+
|
|
150
|
+
data = {"serialNum": serial_num}
|
|
151
|
+
|
|
152
|
+
cache_key = self._get_cache_key("energy", serialNum=serial_num)
|
|
153
|
+
response = await self.client._request(
|
|
154
|
+
"POST",
|
|
155
|
+
"/WManage/api/inverter/getInverterEnergyInfo",
|
|
156
|
+
data=data,
|
|
157
|
+
cache_key=cache_key,
|
|
158
|
+
cache_endpoint="inverter_energy",
|
|
159
|
+
)
|
|
160
|
+
return EnergyInfo.model_validate(response)
|
|
161
|
+
|
|
162
|
+
async def get_parallel_energy(self, serial_num: str) -> EnergyInfo:
|
|
163
|
+
"""Get aggregate energy statistics for entire parallel group.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
serial_num: Serial number of any inverter in the parallel group
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
EnergyInfo: Aggregate energy statistics for the group
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
energy = await client.devices.get_parallel_energy("1234567890")
|
|
173
|
+
print(f"Group Total Today: {energy.eInvDay / 1000}kWh")
|
|
174
|
+
"""
|
|
175
|
+
await self.client._ensure_authenticated()
|
|
176
|
+
|
|
177
|
+
data = {"serialNum": serial_num}
|
|
178
|
+
|
|
179
|
+
cache_key = self._get_cache_key("parallel_energy", serialNum=serial_num)
|
|
180
|
+
response = await self.client._request(
|
|
181
|
+
"POST",
|
|
182
|
+
"/WManage/api/inverter/getInverterEnergyInfoParallel",
|
|
183
|
+
data=data,
|
|
184
|
+
cache_key=cache_key,
|
|
185
|
+
cache_endpoint="inverter_energy",
|
|
186
|
+
)
|
|
187
|
+
return EnergyInfo.model_validate(response)
|
|
188
|
+
|
|
189
|
+
async def get_battery_info(self, serial_num: str) -> BatteryInfo:
|
|
190
|
+
"""Get battery information including individual modules.
|
|
191
|
+
|
|
192
|
+
Note: Cell voltages are in millivolts (divide by 1000 for volts).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
serial_num: Inverter serial number
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
BatteryInfo: Battery status and individual module data
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
battery = await client.devices.get_battery_info("1234567890")
|
|
202
|
+
print(f"Battery SOC: {battery.soc}%")
|
|
203
|
+
print(f"Number of Modules: {len(battery.batteryArray)}")
|
|
204
|
+
for module in battery.batteryArray:
|
|
205
|
+
print(f" Module {module.batIndex}: {module.vBat / 100}V")
|
|
206
|
+
"""
|
|
207
|
+
await self.client._ensure_authenticated()
|
|
208
|
+
|
|
209
|
+
data = {"serialNum": serial_num}
|
|
210
|
+
|
|
211
|
+
cache_key = self._get_cache_key("battery", serialNum=serial_num)
|
|
212
|
+
response = await self.client._request(
|
|
213
|
+
"POST",
|
|
214
|
+
"/WManage/api/battery/getBatteryInfo",
|
|
215
|
+
data=data,
|
|
216
|
+
cache_key=cache_key,
|
|
217
|
+
cache_endpoint="battery_info",
|
|
218
|
+
)
|
|
219
|
+
return BatteryInfo.model_validate(response)
|
|
220
|
+
|
|
221
|
+
async def get_midbox_runtime(self, serial_num: str) -> MidboxRuntime:
|
|
222
|
+
"""Get GridBOSS/MID device runtime data.
|
|
223
|
+
|
|
224
|
+
Note: Voltages, currents, and frequency require scaling (÷100).
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
serial_num: GridBOSS device serial number
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
MidboxRuntime: GridBOSS runtime metrics
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
midbox = await client.devices.get_midbox_runtime("1234567890")
|
|
234
|
+
print(f"Grid Power: {midbox.gridPower}W")
|
|
235
|
+
print(f"Load Power: {midbox.loadPower}W")
|
|
236
|
+
print(f"Generator Power: {midbox.genPower}W")
|
|
237
|
+
"""
|
|
238
|
+
await self.client._ensure_authenticated()
|
|
239
|
+
|
|
240
|
+
data = {"serialNum": serial_num}
|
|
241
|
+
|
|
242
|
+
cache_key = self._get_cache_key("midbox", serialNum=serial_num)
|
|
243
|
+
response = await self.client._request(
|
|
244
|
+
"POST",
|
|
245
|
+
"/WManage/api/midbox/getMidboxRuntime",
|
|
246
|
+
data=data,
|
|
247
|
+
cache_key=cache_key,
|
|
248
|
+
cache_endpoint="midbox_runtime",
|
|
249
|
+
)
|
|
250
|
+
return MidboxRuntime.model_validate(response)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Data export endpoints for the Luxpower API.
|
|
2
|
+
|
|
3
|
+
This module provides data export functionality for downloading
|
|
4
|
+
historical runtime data in CSV or Excel formats.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
from urllib.parse import urljoin
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
|
|
14
|
+
from pylxpweb.endpoints.base import BaseEndpoint
|
|
15
|
+
from pylxpweb.exceptions import LuxpowerConnectionError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pylxpweb.client import LuxpowerClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExportEndpoints(BaseEndpoint):
|
|
22
|
+
"""Data export endpoints for downloading historical data."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, client: LuxpowerClient) -> None:
|
|
25
|
+
"""Initialize export endpoints.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
client: The parent LuxpowerClient instance
|
|
29
|
+
"""
|
|
30
|
+
super().__init__(client)
|
|
31
|
+
|
|
32
|
+
async def export_data(
|
|
33
|
+
self,
|
|
34
|
+
serial_num: str,
|
|
35
|
+
start_date: str,
|
|
36
|
+
end_date: str | None = None,
|
|
37
|
+
) -> bytes:
|
|
38
|
+
"""Export historical data to CSV/Excel.
|
|
39
|
+
|
|
40
|
+
Downloads historical runtime data for the specified date range.
|
|
41
|
+
Returns binary data (CSV or Excel format) for external analysis.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
serial_num: Device serial number
|
|
45
|
+
start_date: Start date in YYYY-MM-DD format
|
|
46
|
+
end_date: Optional end date (if None, exports single day)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bytes: CSV/Excel file content
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
LuxpowerAPIError: If export fails
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
# Export single day
|
|
56
|
+
csv_data = await client.export.export_data("1234567890", "2025-11-19")
|
|
57
|
+
with open("data.csv", "wb") as f:
|
|
58
|
+
f.write(csv_data)
|
|
59
|
+
|
|
60
|
+
# Export date range
|
|
61
|
+
csv_data = await client.export.export_data(
|
|
62
|
+
"1234567890",
|
|
63
|
+
"2025-11-01",
|
|
64
|
+
"2025-11-19"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
Note:
|
|
68
|
+
This is a GET request that returns binary data, not JSON.
|
|
69
|
+
"""
|
|
70
|
+
await self.client._ensure_authenticated()
|
|
71
|
+
|
|
72
|
+
session = await self.client._get_session()
|
|
73
|
+
url_path = f"/WManage/web/analyze/data/export/{serial_num}/{start_date}"
|
|
74
|
+
|
|
75
|
+
if end_date:
|
|
76
|
+
url_path += f"?endDateText={end_date}"
|
|
77
|
+
|
|
78
|
+
url = urljoin(self.client.base_url, url_path)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
async with session.get(url) as response:
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return await response.read()
|
|
84
|
+
|
|
85
|
+
except aiohttp.ClientError as err:
|
|
86
|
+
raise LuxpowerConnectionError(f"Export failed: {err}") from err
|