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,192 @@
1
+ """Generic inverter implementation for standard EG4/Luxpower models.
2
+
3
+ This module provides the GenericInverter class that handles all standard
4
+ inverter models including FlexBOSS21, FlexBOSS18, 18KPV, 12KPV, and XP.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from ..models import DeviceClass, Entity, StateClass
12
+ from .base import BaseInverter
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+
18
+ class GenericInverter(BaseInverter):
19
+ """Generic inverter for standard EG4/Luxpower models.
20
+
21
+ Handles all standard inverter models:
22
+ - FlexBOSS21 (21kW)
23
+ - FlexBOSS18 (18kW)
24
+ - 18KPV (18kW)
25
+ - 12KPV (12kW)
26
+ - XP (various power ratings)
27
+
28
+ Example:
29
+ ```python
30
+ inverter = GenericInverter(
31
+ client=client,
32
+ serial_number="1234567890",
33
+ model="FlexBOSS21"
34
+ )
35
+ await inverter.refresh()
36
+ print(f"Power: {inverter.power_output}W")
37
+ print(f"SOC: {inverter.battery_soc}%")
38
+ ```
39
+ """
40
+
41
+ def to_entities(self) -> list[Entity]:
42
+ """Generate entities for this inverter.
43
+
44
+ Returns:
45
+ List of Entity objects representing sensors for this inverter.
46
+ """
47
+ entities = []
48
+
49
+ # Power sensors
50
+ if self._runtime:
51
+ # AC Power Output
52
+ entities.append(
53
+ Entity(
54
+ unique_id=f"{self.serial_number}_power",
55
+ name=f"{self.model} {self.serial_number} Power",
56
+ device_class=DeviceClass.POWER,
57
+ state_class=StateClass.MEASUREMENT,
58
+ unit_of_measurement="W",
59
+ value=self.power_output,
60
+ )
61
+ )
62
+
63
+ # Battery SOC
64
+ if self.battery_soc is not None:
65
+ entities.append(
66
+ Entity(
67
+ unique_id=f"{self.serial_number}_soc",
68
+ name=f"{self.model} {self.serial_number} Battery SOC",
69
+ device_class=DeviceClass.BATTERY,
70
+ state_class=StateClass.MEASUREMENT,
71
+ unit_of_measurement="%",
72
+ value=self.battery_soc,
73
+ )
74
+ )
75
+
76
+ # Battery Voltage
77
+ if hasattr(self._runtime, "vBat") and self._runtime.vBat:
78
+ entities.append(
79
+ Entity(
80
+ unique_id=f"{self.serial_number}_battery_voltage",
81
+ name=f"{self.model} {self.serial_number} Battery Voltage",
82
+ device_class=DeviceClass.VOLTAGE,
83
+ state_class=StateClass.MEASUREMENT,
84
+ unit_of_measurement="V",
85
+ value=self._runtime.vBat / 100.0, # Scaled value
86
+ )
87
+ )
88
+
89
+ # PV Power
90
+ if hasattr(self._runtime, "ppv"):
91
+ entities.append(
92
+ Entity(
93
+ unique_id=f"{self.serial_number}_pv_power",
94
+ name=f"{self.model} {self.serial_number} PV Power",
95
+ device_class=DeviceClass.POWER,
96
+ state_class=StateClass.MEASUREMENT,
97
+ unit_of_measurement="W",
98
+ value=self._runtime.ppv,
99
+ )
100
+ )
101
+
102
+ # Grid Power
103
+ if hasattr(self._runtime, "pToGrid"):
104
+ entities.append(
105
+ Entity(
106
+ unique_id=f"{self.serial_number}_grid_power",
107
+ name=f"{self.model} {self.serial_number} Grid Power",
108
+ device_class=DeviceClass.POWER,
109
+ state_class=StateClass.MEASUREMENT,
110
+ unit_of_measurement="W",
111
+ value=self._runtime.pToGrid,
112
+ )
113
+ )
114
+
115
+ # Load Power
116
+ if hasattr(self._runtime, "pToUser"):
117
+ entities.append(
118
+ Entity(
119
+ unique_id=f"{self.serial_number}_load_power",
120
+ name=f"{self.model} {self.serial_number} Load Power",
121
+ device_class=DeviceClass.POWER,
122
+ state_class=StateClass.MEASUREMENT,
123
+ unit_of_measurement="W",
124
+ value=self._runtime.pToUser,
125
+ )
126
+ )
127
+
128
+ # Battery Charge/Discharge Power
129
+ if hasattr(self._runtime, "batPower"):
130
+ entities.append(
131
+ Entity(
132
+ unique_id=f"{self.serial_number}_battery_power",
133
+ name=f"{self.model} {self.serial_number} Battery Power",
134
+ device_class=DeviceClass.POWER,
135
+ state_class=StateClass.MEASUREMENT,
136
+ unit_of_measurement="W",
137
+ value=self._runtime.batPower,
138
+ )
139
+ )
140
+
141
+ # Temperature sensors
142
+ if hasattr(self._runtime, "tinner"):
143
+ entities.append(
144
+ Entity(
145
+ unique_id=f"{self.serial_number}_temp_internal",
146
+ name=f"{self.model} {self.serial_number} Internal Temperature",
147
+ device_class=DeviceClass.TEMPERATURE,
148
+ state_class=StateClass.MEASUREMENT,
149
+ unit_of_measurement="°C",
150
+ value=self._runtime.tinner,
151
+ )
152
+ )
153
+
154
+ if hasattr(self._runtime, "tBat"):
155
+ entities.append(
156
+ Entity(
157
+ unique_id=f"{self.serial_number}_temp_battery",
158
+ name=f"{self.model} {self.serial_number} Battery Temperature",
159
+ device_class=DeviceClass.TEMPERATURE,
160
+ state_class=StateClass.MEASUREMENT,
161
+ unit_of_measurement="°C",
162
+ value=self._runtime.tBat,
163
+ )
164
+ )
165
+
166
+ # Energy sensors
167
+ if self._energy:
168
+ # Today's Production
169
+ entities.append(
170
+ Entity(
171
+ unique_id=f"{self.serial_number}_energy_today",
172
+ name=f"{self.model} {self.serial_number} Energy Today",
173
+ device_class=DeviceClass.ENERGY,
174
+ state_class=StateClass.TOTAL_INCREASING,
175
+ unit_of_measurement="kWh",
176
+ value=self.total_energy_today,
177
+ )
178
+ )
179
+
180
+ # Lifetime Production
181
+ entities.append(
182
+ Entity(
183
+ unique_id=f"{self.serial_number}_energy_total",
184
+ name=f"{self.model} {self.serial_number} Energy Total",
185
+ device_class=DeviceClass.ENERGY,
186
+ state_class=StateClass.TOTAL_INCREASING,
187
+ unit_of_measurement="kWh",
188
+ value=self.total_energy_lifetime,
189
+ )
190
+ )
191
+
192
+ return entities
@@ -0,0 +1,274 @@
1
+ """Hybrid inverter implementation for grid-tied models with battery storage.
2
+
3
+ This module provides the HybridInverter class for hybrid inverters that support:
4
+ - AC charging from grid
5
+ - Forced charge/discharge
6
+ - EPS (backup) mode
7
+ - Time-of-use scheduling
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from .generic import GenericInverter
15
+
16
+ if TYPE_CHECKING:
17
+ pass
18
+
19
+
20
+ class HybridInverter(GenericInverter):
21
+ """Hybrid inverter with grid-tied and battery backup capabilities.
22
+
23
+ Extends GenericInverter with hybrid-specific controls:
24
+ - AC charging from grid
25
+ - Forced charge/discharge
26
+ - EPS/backup mode enable/disable
27
+ - Time-based charge/discharge scheduling
28
+
29
+ Suitable for models: FlexBOSS21, FlexBOSS18, 18KPV, 12KPV
30
+
31
+ Example:
32
+ ```python
33
+ inverter = HybridInverter(
34
+ client=client,
35
+ serial_number="1234567890",
36
+ model="18KPV"
37
+ )
38
+
39
+ # Enable AC charging at 50% power up to 100% SOC
40
+ await inverter.set_ac_charge(enabled=True, power_percent=50, soc_limit=100)
41
+
42
+ # Enable EPS backup mode
43
+ await inverter.set_eps_enabled(True)
44
+
45
+ # Set forced charge
46
+ await inverter.set_forced_charge(True)
47
+ ```
48
+ """
49
+
50
+ # ============================================================================
51
+ # Hybrid-Specific Control Operations
52
+ # ============================================================================
53
+
54
+ async def get_ac_charge_settings(self) -> dict[str, int | bool]:
55
+ """Get AC charge configuration.
56
+
57
+ Returns:
58
+ Dictionary with:
59
+ - enabled: AC charge function enabled
60
+ - power_percent: Charge power (0-100%)
61
+ - soc_limit: Target SOC (0-100%)
62
+ - schedule1_enabled: Time schedule 1 enabled
63
+ - schedule2_enabled: Time schedule 2 enabled
64
+
65
+ Example:
66
+ >>> settings = await inverter.get_ac_charge_settings()
67
+ >>> settings
68
+ {
69
+ 'enabled': True,
70
+ 'power_percent': 50,
71
+ 'soc_limit': 100,
72
+ 'schedule1_enabled': True,
73
+ 'schedule2_enabled': False
74
+ }
75
+ """
76
+ from pylxpweb.constants import (
77
+ FUNC_EN_BIT_AC_CHARGE_EN,
78
+ FUNC_EN_REGISTER,
79
+ HOLD_AC_CHARGE_POWER_CMD,
80
+ )
81
+
82
+ # Read function enable register for AC charge bit
83
+ func_params = await self.read_parameters(FUNC_EN_REGISTER, 1)
84
+ func_value = func_params.get(f"reg_{FUNC_EN_REGISTER}", 0)
85
+ ac_charge_enabled = bool(func_value & (1 << FUNC_EN_BIT_AC_CHARGE_EN))
86
+
87
+ # Read AC charge parameters
88
+ ac_params = await self.read_parameters(HOLD_AC_CHARGE_POWER_CMD, 8)
89
+
90
+ return {
91
+ "enabled": ac_charge_enabled,
92
+ "power_percent": ac_params.get("HOLD_AC_CHARGE_POWER_CMD", 0),
93
+ "soc_limit": ac_params.get("HOLD_AC_CHARGE_SOC_LIMIT"),
94
+ "schedule1_enabled": bool(ac_params.get("HOLD_AC_CHARGE_ENABLE_1", 0)),
95
+ "schedule2_enabled": bool(ac_params.get("HOLD_AC_CHARGE_ENABLE_2", 0)),
96
+ }
97
+
98
+ async def set_ac_charge(
99
+ self, enabled: bool, power_percent: int | None = None, soc_limit: int | None = None
100
+ ) -> bool:
101
+ """Configure AC charging from grid.
102
+
103
+ Args:
104
+ enabled: Enable AC charging
105
+ power_percent: Charge power percentage (0-100), optional
106
+ soc_limit: Target SOC percentage (0-100), optional
107
+
108
+ Returns:
109
+ True if successful
110
+
111
+ Example:
112
+ >>> # Enable AC charge at 50% power to 100% SOC
113
+ >>> await inverter.set_ac_charge(True, power_percent=50, soc_limit=100)
114
+ True
115
+
116
+ >>> # Disable AC charge
117
+ >>> await inverter.set_ac_charge(False)
118
+ True
119
+ """
120
+ from pylxpweb.constants import (
121
+ FUNC_EN_BIT_AC_CHARGE_EN,
122
+ FUNC_EN_REGISTER,
123
+ HOLD_AC_CHARGE_POWER_CMD,
124
+ HOLD_AC_CHARGE_SOC_LIMIT,
125
+ )
126
+
127
+ # Validate parameters first (before any API calls)
128
+ if power_percent is not None and not 0 <= power_percent <= 100:
129
+ raise ValueError("power_percent must be between 0 and 100")
130
+
131
+ if soc_limit is not None and not 0 <= soc_limit <= 100:
132
+ raise ValueError("soc_limit must be between 0 and 100")
133
+
134
+ # Update function enable bit
135
+ func_params = await self.read_parameters(FUNC_EN_REGISTER, 1)
136
+ current_func = func_params.get(f"reg_{FUNC_EN_REGISTER}", 0)
137
+
138
+ if enabled:
139
+ new_func = current_func | (1 << FUNC_EN_BIT_AC_CHARGE_EN)
140
+ else:
141
+ new_func = current_func & ~(1 << FUNC_EN_BIT_AC_CHARGE_EN)
142
+
143
+ params_to_write = {FUNC_EN_REGISTER: new_func}
144
+
145
+ # Add power and SOC limit if provided (already validated)
146
+ if power_percent is not None:
147
+ params_to_write[HOLD_AC_CHARGE_POWER_CMD] = power_percent
148
+
149
+ if soc_limit is not None:
150
+ params_to_write[HOLD_AC_CHARGE_SOC_LIMIT] = soc_limit
151
+
152
+ return await self.write_parameters(params_to_write)
153
+
154
+ async def set_eps_enabled(self, enabled: bool) -> bool:
155
+ """Enable or disable EPS (backup/off-grid) mode.
156
+
157
+ Args:
158
+ enabled: True to enable EPS mode, False to disable
159
+
160
+ Returns:
161
+ True if successful
162
+
163
+ Example:
164
+ >>> await inverter.set_eps_enabled(True)
165
+ True
166
+ """
167
+ from pylxpweb.constants import FUNC_EN_BIT_EPS_EN, FUNC_EN_REGISTER
168
+
169
+ # Read current function enable register
170
+ params = await self.read_parameters(FUNC_EN_REGISTER, 1)
171
+ current_value = params.get(f"reg_{FUNC_EN_REGISTER}", 0)
172
+
173
+ if enabled:
174
+ new_value = current_value | (1 << FUNC_EN_BIT_EPS_EN)
175
+ else:
176
+ new_value = current_value & ~(1 << FUNC_EN_BIT_EPS_EN)
177
+
178
+ return await self.write_parameters({FUNC_EN_REGISTER: new_value})
179
+
180
+ async def set_forced_charge(self, enabled: bool) -> bool:
181
+ """Enable or disable forced charge mode.
182
+
183
+ Forces inverter to charge batteries regardless of time schedule.
184
+
185
+ Args:
186
+ enabled: True to enable forced charge, False to disable
187
+
188
+ Returns:
189
+ True if successful
190
+
191
+ Example:
192
+ >>> await inverter.set_forced_charge(True)
193
+ True
194
+ """
195
+ from pylxpweb.constants import FUNC_EN_BIT_FORCED_CHG_EN, FUNC_EN_REGISTER
196
+
197
+ params = await self.read_parameters(FUNC_EN_REGISTER, 1)
198
+ current_value = params.get(f"reg_{FUNC_EN_REGISTER}", 0)
199
+
200
+ if enabled:
201
+ new_value = current_value | (1 << FUNC_EN_BIT_FORCED_CHG_EN)
202
+ else:
203
+ new_value = current_value & ~(1 << FUNC_EN_BIT_FORCED_CHG_EN)
204
+
205
+ return await self.write_parameters({FUNC_EN_REGISTER: new_value})
206
+
207
+ async def set_forced_discharge(self, enabled: bool) -> bool:
208
+ """Enable or disable forced discharge mode.
209
+
210
+ Forces inverter to discharge batteries regardless of time schedule.
211
+
212
+ Args:
213
+ enabled: True to enable forced discharge, False to disable
214
+
215
+ Returns:
216
+ True if successful
217
+
218
+ Example:
219
+ >>> await inverter.set_forced_discharge(True)
220
+ True
221
+ """
222
+ from pylxpweb.constants import FUNC_EN_BIT_FORCED_DISCHG_EN, FUNC_EN_REGISTER
223
+
224
+ params = await self.read_parameters(FUNC_EN_REGISTER, 1)
225
+ current_value = params.get(f"reg_{FUNC_EN_REGISTER}", 0)
226
+
227
+ if enabled:
228
+ new_value = current_value | (1 << FUNC_EN_BIT_FORCED_DISCHG_EN)
229
+ else:
230
+ new_value = current_value & ~(1 << FUNC_EN_BIT_FORCED_DISCHG_EN)
231
+
232
+ return await self.write_parameters({FUNC_EN_REGISTER: new_value})
233
+
234
+ async def get_charge_discharge_power(self) -> dict[str, int]:
235
+ """Get charge and discharge power settings.
236
+
237
+ Returns:
238
+ Dictionary with:
239
+ - charge_power_percent: AC charge power (0-100%)
240
+ - discharge_power_percent: Discharge power (0-100%)
241
+
242
+ Example:
243
+ >>> settings = await inverter.get_charge_discharge_power()
244
+ >>> settings
245
+ {'charge_power_percent': 50, 'discharge_power_percent': 100}
246
+ """
247
+ from pylxpweb.constants import HOLD_AC_CHARGE_POWER_CMD
248
+
249
+ params = await self.read_parameters(HOLD_AC_CHARGE_POWER_CMD, 9)
250
+
251
+ return {
252
+ "charge_power_percent": params.get("HOLD_AC_CHARGE_POWER_CMD", 0),
253
+ "discharge_power_percent": params.get("HOLD_DISCHG_POWER_CMD"),
254
+ }
255
+
256
+ async def set_discharge_power(self, power_percent: int) -> bool:
257
+ """Set battery discharge power limit.
258
+
259
+ Args:
260
+ power_percent: Discharge power percentage (0-100)
261
+
262
+ Returns:
263
+ True if successful
264
+
265
+ Example:
266
+ >>> await inverter.set_discharge_power(80)
267
+ True
268
+ """
269
+ from pylxpweb.constants import HOLD_DISCHG_POWER_CMD
270
+
271
+ if not 0 <= power_percent <= 100:
272
+ raise ValueError("power_percent must be between 0 and 100")
273
+
274
+ return await self.write_parameters({HOLD_DISCHG_POWER_CMD: power_percent})
@@ -0,0 +1,183 @@
1
+ """MID Device (GridBOSS) module for grid management and load control.
2
+
3
+ This module provides the MIDDevice class for GridBOSS devices that handle
4
+ grid interconnection, UPS functionality, smart loads, and AC coupling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import datetime
11
+ from typing import TYPE_CHECKING
12
+
13
+ from pylxpweb.exceptions import LuxpowerAPIError, LuxpowerConnectionError, LuxpowerDeviceError
14
+
15
+ from ._firmware_update_mixin import FirmwareUpdateMixin
16
+ from ._mid_runtime_properties import MIDRuntimePropertiesMixin
17
+ from .base import BaseDevice
18
+ from .models import DeviceClass, DeviceInfo, Entity, StateClass
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+ if TYPE_CHECKING:
23
+ from pylxpweb import LuxpowerClient
24
+ from pylxpweb.models import MidboxRuntime
25
+
26
+
27
+ class MIDDevice(FirmwareUpdateMixin, MIDRuntimePropertiesMixin, BaseDevice):
28
+ """Represents a GridBOSS/MID device for grid management.
29
+
30
+ GridBOSS devices handle:
31
+ - Grid interconnection and UPS functionality
32
+ - Smart load management (4 configurable outputs)
33
+ - AC coupling for additional inverters/generators
34
+ - Load monitoring and control
35
+
36
+ Example:
37
+ ```python
38
+ # MIDDevice is typically created from parallel group data
39
+ mid_device = MIDDevice(
40
+ client=client,
41
+ serial_number="1234567890",
42
+ model="GridBOSS"
43
+ )
44
+ await mid_device.refresh()
45
+ print(f"Grid Power: {mid_device.grid_power}W")
46
+ print(f"UPS Power: {mid_device.ups_power}W")
47
+ ```
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ client: LuxpowerClient,
53
+ serial_number: str,
54
+ model: str = "GridBOSS",
55
+ ) -> None:
56
+ """Initialize MID device.
57
+
58
+ Args:
59
+ client: LuxpowerClient instance for API access
60
+ serial_number: MID device serial number (10-digit)
61
+ model: Device model name (default: "GridBOSS")
62
+ """
63
+ super().__init__(client, serial_number, model)
64
+
65
+ # Runtime data (private - use properties for access)
66
+ self._runtime: MidboxRuntime | None = None
67
+
68
+ # Initialize firmware update detection (from FirmwareUpdateMixin)
69
+ self._init_firmware_update_cache()
70
+
71
+ async def refresh(self) -> None:
72
+ """Refresh MID device runtime data from API."""
73
+ try:
74
+ runtime_data = await self._client.api.devices.get_midbox_runtime(self.serial_number)
75
+ self._runtime = runtime_data
76
+ self._last_refresh = datetime.now()
77
+ except (LuxpowerAPIError, LuxpowerConnectionError, LuxpowerDeviceError) as err:
78
+ # Graceful error handling - keep existing cached data
79
+ _LOGGER.debug("Failed to fetch MID device runtime for %s: %s", self.serial_number, err)
80
+
81
+ # All properties are provided by MIDRuntimePropertiesMixin
82
+
83
+ def to_device_info(self) -> DeviceInfo:
84
+ """Convert to device info model.
85
+
86
+ Returns:
87
+ DeviceInfo with MID device metadata.
88
+ """
89
+ return DeviceInfo(
90
+ identifiers={("pylxpweb", f"mid_{self.serial_number}")},
91
+ name=f"GridBOSS {self.serial_number}",
92
+ manufacturer="EG4/Luxpower",
93
+ model=self.model,
94
+ sw_version=self.firmware_version,
95
+ )
96
+
97
+ def to_entities(self) -> list[Entity]:
98
+ """Generate entities for this MID device.
99
+
100
+ Returns:
101
+ List of Entity objects for GridBOSS monitoring.
102
+
103
+ Note: This implementation focuses on core grid/UPS monitoring.
104
+ Future versions will add smart loads, AC coupling, and generator sensors.
105
+ """
106
+ if self._runtime is None:
107
+ return []
108
+
109
+ entities = []
110
+
111
+ # Grid Voltage
112
+ entities.append(
113
+ Entity(
114
+ unique_id=f"{self.serial_number}_grid_voltage",
115
+ name=f"{self.model} {self.serial_number} Grid Voltage",
116
+ device_class=DeviceClass.VOLTAGE,
117
+ state_class=StateClass.MEASUREMENT,
118
+ unit_of_measurement="V",
119
+ value=self.grid_voltage,
120
+ )
121
+ )
122
+
123
+ # Grid Power
124
+ entities.append(
125
+ Entity(
126
+ unique_id=f"{self.serial_number}_grid_power",
127
+ name=f"{self.model} {self.serial_number} Grid Power",
128
+ device_class=DeviceClass.POWER,
129
+ state_class=StateClass.MEASUREMENT,
130
+ unit_of_measurement="W",
131
+ value=self.grid_power,
132
+ )
133
+ )
134
+
135
+ # UPS Voltage
136
+ entities.append(
137
+ Entity(
138
+ unique_id=f"{self.serial_number}_ups_voltage",
139
+ name=f"{self.model} {self.serial_number} UPS Voltage",
140
+ device_class=DeviceClass.VOLTAGE,
141
+ state_class=StateClass.MEASUREMENT,
142
+ unit_of_measurement="V",
143
+ value=self.ups_voltage,
144
+ )
145
+ )
146
+
147
+ # UPS Power
148
+ entities.append(
149
+ Entity(
150
+ unique_id=f"{self.serial_number}_ups_power",
151
+ name=f"{self.model} {self.serial_number} UPS Power",
152
+ device_class=DeviceClass.POWER,
153
+ state_class=StateClass.MEASUREMENT,
154
+ unit_of_measurement="W",
155
+ value=self.ups_power,
156
+ )
157
+ )
158
+
159
+ # Hybrid Power
160
+ entities.append(
161
+ Entity(
162
+ unique_id=f"{self.serial_number}_hybrid_power",
163
+ name=f"{self.model} {self.serial_number} Hybrid Power",
164
+ device_class=DeviceClass.POWER,
165
+ state_class=StateClass.MEASUREMENT,
166
+ unit_of_measurement="W",
167
+ value=self.hybrid_power,
168
+ )
169
+ )
170
+
171
+ # Grid Frequency
172
+ entities.append(
173
+ Entity(
174
+ unique_id=f"{self.serial_number}_grid_frequency",
175
+ name=f"{self.model} {self.serial_number} Grid Frequency",
176
+ device_class=DeviceClass.FREQUENCY,
177
+ state_class=StateClass.MEASUREMENT,
178
+ unit_of_measurement="Hz",
179
+ value=self.grid_frequency,
180
+ )
181
+ )
182
+
183
+ return entities