pylxpweb 0.1.0__py3-none-any.whl → 0.5.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.
Files changed (46) hide show
  1. pylxpweb/__init__.py +47 -2
  2. pylxpweb/api_namespace.py +241 -0
  3. pylxpweb/cli/__init__.py +3 -0
  4. pylxpweb/cli/collect_device_data.py +874 -0
  5. pylxpweb/client.py +387 -26
  6. pylxpweb/constants/__init__.py +481 -0
  7. pylxpweb/constants/api.py +48 -0
  8. pylxpweb/constants/devices.py +98 -0
  9. pylxpweb/constants/locations.py +227 -0
  10. pylxpweb/{constants.py → constants/registers.py} +72 -238
  11. pylxpweb/constants/scaling.py +479 -0
  12. pylxpweb/devices/__init__.py +32 -0
  13. pylxpweb/devices/_firmware_update_mixin.py +504 -0
  14. pylxpweb/devices/_mid_runtime_properties.py +545 -0
  15. pylxpweb/devices/base.py +122 -0
  16. pylxpweb/devices/battery.py +589 -0
  17. pylxpweb/devices/battery_bank.py +331 -0
  18. pylxpweb/devices/inverters/__init__.py +32 -0
  19. pylxpweb/devices/inverters/_features.py +378 -0
  20. pylxpweb/devices/inverters/_runtime_properties.py +596 -0
  21. pylxpweb/devices/inverters/base.py +2124 -0
  22. pylxpweb/devices/inverters/generic.py +192 -0
  23. pylxpweb/devices/inverters/hybrid.py +274 -0
  24. pylxpweb/devices/mid_device.py +183 -0
  25. pylxpweb/devices/models.py +126 -0
  26. pylxpweb/devices/parallel_group.py +351 -0
  27. pylxpweb/devices/station.py +908 -0
  28. pylxpweb/endpoints/control.py +980 -2
  29. pylxpweb/endpoints/devices.py +249 -16
  30. pylxpweb/endpoints/firmware.py +43 -10
  31. pylxpweb/endpoints/plants.py +15 -19
  32. pylxpweb/exceptions.py +4 -0
  33. pylxpweb/models.py +629 -40
  34. pylxpweb/transports/__init__.py +78 -0
  35. pylxpweb/transports/capabilities.py +101 -0
  36. pylxpweb/transports/data.py +495 -0
  37. pylxpweb/transports/exceptions.py +59 -0
  38. pylxpweb/transports/factory.py +119 -0
  39. pylxpweb/transports/http.py +329 -0
  40. pylxpweb/transports/modbus.py +557 -0
  41. pylxpweb/transports/protocol.py +217 -0
  42. {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/METADATA +130 -85
  43. pylxpweb-0.5.0.dist-info/RECORD +52 -0
  44. {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/WHEEL +1 -1
  45. pylxpweb-0.5.0.dist-info/entry_points.txt +3 -0
  46. pylxpweb-0.1.0.dist-info/RECORD +0 -19
@@ -0,0 +1,479 @@
1
+ """Data scaling constants and functions for API values.
2
+
3
+ This module contains all scaling factors and helper functions for converting
4
+ raw API values to properly scaled units (volts, amps, watts, etc.).
5
+
6
+ Source: Analysis of EG4 Web Monitor and actual API responses.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+ from typing import Literal
13
+
14
+ # ============================================================================
15
+ # DATA SCALING CONSTANTS
16
+ # ============================================================================
17
+ # Centralized scaling configuration for all API data types.
18
+ # Source: Analysis of EG4 Web Monitor and actual API responses.
19
+ # Reference: docs/claude/PARAMETER_MAPPING_ANALYSIS.md
20
+ #
21
+ # **Design Rationale**:
22
+ # - Use dictionaries for O(1) lookup performance
23
+ # - Group by data source (runtime, energy, battery, etc.)
24
+ # - Include documentation for maintainability
25
+ # - Support both field-based and frozenset-based lookups
26
+
27
+
28
+ class ScaleFactor(int, Enum):
29
+ """Enumeration of scaling factors used in API data.
30
+
31
+ Values represent the divisor to apply to raw API values.
32
+ Example: SCALE_10 means divide by 10 (e.g., 5300 → 530.0)
33
+ """
34
+
35
+ SCALE_10 = 10 # Divide by 10
36
+ SCALE_100 = 100 # Divide by 100
37
+ SCALE_1000 = 1000 # Divide by 1000
38
+ SCALE_NONE = 1 # No scaling (direct value)
39
+
40
+
41
+ # ============================================================================
42
+ # INVERTER RUNTIME DATA SCALING
43
+ # ============================================================================
44
+ # Source: InverterRuntime model from getInverterRuntime endpoint
45
+ # Verified against: research/.../runtime_4512670118.json
46
+
47
+ INVERTER_RUNTIME_SCALING: dict[str, ScaleFactor] = {
48
+ # PV Input Voltages (÷10: 5100 → 510.0V)
49
+ "vpv1": ScaleFactor.SCALE_10,
50
+ "vpv2": ScaleFactor.SCALE_10,
51
+ "vpv3": ScaleFactor.SCALE_10,
52
+ # AC Voltages (÷10: 2411 → 241.1V)
53
+ "vacr": ScaleFactor.SCALE_10,
54
+ "vacs": ScaleFactor.SCALE_10,
55
+ "vact": ScaleFactor.SCALE_10,
56
+ # EPS Voltages (÷10)
57
+ "vepsr": ScaleFactor.SCALE_10,
58
+ "vepss": ScaleFactor.SCALE_10,
59
+ "vepst": ScaleFactor.SCALE_10,
60
+ # Battery Voltage in Runtime (÷10: 530 → 53.0V)
61
+ "vBat": ScaleFactor.SCALE_10,
62
+ # Bus Voltages (÷100: 3703 → 37.03V)
63
+ "vBus1": ScaleFactor.SCALE_100,
64
+ "vBus2": ScaleFactor.SCALE_100,
65
+ # AC Frequency (÷100: 5998 → 59.98Hz)
66
+ "fac": ScaleFactor.SCALE_100,
67
+ "feps": ScaleFactor.SCALE_100,
68
+ # Generator Frequency (÷100)
69
+ "genFreq": ScaleFactor.SCALE_100,
70
+ # Generator Voltage (÷10)
71
+ "genVolt": ScaleFactor.SCALE_10,
72
+ # Currents (÷100: 1500 → 15.00A)
73
+ "maxChgCurr": ScaleFactor.SCALE_100,
74
+ "maxDischgCurr": ScaleFactor.SCALE_100,
75
+ "maxChgCurrValue": ScaleFactor.SCALE_100,
76
+ "maxDischgCurrValue": ScaleFactor.SCALE_100,
77
+ # Power values - NO SCALING (direct Watts)
78
+ "ppv1": ScaleFactor.SCALE_NONE,
79
+ "ppv2": ScaleFactor.SCALE_NONE,
80
+ "ppv3": ScaleFactor.SCALE_NONE,
81
+ "ppv": ScaleFactor.SCALE_NONE,
82
+ "pCharge": ScaleFactor.SCALE_NONE,
83
+ "pDisCharge": ScaleFactor.SCALE_NONE,
84
+ "batPower": ScaleFactor.SCALE_NONE,
85
+ "pToGrid": ScaleFactor.SCALE_NONE,
86
+ "pToUser": ScaleFactor.SCALE_NONE,
87
+ "pinv": ScaleFactor.SCALE_NONE,
88
+ "prec": ScaleFactor.SCALE_NONE,
89
+ "peps": ScaleFactor.SCALE_NONE,
90
+ "acCouplePower": ScaleFactor.SCALE_NONE,
91
+ "genPower": ScaleFactor.SCALE_NONE,
92
+ "consumptionPower114": ScaleFactor.SCALE_NONE,
93
+ "consumptionPower": ScaleFactor.SCALE_NONE,
94
+ "pEpsL1N": ScaleFactor.SCALE_NONE,
95
+ "pEpsL2N": ScaleFactor.SCALE_NONE,
96
+ # Temperature - NO SCALING (direct Celsius)
97
+ "tinner": ScaleFactor.SCALE_NONE,
98
+ "tradiator1": ScaleFactor.SCALE_NONE,
99
+ "tradiator2": ScaleFactor.SCALE_NONE,
100
+ "tBat": ScaleFactor.SCALE_NONE,
101
+ # Percentages - NO SCALING
102
+ "soc": ScaleFactor.SCALE_NONE,
103
+ "seps": ScaleFactor.SCALE_NONE,
104
+ }
105
+
106
+
107
+ # ============================================================================
108
+ # ENERGY DATA SCALING
109
+ # ============================================================================
110
+ # Source: EnergyInfo model from getInverterEnergyInfo endpoint
111
+ # All energy values from API are in 0.1 kWh units, need ÷10 to get kWh directly
112
+ # Example: API returns 184 → 184 ÷ 10 = 18.4 kWh
113
+
114
+ ENERGY_INFO_SCALING: dict[str, ScaleFactor] = {
115
+ # Daily Energy (÷10 to get kWh: 184 → 18.4 kWh)
116
+ "todayYielding": ScaleFactor.SCALE_10,
117
+ "todayCharging": ScaleFactor.SCALE_10,
118
+ "todayDischarging": ScaleFactor.SCALE_10,
119
+ "todayGridImport": ScaleFactor.SCALE_10,
120
+ "todayImport": ScaleFactor.SCALE_10, # Alternative field name for todayGridImport
121
+ "todayUsage": ScaleFactor.SCALE_10,
122
+ "todayExport": ScaleFactor.SCALE_10,
123
+ # Monthly Energy (÷10 to get kWh)
124
+ "monthYielding": ScaleFactor.SCALE_10,
125
+ "monthCharging": ScaleFactor.SCALE_10,
126
+ "monthDischarging": ScaleFactor.SCALE_10,
127
+ "monthGridImport": ScaleFactor.SCALE_10,
128
+ "monthImport": ScaleFactor.SCALE_10, # Alternative field name for monthGridImport
129
+ "monthUsage": ScaleFactor.SCALE_10,
130
+ "monthExport": ScaleFactor.SCALE_10,
131
+ # Yearly Energy (÷10 to get kWh)
132
+ "yearYielding": ScaleFactor.SCALE_10,
133
+ "yearCharging": ScaleFactor.SCALE_10,
134
+ "yearDischarging": ScaleFactor.SCALE_10,
135
+ "yearGridImport": ScaleFactor.SCALE_10,
136
+ "yearImport": ScaleFactor.SCALE_10, # Alternative field name for yearGridImport
137
+ "yearUsage": ScaleFactor.SCALE_10,
138
+ "yearExport": ScaleFactor.SCALE_10,
139
+ # Lifetime Total Energy (÷10 to get kWh)
140
+ "totalYielding": ScaleFactor.SCALE_10,
141
+ "totalCharging": ScaleFactor.SCALE_10,
142
+ "totalDischarging": ScaleFactor.SCALE_10,
143
+ "totalGridImport": ScaleFactor.SCALE_10,
144
+ "totalImport": ScaleFactor.SCALE_10, # Alternative field name for totalGridImport
145
+ "totalUsage": ScaleFactor.SCALE_10,
146
+ "totalExport": ScaleFactor.SCALE_10,
147
+ }
148
+
149
+
150
+ # ============================================================================
151
+ # BATTERY DATA SCALING
152
+ # ============================================================================
153
+
154
+ # Battery Bank Aggregate (from BatteryInfo header)
155
+ BATTERY_BANK_SCALING: dict[str, ScaleFactor] = {
156
+ # Aggregate voltage (÷10: 530 → 53.0V)
157
+ "vBat": ScaleFactor.SCALE_10,
158
+ # Power - NO SCALING (direct Watts)
159
+ "pCharge": ScaleFactor.SCALE_NONE,
160
+ "pDisCharge": ScaleFactor.SCALE_NONE,
161
+ "batPower": ScaleFactor.SCALE_NONE,
162
+ # Capacity (direct Ah)
163
+ "maxBatteryCharge": ScaleFactor.SCALE_NONE,
164
+ "currentBatteryCharge": ScaleFactor.SCALE_NONE,
165
+ "remainCapacity": ScaleFactor.SCALE_NONE,
166
+ "fullCapacity": ScaleFactor.SCALE_NONE,
167
+ # Percentage - NO SCALING
168
+ "soc": ScaleFactor.SCALE_NONE,
169
+ "capacityPercent": ScaleFactor.SCALE_NONE,
170
+ }
171
+
172
+ # Individual Battery Module (from batteryArray)
173
+ BATTERY_MODULE_SCALING: dict[str, ScaleFactor] = {
174
+ # Total voltage (÷100: 5305 → 53.05V)
175
+ "totalVoltage": ScaleFactor.SCALE_100,
176
+ # Current (÷10: 60 → 6.0A) **CRITICAL: Not ÷100**
177
+ "current": ScaleFactor.SCALE_10,
178
+ # Cell Voltages (÷1000: 3317 → 3.317V - millivolts)
179
+ "batMaxCellVoltage": ScaleFactor.SCALE_1000,
180
+ "batMinCellVoltage": ScaleFactor.SCALE_1000,
181
+ # Cell Temperatures (÷10: 240 → 24.0°C)
182
+ "batMaxCellTemp": ScaleFactor.SCALE_10,
183
+ "batMinCellTemp": ScaleFactor.SCALE_10,
184
+ "ambientTemp": ScaleFactor.SCALE_10,
185
+ "mosTemp": ScaleFactor.SCALE_10,
186
+ # Charge/Discharge Reference Values (÷10, consistent with battery current scaling)
187
+ "batChargeMaxCur": ScaleFactor.SCALE_10, # 2000 → 200.0A
188
+ "batChargeVoltRef": ScaleFactor.SCALE_10, # 560 → 56.0V
189
+ # Percentages - NO SCALING
190
+ "soc": ScaleFactor.SCALE_NONE,
191
+ "soh": ScaleFactor.SCALE_NONE,
192
+ "currentCapacityPercent": ScaleFactor.SCALE_NONE,
193
+ # Capacity (direct Ah)
194
+ "currentRemainCapacity": ScaleFactor.SCALE_NONE,
195
+ "currentFullCapacity": ScaleFactor.SCALE_NONE,
196
+ "maxBatteryCharge": ScaleFactor.SCALE_NONE,
197
+ # Cycle Count - NO SCALING
198
+ "cycleCnt": ScaleFactor.SCALE_NONE,
199
+ # Cell Numbers - NO SCALING (integer indices)
200
+ "batMaxCellNumTemp": ScaleFactor.SCALE_NONE,
201
+ "batMinCellNumTemp": ScaleFactor.SCALE_NONE,
202
+ "batMaxCellNumVolt": ScaleFactor.SCALE_NONE,
203
+ "batMinCellNumVolt": ScaleFactor.SCALE_NONE,
204
+ }
205
+
206
+
207
+ # ============================================================================
208
+ # GRIDBOSS (MIDBOX) RUNTIME DATA SCALING
209
+ # ============================================================================
210
+ # Source: MIDBoxRuntime model from getMidboxRuntime endpoint
211
+ # NOTE: GridBOSS has different scaling than standard inverters
212
+
213
+ GRIDBOSS_RUNTIME_SCALING: dict[str, ScaleFactor] = {
214
+ # Voltages (÷10)
215
+ "gridVoltageR": ScaleFactor.SCALE_10,
216
+ "gridVoltageS": ScaleFactor.SCALE_10,
217
+ "gridVoltageT": ScaleFactor.SCALE_10,
218
+ "loadVoltageR": ScaleFactor.SCALE_10,
219
+ "loadVoltageS": ScaleFactor.SCALE_10,
220
+ "loadVoltageT": ScaleFactor.SCALE_10,
221
+ "genVoltageR": ScaleFactor.SCALE_10,
222
+ "genVoltageS": ScaleFactor.SCALE_10,
223
+ "genVoltageT": ScaleFactor.SCALE_10,
224
+ # Currents (÷10: Different from standard inverter!)
225
+ "gridCurrentR": ScaleFactor.SCALE_10,
226
+ "gridCurrentS": ScaleFactor.SCALE_10,
227
+ "gridCurrentT": ScaleFactor.SCALE_10,
228
+ "loadCurrentR": ScaleFactor.SCALE_10,
229
+ "loadCurrentS": ScaleFactor.SCALE_10,
230
+ "loadCurrentT": ScaleFactor.SCALE_10,
231
+ # Frequency (÷100)
232
+ "gridFrequency": ScaleFactor.SCALE_100,
233
+ "loadFrequency": ScaleFactor.SCALE_100,
234
+ "genFrequency": ScaleFactor.SCALE_100,
235
+ # Power - NO SCALING (direct Watts)
236
+ "gridPower": ScaleFactor.SCALE_NONE,
237
+ "loadPower": ScaleFactor.SCALE_NONE,
238
+ "smartLoadPower": ScaleFactor.SCALE_NONE,
239
+ "generatorPower": ScaleFactor.SCALE_NONE,
240
+ # Energy (÷10 for Wh)
241
+ "todayGridEnergy": ScaleFactor.SCALE_10,
242
+ "todayLoadEnergy": ScaleFactor.SCALE_10,
243
+ "totalGridEnergy": ScaleFactor.SCALE_10,
244
+ "totalLoadEnergy": ScaleFactor.SCALE_10,
245
+ }
246
+
247
+
248
+ # ============================================================================
249
+ # INVERTER OVERVIEW DATA SCALING
250
+ # ============================================================================
251
+ # Source: InverterOverviewItem from inverterOverview/list endpoint
252
+
253
+ INVERTER_OVERVIEW_SCALING: dict[str, ScaleFactor] = {
254
+ # Battery voltage (÷10: 530 → 53.0V)
255
+ "vBat": ScaleFactor.SCALE_10,
256
+ # Power - NO SCALING (direct Watts)
257
+ "ppv": ScaleFactor.SCALE_NONE,
258
+ "pCharge": ScaleFactor.SCALE_NONE,
259
+ "pDisCharge": ScaleFactor.SCALE_NONE,
260
+ "pConsumption": ScaleFactor.SCALE_NONE,
261
+ # Energy totals (÷10 for Wh)
262
+ "totalYielding": ScaleFactor.SCALE_10,
263
+ "totalDischarging": ScaleFactor.SCALE_10,
264
+ "totalExport": ScaleFactor.SCALE_10,
265
+ "totalUsage": ScaleFactor.SCALE_10,
266
+ }
267
+
268
+
269
+ # ============================================================================
270
+ # PARAMETER DATA SCALING (Hold Registers)
271
+ # ============================================================================
272
+ # Scaling for parameter values read via remoteRead endpoint
273
+
274
+ PARAMETER_SCALING: dict[str, ScaleFactor] = {
275
+ # Voltage Parameters (÷100)
276
+ "HOLD_BAT_VOLT_MAX_CHG": ScaleFactor.SCALE_100,
277
+ "HOLD_BAT_VOLT_MIN_CHG": ScaleFactor.SCALE_100,
278
+ "HOLD_BAT_VOLT_MAX_DISCHG": ScaleFactor.SCALE_100,
279
+ "HOLD_BAT_VOLT_MIN_DISCHG": ScaleFactor.SCALE_100,
280
+ "HOLD_GRID_VOLT_HIGH_1": ScaleFactor.SCALE_10,
281
+ "HOLD_GRID_VOLT_LOW_1": ScaleFactor.SCALE_10,
282
+ "HOLD_LEAD_ACID_CHARGE_VOLT_REF": ScaleFactor.SCALE_100,
283
+ "HOLD_LEAD_ACID_DISCHARGE_CUT_OFF_VOLT": ScaleFactor.SCALE_100,
284
+ "HOLD_EQUALIZATION_VOLTAGE": ScaleFactor.SCALE_100,
285
+ "HOLD_FLOATING_VOLTAGE": ScaleFactor.SCALE_100,
286
+ "HOLD_EPS_VOLT_SET": ScaleFactor.SCALE_10,
287
+ # Current Parameters (÷10)
288
+ "HOLD_MAX_CHG_CURR": ScaleFactor.SCALE_10,
289
+ "HOLD_MAX_DISCHG_CURR": ScaleFactor.SCALE_10,
290
+ "HOLD_AC_CHARGE_BATTERY_CURRENT": ScaleFactor.SCALE_10,
291
+ "OFF_GRID_HOLD_MAX_GEN_CHG_BAT_CURR": ScaleFactor.SCALE_10,
292
+ # Frequency Parameters (÷100)
293
+ "HOLD_GRID_FREQ_HIGH_1": ScaleFactor.SCALE_100,
294
+ "HOLD_GRID_FREQ_LOW_1": ScaleFactor.SCALE_100,
295
+ "HOLD_EPS_FREQ_SET": ScaleFactor.SCALE_100,
296
+ # Power Parameters (direct Watts or percentage)
297
+ "HOLD_AC_CHARGE_POWER_CMD": ScaleFactor.SCALE_NONE, # Watts
298
+ "HOLD_DISCHG_POWER_CMD": ScaleFactor.SCALE_NONE, # Percentage (0-100)
299
+ "HOLD_FEED_IN_GRID_POWER_PERCENT": ScaleFactor.SCALE_NONE, # Percentage
300
+ # SOC Parameters (percentage, no scaling)
301
+ "HOLD_AC_CHARGE_SOC_LIMIT": ScaleFactor.SCALE_NONE,
302
+ "HOLD_DISCHG_CUT_OFF_SOC_EOD": ScaleFactor.SCALE_NONE,
303
+ "HOLD_SOC_LOW_LIMIT_EPS_DISCHG": ScaleFactor.SCALE_NONE,
304
+ "HOLD_AC_CHARGE_START_BATTERY_SOC": ScaleFactor.SCALE_NONE,
305
+ "HOLD_AC_CHARGE_END_BATTERY_SOC": ScaleFactor.SCALE_NONE,
306
+ # Time Parameters (no scaling - hours/minutes)
307
+ "HOLD_AC_CHARGE_START_HOUR_1": ScaleFactor.SCALE_NONE,
308
+ "HOLD_AC_CHARGE_START_MIN_1": ScaleFactor.SCALE_NONE,
309
+ "HOLD_AC_CHARGE_END_HOUR_1": ScaleFactor.SCALE_NONE,
310
+ "HOLD_AC_CHARGE_END_MIN_1": ScaleFactor.SCALE_NONE,
311
+ }
312
+
313
+
314
+ # ============================================================================
315
+ # SCALING HELPER FUNCTIONS
316
+ # ============================================================================
317
+
318
+
319
+ def apply_scale(value: int | float, scale_factor: ScaleFactor) -> float:
320
+ """Apply scaling factor to a value.
321
+
322
+ Args:
323
+ value: Raw value from API
324
+ scale_factor: ScaleFactor enum indicating how to scale
325
+
326
+ Returns:
327
+ Scaled floating-point value
328
+
329
+ Example:
330
+ >>> apply_scale(5300, ScaleFactor.SCALE_10)
331
+ 530.0
332
+ >>> apply_scale(3317, ScaleFactor.SCALE_1000)
333
+ 3.317
334
+ """
335
+ if scale_factor == ScaleFactor.SCALE_NONE:
336
+ return float(value)
337
+ return float(value) / float(scale_factor.value)
338
+
339
+
340
+ def get_precision(scale_factor: ScaleFactor) -> int:
341
+ """Get decimal precision from a scale factor.
342
+
343
+ Args:
344
+ scale_factor: ScaleFactor enum
345
+
346
+ Returns:
347
+ Number of decimal places (0 for SCALE_NONE, 1 for SCALE_10, etc.)
348
+
349
+ Example:
350
+ >>> get_precision(ScaleFactor.SCALE_10)
351
+ 1
352
+ >>> get_precision(ScaleFactor.SCALE_1000)
353
+ 3
354
+ """
355
+ if scale_factor == ScaleFactor.SCALE_NONE:
356
+ return 0
357
+ # log10 of scale factor gives decimal places
358
+ import math
359
+
360
+ return int(math.log10(scale_factor.value))
361
+
362
+
363
+ def get_battery_field_precision(field_name: str) -> int:
364
+ """Get decimal precision for a battery module field.
365
+
366
+ Args:
367
+ field_name: Field name from BatteryModule model
368
+
369
+ Returns:
370
+ Number of decimal places for that field
371
+
372
+ Example:
373
+ >>> get_battery_field_precision("batMaxCellVoltage")
374
+ 3
375
+ >>> get_battery_field_precision("current")
376
+ 1
377
+ """
378
+ if field_name not in BATTERY_MODULE_SCALING:
379
+ return 0
380
+ return get_precision(BATTERY_MODULE_SCALING[field_name])
381
+
382
+
383
+ def _get_scaling_for_field(
384
+ field_name: str,
385
+ data_type: Literal[
386
+ "runtime", "energy", "battery_bank", "battery_module", "gridboss", "overview", "parameter"
387
+ ],
388
+ ) -> ScaleFactor:
389
+ """Get the appropriate scaling factor for a field.
390
+
391
+ This is an internal function. External users should use the data type-specific
392
+ convenience functions instead (e.g., scale_runtime_value, scale_battery_value).
393
+
394
+ Args:
395
+ field_name: Name of the field (e.g., "vpv1", "totalVoltage")
396
+ data_type: Type of data source
397
+
398
+ Returns:
399
+ ScaleFactor enum indicating how to scale the value
400
+
401
+ Raises:
402
+ KeyError: If field_name not found in the specified data type
403
+
404
+ Example:
405
+ >>> scale = _get_scaling_for_field("vpv1", "runtime")
406
+ >>> apply_scale(5100, scale)
407
+ 510.0
408
+ """
409
+ scaling_map = {
410
+ "runtime": INVERTER_RUNTIME_SCALING,
411
+ "energy": ENERGY_INFO_SCALING,
412
+ "battery_bank": BATTERY_BANK_SCALING,
413
+ "battery_module": BATTERY_MODULE_SCALING,
414
+ "gridboss": GRIDBOSS_RUNTIME_SCALING,
415
+ "overview": INVERTER_OVERVIEW_SCALING,
416
+ "parameter": PARAMETER_SCALING,
417
+ }
418
+
419
+ return scaling_map[data_type][field_name]
420
+
421
+
422
+ def scale_runtime_value(field_name: str, value: int | float) -> float:
423
+ """Convenience function to scale inverter runtime values.
424
+
425
+ Args:
426
+ field_name: Field name from InverterRuntime model
427
+ value: Raw API value
428
+
429
+ Returns:
430
+ Scaled value
431
+ """
432
+ if field_name not in INVERTER_RUNTIME_SCALING:
433
+ # Field doesn't need scaling (or unknown field)
434
+ return float(value)
435
+ return apply_scale(value, INVERTER_RUNTIME_SCALING[field_name])
436
+
437
+
438
+ def scale_battery_value(field_name: str, value: int | float) -> float:
439
+ """Convenience function to scale battery module values.
440
+
441
+ Args:
442
+ field_name: Field name from BatteryModule model
443
+ value: Raw API value
444
+
445
+ Returns:
446
+ Scaled value
447
+ """
448
+ if field_name not in BATTERY_MODULE_SCALING:
449
+ return float(value)
450
+ return apply_scale(value, BATTERY_MODULE_SCALING[field_name])
451
+
452
+
453
+ def scale_energy_value(field_name: str, value: int | float, to_kwh: bool = True) -> float:
454
+ """Convenience function to scale energy values.
455
+
456
+ Args:
457
+ field_name: Field name from EnergyInfo model
458
+ value: Raw API value (in 0.1 kWh units)
459
+ to_kwh: If True, return kWh; if False, return Wh
460
+
461
+ Returns:
462
+ Scaled value in kWh (if to_kwh=True) or Wh
463
+
464
+ Example:
465
+ >>> scale_energy_value("todayYielding", 184, to_kwh=True)
466
+ 18.4 # kWh
467
+ >>> scale_energy_value("todayYielding", 184, to_kwh=False)
468
+ 18400.0 # Wh
469
+ """
470
+ if field_name not in ENERGY_INFO_SCALING:
471
+ return float(value)
472
+
473
+ # Apply API scaling (÷10 to get kWh directly - API uses 0.1 kWh units)
474
+ kwh_value = apply_scale(value, ENERGY_INFO_SCALING[field_name])
475
+
476
+ # Convert to Wh if requested
477
+ if not to_kwh:
478
+ return kwh_value * 1000.0
479
+ return kwh_value
@@ -0,0 +1,32 @@
1
+ """Device hierarchy for pylxpweb.
2
+
3
+ This module provides object-oriented access to stations, inverters,
4
+ batteries, and MID devices.
5
+ """
6
+
7
+ from .base import BaseDevice
8
+ from .battery import Battery
9
+ from .battery_bank import BatteryBank
10
+ from .inverters import BaseInverter, GenericInverter, HybridInverter
11
+ from .mid_device import MIDDevice
12
+ from .models import DeviceClass, DeviceInfo, Entity, EntityCategory, StateClass
13
+ from .parallel_group import ParallelGroup
14
+ from .station import Location, Station
15
+
16
+ __all__ = [
17
+ "BaseDevice",
18
+ "Battery",
19
+ "BatteryBank",
20
+ "BaseInverter",
21
+ "GenericInverter",
22
+ "HybridInverter",
23
+ "MIDDevice",
24
+ "DeviceInfo",
25
+ "Entity",
26
+ "DeviceClass",
27
+ "StateClass",
28
+ "EntityCategory",
29
+ "Location",
30
+ "Station",
31
+ "ParallelGroup",
32
+ ]