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