pylxpweb 0.1.0__py3-none-any.whl → 0.5.2__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 +47 -2
- pylxpweb/api_namespace.py +241 -0
- pylxpweb/cli/__init__.py +3 -0
- pylxpweb/cli/collect_device_data.py +874 -0
- pylxpweb/client.py +387 -26
- pylxpweb/constants/__init__.py +481 -0
- pylxpweb/constants/api.py +48 -0
- pylxpweb/constants/devices.py +98 -0
- pylxpweb/constants/locations.py +227 -0
- pylxpweb/{constants.py → constants/registers.py} +72 -238
- pylxpweb/constants/scaling.py +479 -0
- pylxpweb/devices/__init__.py +32 -0
- pylxpweb/devices/_firmware_update_mixin.py +504 -0
- pylxpweb/devices/_mid_runtime_properties.py +1427 -0
- pylxpweb/devices/base.py +122 -0
- pylxpweb/devices/battery.py +589 -0
- pylxpweb/devices/battery_bank.py +331 -0
- pylxpweb/devices/inverters/__init__.py +32 -0
- pylxpweb/devices/inverters/_features.py +378 -0
- pylxpweb/devices/inverters/_runtime_properties.py +596 -0
- pylxpweb/devices/inverters/base.py +2124 -0
- pylxpweb/devices/inverters/generic.py +192 -0
- pylxpweb/devices/inverters/hybrid.py +274 -0
- pylxpweb/devices/mid_device.py +183 -0
- pylxpweb/devices/models.py +126 -0
- pylxpweb/devices/parallel_group.py +364 -0
- pylxpweb/devices/station.py +908 -0
- pylxpweb/endpoints/control.py +980 -2
- pylxpweb/endpoints/devices.py +249 -16
- pylxpweb/endpoints/firmware.py +43 -10
- pylxpweb/endpoints/plants.py +15 -19
- pylxpweb/exceptions.py +4 -0
- pylxpweb/models.py +708 -41
- pylxpweb/transports/__init__.py +78 -0
- pylxpweb/transports/capabilities.py +101 -0
- pylxpweb/transports/data.py +501 -0
- pylxpweb/transports/exceptions.py +59 -0
- pylxpweb/transports/factory.py +119 -0
- pylxpweb/transports/http.py +329 -0
- pylxpweb/transports/modbus.py +617 -0
- pylxpweb/transports/protocol.py +217 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.2.dist-info}/METADATA +130 -85
- pylxpweb-0.5.2.dist-info/RECORD +52 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.2.dist-info}/WHEEL +1 -1
- pylxpweb-0.5.2.dist-info/entry_points.txt +3 -0
- pylxpweb-0.1.0.dist-info/RECORD +0 -19
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Transport protocol definition.
|
|
2
|
+
|
|
3
|
+
This module defines the InverterTransport protocol that all transport
|
|
4
|
+
implementations must follow. Using Protocol allows for structural subtyping
|
|
5
|
+
(duck typing) while still providing type safety.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .capabilities import TransportCapabilities
|
|
14
|
+
from .data import BatteryBankData, InverterEnergyData, InverterRuntimeData
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class InverterTransport(Protocol):
|
|
19
|
+
"""Protocol defining the interface for inverter communication.
|
|
20
|
+
|
|
21
|
+
All transport implementations (HTTP, Modbus) must implement this interface.
|
|
22
|
+
This enables the same device code to work with any transport type.
|
|
23
|
+
|
|
24
|
+
The protocol is runtime-checkable, allowing isinstance() checks:
|
|
25
|
+
if isinstance(transport, InverterTransport):
|
|
26
|
+
await transport.read_runtime()
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def serial(self) -> str:
|
|
31
|
+
"""Get the inverter serial number.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
10-digit serial number string
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_connected(self) -> bool:
|
|
40
|
+
"""Check if transport is currently connected.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if connected and ready for operations
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def capabilities(self) -> TransportCapabilities:
|
|
49
|
+
"""Get transport capabilities.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Capabilities indicating what operations are supported
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
async def connect(self) -> None:
|
|
57
|
+
"""Establish connection to the device.
|
|
58
|
+
|
|
59
|
+
For HTTP: Validates credentials and establishes session
|
|
60
|
+
For Modbus: Opens TCP connection to the adapter
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TransportConnectionError: If connection fails
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
async def disconnect(self) -> None:
|
|
68
|
+
"""Close the connection.
|
|
69
|
+
|
|
70
|
+
Should be called when done with the transport.
|
|
71
|
+
Safe to call multiple times.
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
async def read_runtime(self) -> InverterRuntimeData:
|
|
76
|
+
"""Read real-time operating data from inverter.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Runtime data with all values properly scaled
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
TransportReadError: If read operation fails
|
|
83
|
+
TransportConnectionError: If not connected
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
async def read_energy(self) -> InverterEnergyData:
|
|
88
|
+
"""Read energy statistics from inverter.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Energy data with all values in kWh
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
TransportReadError: If read operation fails
|
|
95
|
+
TransportConnectionError: If not connected
|
|
96
|
+
"""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
async def read_battery(self) -> BatteryBankData | None:
|
|
100
|
+
"""Read battery bank information.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Battery bank data if batteries present, None otherwise
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TransportReadError: If read operation fails
|
|
107
|
+
TransportConnectionError: If not connected
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
async def read_parameters(
|
|
112
|
+
self,
|
|
113
|
+
start_address: int,
|
|
114
|
+
count: int,
|
|
115
|
+
) -> dict[int, int]:
|
|
116
|
+
"""Read configuration parameters (hold registers).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
start_address: Starting register address
|
|
120
|
+
count: Number of registers to read (max 127 for HTTP, 40 for Modbus)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dict mapping register address to raw integer value
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
TransportReadError: If read operation fails
|
|
127
|
+
TransportConnectionError: If not connected
|
|
128
|
+
"""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
async def write_parameters(
|
|
132
|
+
self,
|
|
133
|
+
parameters: dict[int, int],
|
|
134
|
+
) -> bool:
|
|
135
|
+
"""Write configuration parameters (hold registers).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
parameters: Dict mapping register address to value to write
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if write succeeded
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
TransportWriteError: If write operation fails
|
|
145
|
+
TransportConnectionError: If not connected
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class BaseTransport:
|
|
151
|
+
"""Base class providing common transport functionality.
|
|
152
|
+
|
|
153
|
+
Transport implementations can inherit from this class to get
|
|
154
|
+
common utilities while implementing the InverterTransport protocol.
|
|
155
|
+
|
|
156
|
+
Supports async context manager for automatic connection management:
|
|
157
|
+
async with transport:
|
|
158
|
+
data = await transport.read_runtime()
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, serial: str) -> None:
|
|
162
|
+
"""Initialize base transport.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
serial: Inverter serial number
|
|
166
|
+
"""
|
|
167
|
+
self._serial = serial
|
|
168
|
+
self._connected = False
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def serial(self) -> str:
|
|
172
|
+
"""Get the inverter serial number."""
|
|
173
|
+
return self._serial
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def is_connected(self) -> bool:
|
|
177
|
+
"""Check if transport is connected."""
|
|
178
|
+
return self._connected
|
|
179
|
+
|
|
180
|
+
async def __aenter__(self) -> Self:
|
|
181
|
+
"""Enter async context manager, connecting the transport.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Self after connecting
|
|
185
|
+
"""
|
|
186
|
+
await self.connect()
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
async def __aexit__(
|
|
190
|
+
self,
|
|
191
|
+
exc_type: type[BaseException] | None,
|
|
192
|
+
exc_val: BaseException | None,
|
|
193
|
+
exc_tb: object,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Exit async context manager, disconnecting the transport."""
|
|
196
|
+
await self.disconnect()
|
|
197
|
+
|
|
198
|
+
async def connect(self) -> None:
|
|
199
|
+
"""Establish connection. Must be implemented by subclasses."""
|
|
200
|
+
raise NotImplementedError
|
|
201
|
+
|
|
202
|
+
async def disconnect(self) -> None:
|
|
203
|
+
"""Close connection. Must be implemented by subclasses."""
|
|
204
|
+
raise NotImplementedError
|
|
205
|
+
|
|
206
|
+
def _ensure_connected(self) -> None:
|
|
207
|
+
"""Ensure transport is connected.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
TransportConnectionError: If not connected
|
|
211
|
+
"""
|
|
212
|
+
if not self._connected:
|
|
213
|
+
from .exceptions import TransportConnectionError
|
|
214
|
+
|
|
215
|
+
raise TransportConnectionError(
|
|
216
|
+
f"Transport not connected for inverter {self._serial}. Call connect() first."
|
|
217
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pylxpweb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python client library for Luxpower/EG4 inverter web monitoring API
|
|
5
5
|
Keywords: luxpower,eg4,inverter,solar,api,client
|
|
6
6
|
Author: Bryan Li
|
|
@@ -15,19 +15,29 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Classifier: Topic :: Home Automation
|
|
16
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
17
|
Requires-Dist: aiohttp>=3.13.2
|
|
18
|
-
Requires-Dist: pydantic>=2.12.
|
|
18
|
+
Requires-Dist: pydantic>=2.12.0
|
|
19
|
+
Requires-Dist: pymodbus>=3.11.4 ; extra == 'all'
|
|
19
20
|
Requires-Dist: pytest>=9.0.1 ; extra == 'dev'
|
|
20
21
|
Requires-Dist: pytest-asyncio>=1.3.0 ; extra == 'dev'
|
|
21
22
|
Requires-Dist: pytest-cov>=7.0.0 ; extra == 'dev'
|
|
22
23
|
Requires-Dist: aioresponses>=0.7.8 ; extra == 'dev'
|
|
23
24
|
Requires-Dist: mypy>=1.18.2 ; extra == 'dev'
|
|
24
25
|
Requires-Dist: ruff>=0.14.5 ; extra == 'dev'
|
|
26
|
+
Requires-Dist: pymodbus>=3.11.4 ; extra == 'modbus'
|
|
25
27
|
Requires-Python: >=3.12
|
|
28
|
+
Provides-Extra: all
|
|
26
29
|
Provides-Extra: dev
|
|
30
|
+
Provides-Extra: modbus
|
|
27
31
|
Description-Content-Type: text/markdown
|
|
28
32
|
|
|
29
33
|
# pylxpweb
|
|
30
34
|
|
|
35
|
+
[](https://github.com/joyfulhouse/pylxpweb/actions/workflows/ci.yml)
|
|
36
|
+
[](https://codecov.io/gh/joyfulhouse/pylxpweb)
|
|
37
|
+
[](https://badge.fury.io/py/pylxpweb)
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
31
41
|
A Python client library for Luxpower/EG4 solar inverters and energy storage systems, providing programmatic access to the Luxpower/EG4 web monitoring API.
|
|
32
42
|
|
|
33
43
|
## Supported API Endpoints
|
|
@@ -69,9 +79,12 @@ uv sync --all-extras --dev
|
|
|
69
79
|
|
|
70
80
|
## Quick Start
|
|
71
81
|
|
|
82
|
+
### Basic Usage with Device Objects
|
|
83
|
+
|
|
72
84
|
```python
|
|
73
85
|
import asyncio
|
|
74
86
|
from pylxpweb import LuxpowerClient
|
|
87
|
+
from pylxpweb.devices.station import Station
|
|
75
88
|
|
|
76
89
|
async def main():
|
|
77
90
|
# Create client with credentials
|
|
@@ -81,43 +94,93 @@ async def main():
|
|
|
81
94
|
password="your_password",
|
|
82
95
|
base_url="https://monitor.eg4electronics.com" # or us.luxpowertek.com, eu.luxpowertek.com
|
|
83
96
|
) as client:
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
print(f"Found {len(
|
|
87
|
-
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
97
|
+
# Load all stations with device hierarchy
|
|
98
|
+
stations = await Station.load_all(client)
|
|
99
|
+
print(f"Found {len(stations)} stations")
|
|
100
|
+
|
|
101
|
+
# Work with first station
|
|
102
|
+
station = stations[0]
|
|
103
|
+
print(f"\nStation: {station.name}")
|
|
104
|
+
|
|
105
|
+
# Access inverters - all have properly-scaled properties
|
|
106
|
+
for inverter in station.all_inverters:
|
|
107
|
+
await inverter.refresh() # Fetch latest data
|
|
108
|
+
|
|
109
|
+
print(f"\n{inverter.model} {inverter.serial_number}:")
|
|
110
|
+
|
|
111
|
+
# All properties return properly-scaled values
|
|
112
|
+
print(f" PV Power: {inverter.pv_total_power}W")
|
|
113
|
+
print(f" Battery: {inverter.battery_soc}% @ {inverter.battery_voltage}V")
|
|
114
|
+
print(f" Grid: {inverter.grid_voltage_r}V @ {inverter.grid_frequency}Hz")
|
|
115
|
+
print(f" Inverter Power: {inverter.inverter_power}W")
|
|
116
|
+
print(f" To Grid: {inverter.power_to_grid}W")
|
|
117
|
+
print(f" To User: {inverter.power_to_user}W")
|
|
118
|
+
print(f" Temperature: {inverter.inverter_temperature}°C")
|
|
119
|
+
print(f" Today: {inverter.total_energy_today}kWh")
|
|
120
|
+
print(f" Lifetime: {inverter.total_energy_lifetime}kWh")
|
|
121
|
+
|
|
122
|
+
# Access battery bank if available
|
|
123
|
+
if inverter.battery_bank:
|
|
124
|
+
bank = inverter.battery_bank
|
|
125
|
+
print(f"\n Battery Bank:")
|
|
126
|
+
print(f" Voltage: {bank.voltage}V")
|
|
127
|
+
print(f" SOC: {bank.soc}%")
|
|
128
|
+
print(f" Charge Power: {bank.charge_power}W")
|
|
129
|
+
print(f" Discharge Power: {bank.discharge_power}W")
|
|
130
|
+
print(f" Capacity: {bank.current_capacity}/{bank.max_capacity} Ah")
|
|
131
|
+
|
|
132
|
+
# Individual battery modules
|
|
133
|
+
for battery in bank.batteries:
|
|
134
|
+
print(f" Battery {battery.battery_index + 1}:")
|
|
135
|
+
print(f" Voltage: {battery.voltage}V")
|
|
136
|
+
print(f" Current: {battery.current}A")
|
|
137
|
+
print(f" SOC: {battery.soc}%")
|
|
138
|
+
print(f" Temp: {battery.max_cell_temp}°C")
|
|
139
|
+
|
|
140
|
+
# Access GridBOSS (MID) devices if present
|
|
141
|
+
for group in station.parallel_groups:
|
|
142
|
+
if group.mid_device:
|
|
143
|
+
mid = group.mid_device
|
|
144
|
+
await mid.refresh()
|
|
145
|
+
|
|
146
|
+
print(f"\nGridBOSS {mid.serial_number}:")
|
|
147
|
+
print(f" Grid: {mid.grid_voltage}V @ {mid.grid_frequency}Hz")
|
|
148
|
+
print(f" Grid Power: {mid.grid_power}W")
|
|
149
|
+
print(f" UPS Power: {mid.ups_power}W")
|
|
150
|
+
print(f" Load L1: {mid.load_l1_power}W @ {mid.load_l1_current}A")
|
|
151
|
+
print(f" Load L2: {mid.load_l2_power}W @ {mid.load_l2_current}A")
|
|
117
152
|
|
|
118
153
|
asyncio.run(main())
|
|
119
154
|
```
|
|
120
155
|
|
|
156
|
+
### Low-Level API Access
|
|
157
|
+
|
|
158
|
+
For direct API access without device objects:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
async with LuxpowerClient(username, password) as client:
|
|
162
|
+
# Get stations
|
|
163
|
+
plants = await client.api.plants.get_plants()
|
|
164
|
+
plant_id = plants.rows[0].plantId
|
|
165
|
+
|
|
166
|
+
# Get devices
|
|
167
|
+
devices = await client.api.devices.get_devices(str(plant_id))
|
|
168
|
+
|
|
169
|
+
# Get runtime data for first inverter
|
|
170
|
+
inverter = devices.rows[0]
|
|
171
|
+
serial = inverter.serialNum
|
|
172
|
+
|
|
173
|
+
# Fetch data (returns Pydantic models)
|
|
174
|
+
runtime = await client.api.devices.get_inverter_runtime(serial)
|
|
175
|
+
energy = await client.api.devices.get_inverter_energy(serial)
|
|
176
|
+
|
|
177
|
+
# NOTE: Raw API returns scaled integers - you must scale manually
|
|
178
|
+
print(f"AC Power: {runtime.pac}W") # No scaling needed for power
|
|
179
|
+
print(f"Grid Voltage: {runtime.vacr / 10}V") # Must divide by 10
|
|
180
|
+
print(f"Grid Frequency: {runtime.fac / 100}Hz") # Must divide by 100
|
|
181
|
+
print(f"Battery Voltage: {runtime.vBat / 10}V") # Must divide by 10
|
|
182
|
+
```
|
|
183
|
+
|
|
121
184
|
## Advanced Usage
|
|
122
185
|
|
|
123
186
|
### Regional Endpoints and Custom Session
|
|
@@ -284,18 +347,40 @@ pylxpweb/
|
|
|
284
347
|
|
|
285
348
|
## Data Scaling
|
|
286
349
|
|
|
287
|
-
|
|
350
|
+
### Automatic Scaling with Device Properties (Recommended)
|
|
288
351
|
|
|
289
|
-
|
|
290
|
-
|-----------|---------|---------|
|
|
291
|
-
| Voltage | ÷ 100 | 5100 → 51.00V |
|
|
292
|
-
| Current | ÷ 100 | 1500 → 15.00A |
|
|
293
|
-
| Frequency | ÷ 100 | 5998 → 59.98Hz |
|
|
294
|
-
| Cell Voltage | ÷ 1000 | 3350 → 3.350V |
|
|
295
|
-
| Power | none | 1030 → 1030W |
|
|
296
|
-
| Temperature | none | 39 → 39°C |
|
|
352
|
+
**Device objects automatically handle all scaling** - just use the properties:
|
|
297
353
|
|
|
298
|
-
|
|
354
|
+
```python
|
|
355
|
+
# ✅ RECOMMENDED: Use device properties (automatically scaled)
|
|
356
|
+
await inverter.refresh()
|
|
357
|
+
voltage = inverter.grid_voltage_r # Returns 241.8 (already scaled)
|
|
358
|
+
frequency = inverter.grid_frequency # Returns 59.98 (already scaled)
|
|
359
|
+
power = inverter.pv_total_power # Returns 1500 (already scaled)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
All device classes (`BaseInverter`, `MIDDevice`, `Battery`, `BatteryBank`, `ParallelGroup`) provide properly-scaled properties. **You never need to manually scale values when using device objects.**
|
|
363
|
+
|
|
364
|
+
### Manual Scaling for Raw API Data
|
|
365
|
+
|
|
366
|
+
If you use the low-level API directly (not recommended for most users), you must scale values manually:
|
|
367
|
+
|
|
368
|
+
| Data Type | Scaling | Raw API | Scaled | Property Name |
|
|
369
|
+
|-----------|---------|---------|--------|---------------|
|
|
370
|
+
| Inverter Voltage | ÷10 | 2410 | 241.0V | `grid_voltage_r` |
|
|
371
|
+
| Battery Voltage (Bank) | ÷10 | 539 | 53.9V | `battery_voltage` |
|
|
372
|
+
| Battery Voltage (Module) | ÷100 | 5394 | 53.94V | `voltage` |
|
|
373
|
+
| Cell Voltage | ÷1000 | 3364 | 3.364V | `max_cell_voltage` |
|
|
374
|
+
| Current | ÷100 | 1500 | 15.00A | `grid_l1_current` |
|
|
375
|
+
| Frequency | ÷100 | 5998 | 59.98Hz | `grid_frequency` |
|
|
376
|
+
| Bus Voltage | ÷100 | 3703 | 37.03V | `bus1_voltage` |
|
|
377
|
+
| Power | Direct | 1030 | 1030W | `inverter_power` |
|
|
378
|
+
| Temperature | Direct | 39 | 39°C | `inverter_temperature` |
|
|
379
|
+
| Energy | ÷10 | 184 | 18.4 kWh | `today_yielding` |
|
|
380
|
+
|
|
381
|
+
**Note**: Different voltage types use different scaling factors. Use device properties to avoid confusion.
|
|
382
|
+
|
|
383
|
+
See [Scaling Guide](docs/SCALING_GUIDE.md) and [API Reference](docs/api/LUXPOWER_API.md#data-scaling-reference) for complete details.
|
|
299
384
|
|
|
300
385
|
## API Endpoints
|
|
301
386
|
|
|
@@ -388,46 +473,6 @@ This library communicates with the official EG4/Luxpower API using the same endp
|
|
|
388
473
|
- **Issues**: [GitHub Issues](https://github.com/joyfulhouse/pylxpweb/issues)
|
|
389
474
|
- **API Reference**: [docs/api/LUXPOWER_API.md](docs/api/LUXPOWER_API.md)
|
|
390
475
|
|
|
391
|
-
## Status
|
|
392
|
-
|
|
393
|
-
**Current Phase**: Core Implementation Complete
|
|
394
|
-
|
|
395
|
-
- ✅ Research and API documentation complete
|
|
396
|
-
- ✅ CLAUDE.md development guidelines
|
|
397
|
-
- ✅ Comprehensive API reference documentation
|
|
398
|
-
- ✅ Core library implementation (async client with full API coverage)
|
|
399
|
-
- ✅ Test suite development (95% code coverage with 44 unit tests)
|
|
400
|
-
- ✅ Package configuration (uv + pyproject.toml)
|
|
401
|
-
- ✅ Type safety (mypy --strict passing)
|
|
402
|
-
- ✅ Code quality (ruff linting passing)
|
|
403
|
-
- ⏳ PyPI publication
|
|
404
|
-
|
|
405
|
-
## Roadmap
|
|
406
|
-
|
|
407
|
-
1. **Phase 1**: Core library implementation
|
|
408
|
-
- Client class with authentication
|
|
409
|
-
- Device discovery and management
|
|
410
|
-
- Runtime data retrieval
|
|
411
|
-
- Data models and scaling
|
|
412
|
-
|
|
413
|
-
2. **Phase 2**: Advanced features
|
|
414
|
-
- Control operations
|
|
415
|
-
- Caching with configurable TTL
|
|
416
|
-
- Retry logic and error handling
|
|
417
|
-
- Session injection support
|
|
418
|
-
|
|
419
|
-
3. **Phase 3**: Testing and polish
|
|
420
|
-
- Comprehensive test suite (>90% coverage)
|
|
421
|
-
- Integration tests
|
|
422
|
-
- Documentation examples
|
|
423
|
-
- Type checking with mypy strict mode
|
|
424
|
-
|
|
425
|
-
4. **Phase 4**: Distribution
|
|
426
|
-
- Package configuration (pyproject.toml)
|
|
427
|
-
- PyPI publication
|
|
428
|
-
- CI/CD pipeline
|
|
429
|
-
- Release automation
|
|
430
|
-
|
|
431
476
|
---
|
|
432
477
|
|
|
433
478
|
**Happy monitoring!** ☀️⚡🔋
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
pylxpweb/__init__.py,sha256=splapsTwdAEK7YTB-QeizTO0k6_D2GHXMNYTXTAzrIo,2370
|
|
2
|
+
pylxpweb/api_namespace.py,sha256=wvQa1UQKR_yG-kzs7NydWQ1pLW8T5MhsJq5CBlsgrRk,7552
|
|
3
|
+
pylxpweb/cli/__init__.py,sha256=VXKyWZs50UeY2xofuTiwbE87U4ts8wSacgX00ThDurM,85
|
|
4
|
+
pylxpweb/cli/collect_device_data.py,sha256=17y8YQLceuzv90-8slB_FfGGEecsPtJB1C84tHG2pic,28919
|
|
5
|
+
pylxpweb/client.py,sha256=oRKnQQ_5N7fM2wRb7amHCHy3UeQQmAjbol4nXH-DW2w,31268
|
|
6
|
+
pylxpweb/constants/__init__.py,sha256=JUwt0mxACX93WjwF6tdBXhJqxvGKJGBm443VEfQRxxc,12682
|
|
7
|
+
pylxpweb/constants/api.py,sha256=7GGyvOgHNc3BPgPrgCBnrAtjwiI8fUVYXTceAuN3LGo,1943
|
|
8
|
+
pylxpweb/constants/devices.py,sha256=XF5y51LodBG6N4mOZddr6DJVd3tMvSXgjSkQ5r1JKWI,3257
|
|
9
|
+
pylxpweb/constants/locations.py,sha256=1V6MHfwFf5KVX1AQeiuc50ZQsca9Aei2RsTHaWpc3Ck,7385
|
|
10
|
+
pylxpweb/constants/registers.py,sha256=V5B3Bkvgbp-8mJ_tVpN8z5GM0OnWrLE2LOR4DfYEHbM,38193
|
|
11
|
+
pylxpweb/constants/scaling.py,sha256=l1QHf4Fa1VQKqcVK9snTHy9BOB0mNzb3d5bGynZSmJ8,17819
|
|
12
|
+
pylxpweb/devices/__init__.py,sha256=gWJ-lHEwfIm7bFwMx4TRDG-eaKg7jObCzSGyflvEfls,788
|
|
13
|
+
pylxpweb/devices/_firmware_update_mixin.py,sha256=LkwS36dSvoE6iTlCk3osRqcOySgEG2MD09pPnbnaIEU,21045
|
|
14
|
+
pylxpweb/devices/_mid_runtime_properties.py,sha256=bV_66CH5Uyw4F3kGvWyU8TEWln5njUeJVnzG7E9Q1MI,44156
|
|
15
|
+
pylxpweb/devices/base.py,sha256=Hdr7O-ct-Ouju8O8XqmzfGMAHKa5JuchElhSye2SYDk,3627
|
|
16
|
+
pylxpweb/devices/battery.py,sha256=6YQ3vG70kPu18Av6RA3Rd0u8qktSgJbVRa-Jkyveq-4,18032
|
|
17
|
+
pylxpweb/devices/battery_bank.py,sha256=lQurUHx8THXgMQMB3ZVudcIVEGF9Lo0w1mCn-NSfZrc,9620
|
|
18
|
+
pylxpweb/devices/inverters/__init__.py,sha256=UmH9C_QxKGydPNm6KpzpUjUGjgAxyw5OijG_mvqtgxg,744
|
|
19
|
+
pylxpweb/devices/inverters/_features.py,sha256=1Y6Bx3Hb7nSr9_i4bIjvAq4uqk0_thXsbaiS35iQRKg,13104
|
|
20
|
+
pylxpweb/devices/inverters/_runtime_properties.py,sha256=C8c0qj8BSes0pHfRBZMqzY1eALsaB2cIi4lipF0zG4M,15634
|
|
21
|
+
pylxpweb/devices/inverters/base.py,sha256=DqwABIrR917nPWqIDLf1Zj599loSqaxQrg2Rg_C4VSw,73756
|
|
22
|
+
pylxpweb/devices/inverters/generic.py,sha256=dfydpVSa38fHCptfaCPDhE1Q3tpP8OPizgOTapiotLE,7148
|
|
23
|
+
pylxpweb/devices/inverters/hybrid.py,sha256=rRncXS0SfVxWLKE7PHTqbQMTDHkiAZvFlfojzKwUpo0,9203
|
|
24
|
+
pylxpweb/devices/mid_device.py,sha256=xXCr5XcL3eRA6TqoN_pxxg99mVkWdX4GXfOQQXumEeU,6167
|
|
25
|
+
pylxpweb/devices/models.py,sha256=OAMGZ9FCkgE3jY9xIky5Qf71dLzgi5bSd_dng-YMwak,3931
|
|
26
|
+
pylxpweb/devices/parallel_group.py,sha256=kij59OTKUMF8DiuEP9_Tf_4kH-xU640F8F1vmtIIHlc,11963
|
|
27
|
+
pylxpweb/devices/station.py,sha256=r7616lb8DUx_BSI20AWBB4bzqn2l6zYw2P4PCs9Kdzw,34067
|
|
28
|
+
pylxpweb/endpoints/__init__.py,sha256=lzHDUiFCoyqRgncBYs5_EHy4I8I5CJAReTx8o4TdbTk,934
|
|
29
|
+
pylxpweb/endpoints/analytics.py,sha256=Zyi9lVDXGr-4cvpv-POdLaF1OE3qgOcYwyzMNvL1xzI,14482
|
|
30
|
+
pylxpweb/endpoints/base.py,sha256=QFFH4l83Bui1oK_ghrROYXAXAnwbLgTTd2Z6KFP6APw,1265
|
|
31
|
+
pylxpweb/endpoints/control.py,sha256=ELGbDfgEsnuhL7UzSH0-hk7VYkkYd0AmHFEKouaf0bU,44476
|
|
32
|
+
pylxpweb/endpoints/devices.py,sha256=PPTMj9g-GOSNUGhRX8CZPkkAWHphy1g3UddmVCOWQqw,17640
|
|
33
|
+
pylxpweb/endpoints/export.py,sha256=UPedThsYCmkeo1Yxu_CMIuG8h6Cu1Z9l4veYwKDVN8I,2508
|
|
34
|
+
pylxpweb/endpoints/firmware.py,sha256=vwuPRzc0TcDNpY2ygtLslUDn8LyxVh11uHum4mzofoQ,10286
|
|
35
|
+
pylxpweb/endpoints/forecasting.py,sha256=0Zs4qGdqIFrNsKn94qSpDixQJrWJIadfPs8XULbEn4w,3836
|
|
36
|
+
pylxpweb/endpoints/plants.py,sha256=zUl1vdQXpQSdG2YhzZfAIRyeSfjbxmTZIqZW4DBcD2g,16278
|
|
37
|
+
pylxpweb/exceptions.py,sha256=-D-dbUFPMGupzhiV52X9Kz3vx7UOvx67lsCx6hR0KWI,671
|
|
38
|
+
pylxpweb/models.py,sha256=rczIBZDOaJUEjwQhiBJ5Ll_MJNsJCf0k1rYKnLbp9Sc,40967
|
|
39
|
+
pylxpweb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
+
pylxpweb/registers.py,sha256=Chk1uIoc5DjapSquEHYtBYgiPKuPhTjVn7PMl5JuPF0,12946
|
|
41
|
+
pylxpweb/transports/__init__.py,sha256=xvLjLWS0VaiyQJqgiuxwq3_3uHETUIMmmZZOnNDKSdU,2119
|
|
42
|
+
pylxpweb/transports/capabilities.py,sha256=IOJ_eWaLFInKKXObXjdTHXYz1UZNgLyP6QvUhAfWK7k,3460
|
|
43
|
+
pylxpweb/transports/data.py,sha256=VE6FzVUPyqsnDCOxjsaDkkZgz2APNFCPJdMmdEvn1Iw,18571
|
|
44
|
+
pylxpweb/transports/exceptions.py,sha256=re2wCq2h25TIhXKrz7BjZjA1G1sZtTjZzVooTNHsaWU,1437
|
|
45
|
+
pylxpweb/transports/factory.py,sha256=hcorD4SHOouoBG5qAP-0TqJbKMPah2kTl-z58UQ-t8E,3380
|
|
46
|
+
pylxpweb/transports/http.py,sha256=O93lxfU052YLKesx-opwAq_gHnAN9n_PR3tb1wbfM6I,13358
|
|
47
|
+
pylxpweb/transports/modbus.py,sha256=A3-Q_G1oj-wwv-ZXiKnE4G0lQ9Gh-07C0O2OCOep2Uo,21418
|
|
48
|
+
pylxpweb/transports/protocol.py,sha256=yIsZJIJ1x0zO1YC0ymEY2Y7T4Ld3zPGojubr_rPsrl0,6069
|
|
49
|
+
pylxpweb-0.5.2.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
|
|
50
|
+
pylxpweb-0.5.2.dist-info/entry_points.txt,sha256=4BNqpz9UhA301Z2CKJy1914L4AlHVHHrweIem2qk64g,76
|
|
51
|
+
pylxpweb-0.5.2.dist-info/METADATA,sha256=_t1ulJPWbf7d3YeR2xUkIFopVMlfRMNUhe-6qoC35fg,17575
|
|
52
|
+
pylxpweb-0.5.2.dist-info/RECORD,,
|
pylxpweb-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
pylxpweb/__init__.py,sha256=z2eTcHo04RfZcyeiPE5Kn4Tt-Audtv-XZTRETOJ8Pvk,857
|
|
2
|
-
pylxpweb/client.py,sha256=5lQoa2i2fmnL6eGuE1m8RWSZcYpYmYusTxZQ0GyRy5c,15507
|
|
3
|
-
pylxpweb/constants.py,sha256=4Bx7CEnl9T8-mTlCOYd5jG80FdSTIH-msOXnIuo9utY,43198
|
|
4
|
-
pylxpweb/endpoints/__init__.py,sha256=lzHDUiFCoyqRgncBYs5_EHy4I8I5CJAReTx8o4TdbTk,934
|
|
5
|
-
pylxpweb/endpoints/analytics.py,sha256=Zyi9lVDXGr-4cvpv-POdLaF1OE3qgOcYwyzMNvL1xzI,14482
|
|
6
|
-
pylxpweb/endpoints/base.py,sha256=QFFH4l83Bui1oK_ghrROYXAXAnwbLgTTd2Z6KFP6APw,1265
|
|
7
|
-
pylxpweb/endpoints/control.py,sha256=_p1wIc7tKeV8KhDrPdBgrmWBUR302_s9KTbL_TYWp9g,10360
|
|
8
|
-
pylxpweb/endpoints/devices.py,sha256=0fBWQ5S14HGnEk57zUHUjX4xju4FY5Uh8vS8_3FwISM,8244
|
|
9
|
-
pylxpweb/endpoints/export.py,sha256=UPedThsYCmkeo1Yxu_CMIuG8h6Cu1Z9l4veYwKDVN8I,2508
|
|
10
|
-
pylxpweb/endpoints/firmware.py,sha256=b1_cjgPVZmn6oZCnHdyX24tB1zDjE1lWgyV5sZ3OcNs,8773
|
|
11
|
-
pylxpweb/endpoints/forecasting.py,sha256=0Zs4qGdqIFrNsKn94qSpDixQJrWJIadfPs8XULbEn4w,3836
|
|
12
|
-
pylxpweb/endpoints/plants.py,sha256=Yt7BE8N-CvsUiRWFR30yb0DRw5UZNC3sX0jI8UsIfzE,16407
|
|
13
|
-
pylxpweb/exceptions.py,sha256=5otooo7NCLMoj4i7tgHPAb85AwgsqqemHTrkqX8R12g,556
|
|
14
|
-
pylxpweb/models.py,sha256=zkw_LU4K3IIF1CwlXkkTYakXU9fiN-PgzwSscC2sULI,18598
|
|
15
|
-
pylxpweb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
pylxpweb/registers.py,sha256=Chk1uIoc5DjapSquEHYtBYgiPKuPhTjVn7PMl5JuPF0,12946
|
|
17
|
-
pylxpweb-0.1.0.dist-info/WHEEL,sha256=ZHijuPszqKbNczrBXkSuoxdxocbxgFghqnequ9ZQlVk,79
|
|
18
|
-
pylxpweb-0.1.0.dist-info/METADATA,sha256=N1Hao1B93D0JZnSa-0AbgVlTBPvukrxVBeAevkQp8oA,14268
|
|
19
|
-
pylxpweb-0.1.0.dist-info/RECORD,,
|