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.
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 +1427 -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 +364 -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 +708 -41
  34. pylxpweb/transports/__init__.py +78 -0
  35. pylxpweb/transports/capabilities.py +101 -0
  36. pylxpweb/transports/data.py +501 -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 +617 -0
  41. pylxpweb/transports/protocol.py +217 -0
  42. {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.2.dist-info}/METADATA +130 -85
  43. pylxpweb-0.5.2.dist-info/RECORD +52 -0
  44. {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.2.dist-info}/WHEEL +1 -1
  45. pylxpweb-0.5.2.dist-info/entry_points.txt +3 -0
  46. pylxpweb-0.1.0.dist-info/RECORD +0 -19
@@ -0,0 +1,227 @@
1
+ """Constants and mappings for Luxpower/EG4 API.
2
+
3
+ This module contains mapping tables extracted from the EG4 web interface
4
+ to convert between human-readable API values and the enum values required
5
+ for configuration updates.
6
+
7
+ These mappings were discovered by analyzing the HTML form at:
8
+ /WManage/web/config/plant/edit/{plant_id}
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ # Timezone mappings: Human-readable (from API) → Form enum (for POST)
14
+ # Source: Analyzed all 28 timezone options from the HTML form
15
+ TIMEZONE_MAP: dict[str, str] = {
16
+ "GMT -12": "WEST12",
17
+ "GMT -11": "WEST11",
18
+ "GMT -10": "WEST10",
19
+ "GMT -9": "WEST9",
20
+ "GMT -8": "WEST8",
21
+ "GMT -7": "WEST7",
22
+ "GMT -6": "WEST6",
23
+ "GMT -5": "WEST5",
24
+ "GMT -4": "WEST4",
25
+ "GMT -3": "WEST3",
26
+ "GMT -2": "WEST2",
27
+ "GMT -1": "WEST1",
28
+ "GMT 0": "ZERO",
29
+ "GMT +1": "EAST1",
30
+ "GMT +2": "EAST2",
31
+ "GMT +3": "EAST3",
32
+ "GMT +3:30": "EAST3_30",
33
+ "GMT +4": "EAST4",
34
+ "GMT +5": "EAST5",
35
+ "GMT +5:30": "EAST5_30",
36
+ "GMT +6": "EAST6",
37
+ "GMT +6:30": "EAST6_30",
38
+ "GMT +7": "EAST7",
39
+ "GMT +8": "EAST8",
40
+ "GMT +9": "EAST9",
41
+ "GMT +10": "EAST10",
42
+ "GMT +11": "EAST11",
43
+ "GMT +12": "EAST12",
44
+ }
45
+
46
+ # Reverse mapping: Form enum → Human-readable
47
+ TIMEZONE_REVERSE_MAP: dict[str, str] = {v: k for k, v in TIMEZONE_MAP.items()}
48
+
49
+ # Country mappings: Human-readable (from API) → Form enum (for POST)
50
+ # Source: Analyzed country options from HTML form (North America region shown)
51
+ # NOTE: This list is incomplete - only shows North American countries
52
+ # Additional countries would appear based on selected continent/region
53
+ COUNTRY_MAP: dict[str, str] = {
54
+ "Canada": "CANADA",
55
+ "United States of America": "UNITED_STATES_OF_AMERICA",
56
+ "Mexico": "MEXICO",
57
+ "Greenland": "GREENLAND",
58
+ }
59
+
60
+ # Reverse mapping: Form enum → Human-readable
61
+ COUNTRY_REVERSE_MAP: dict[str, str] = {v: k for k, v in COUNTRY_MAP.items()}
62
+
63
+ # Continent mappings: Human-readable → Form enum
64
+ # Source: All 6 continent options from HTML form
65
+ CONTINENT_MAP: dict[str, str] = {
66
+ "Africa": "AFRICA",
67
+ "Asia": "ASIA",
68
+ "Europe": "EUROPE",
69
+ "North America": "NORTH_AMERICA",
70
+ "Oceania": "OCEANIA",
71
+ "South America": "SOUTH_AMERICA",
72
+ }
73
+
74
+ CONTINENT_REVERSE_MAP: dict[str, str] = {v: k for k, v in CONTINENT_MAP.items()}
75
+
76
+ # Region mappings: Human-readable → Form enum
77
+ # Source: Region options from HTML form (context: North America continent)
78
+ # NOTE: Region options are hierarchical and depend on selected continent
79
+ REGION_MAP: dict[str, str] = {
80
+ # North America regions (when continent = NORTH_AMERICA)
81
+ "Caribbean": "CARIBBEAN",
82
+ "Central America": "CENTRAL_AMERICA",
83
+ "North America": "NORTH_AMERICA",
84
+ # Additional regions would be discovered when exploring other continents
85
+ }
86
+
87
+ REGION_REVERSE_MAP: dict[str, str] = {v: k for k, v in REGION_MAP.items()}
88
+
89
+
90
+ def get_timezone_enum(human_readable: str) -> str:
91
+ """Convert human-readable timezone to API enum.
92
+
93
+ Args:
94
+ human_readable: Timezone string like "GMT -8"
95
+
96
+ Returns:
97
+ API enum like "WEST8"
98
+
99
+ Raises:
100
+ ValueError: If timezone is not recognized
101
+ """
102
+ if human_readable in TIMEZONE_MAP:
103
+ return TIMEZONE_MAP[human_readable]
104
+ raise ValueError(f"Unknown timezone: {human_readable}")
105
+
106
+
107
+ def get_country_enum(human_readable: str) -> str:
108
+ """Convert human-readable country to API enum.
109
+
110
+ Args:
111
+ human_readable: Country string like "United States of America"
112
+
113
+ Returns:
114
+ API enum like "UNITED_STATES_OF_AMERICA"
115
+
116
+ Raises:
117
+ ValueError: If country is not recognized
118
+ """
119
+ if human_readable in COUNTRY_MAP:
120
+ return COUNTRY_MAP[human_readable]
121
+ raise ValueError(f"Unknown country: {human_readable}")
122
+
123
+
124
+ def get_region_enum(human_readable: str) -> str:
125
+ """Convert human-readable region to API enum.
126
+
127
+ Args:
128
+ human_readable: Region string like "North America"
129
+
130
+ Returns:
131
+ API enum like "NORTH_AMERICA"
132
+
133
+ Raises:
134
+ ValueError: If region is not recognized
135
+ """
136
+ if human_readable in REGION_MAP:
137
+ return REGION_MAP[human_readable]
138
+ raise ValueError(f"Unknown region: {human_readable}")
139
+
140
+
141
+ def get_continent_enum(human_readable: str) -> str:
142
+ """Convert human-readable continent to API enum.
143
+
144
+ Args:
145
+ human_readable: Continent string like "North America"
146
+
147
+ Returns:
148
+ API enum like "NORTH_AMERICA"
149
+
150
+ Raises:
151
+ ValueError: If continent is not recognized
152
+ """
153
+ if human_readable in CONTINENT_MAP:
154
+ return CONTINENT_MAP[human_readable]
155
+ raise ValueError(f"Unknown continent: {human_readable}")
156
+
157
+
158
+ # Static mapping for common countries (fast path)
159
+ # This covers the most frequently used countries to avoid API calls
160
+ COUNTRY_TO_LOCATION_STATIC: dict[str, tuple[str, str]] = {
161
+ # North America
162
+ "United States of America": ("NORTH_AMERICA", "NORTH_AMERICA"),
163
+ "Canada": ("NORTH_AMERICA", "NORTH_AMERICA"),
164
+ "Mexico": ("NORTH_AMERICA", "CENTRAL_AMERICA"),
165
+ "Greenland": ("NORTH_AMERICA", "NORTH_AMERICA"),
166
+ # Europe (common)
167
+ "United Kingdom": ("EUROPE", "WESTERN_EUROPE"),
168
+ "Germany": ("EUROPE", "CENTRAL_EUROPE"),
169
+ "France": ("EUROPE", "WESTERN_EUROPE"),
170
+ "Spain": ("EUROPE", "SOUTHERN_EUROPE"),
171
+ "Italy": ("EUROPE", "SOUTHERN_EUROPE"),
172
+ "The Netherlands": ("EUROPE", "WESTERN_EUROPE"),
173
+ "Belgium": ("EUROPE", "WESTERN_EUROPE"),
174
+ "Switzerland": ("EUROPE", "CENTRAL_EUROPE"),
175
+ "Austria": ("EUROPE", "CENTRAL_EUROPE"),
176
+ "Poland": ("EUROPE", "CENTRAL_EUROPE"),
177
+ "Sweden": ("EUROPE", "NORDIC_EUROPE"),
178
+ "Norway": ("EUROPE", "NORDIC_EUROPE"),
179
+ "Denmark": ("EUROPE", "NORDIC_EUROPE"),
180
+ # Asia (common)
181
+ "China": ("ASIA", "EAST_ASIA"),
182
+ "Japan": ("ASIA", "EAST_ASIA"),
183
+ "South korea": ("ASIA", "EAST_ASIA"),
184
+ "India": ("ASIA", "SOUTH_ASIA"),
185
+ "Singapore": ("ASIA", "SOUTHEAST_ASIA"),
186
+ "Thailand": ("ASIA", "SOUTHEAST_ASIA"),
187
+ "Malaysia": ("ASIA", "SOUTHEAST_ASIA"),
188
+ "Indonesia": ("ASIA", "SOUTHEAST_ASIA"),
189
+ "Philippines": ("ASIA", "SOUTHEAST_ASIA"),
190
+ "Vietnam": ("ASIA", "SOUTHEAST_ASIA"),
191
+ # Oceania
192
+ "Australia": ("OCEANIA", "OCEANIA"),
193
+ "New Zealand": ("OCEANIA", "OCEANIA"),
194
+ # South America
195
+ "Brazil": ("SOUTH_AMERICA", "SA_EAST"),
196
+ "Argentina": ("SOUTH_AMERICA", "SA_SOUTHERN_PART"), # Note: API has "Aregntine" typo
197
+ "Chile": ("SOUTH_AMERICA", "SA_SOUTHERN_PART"),
198
+ # Africa (common)
199
+ "South Africa": ("AFRICA", "SOUTH_AFRICA"),
200
+ "Egypt": ("AFRICA", "NORTH_AFRICA"),
201
+ }
202
+
203
+
204
+ def get_continent_region_from_country(country_human: str) -> tuple[str, str]:
205
+ """Derive continent and region enums from country name.
206
+
207
+ Uses static mapping for common countries (fast path).
208
+ For unknown countries, requires dynamic fetching from locale API.
209
+
210
+ Args:
211
+ country_human: Human-readable country name from API
212
+
213
+ Returns:
214
+ Tuple of (continent_enum, region_enum)
215
+
216
+ Raises:
217
+ ValueError: If country is not in static mapping (requires dynamic fetch)
218
+ """
219
+ # Fast path: check static mapping
220
+ if country_human in COUNTRY_TO_LOCATION_STATIC:
221
+ return COUNTRY_TO_LOCATION_STATIC[country_human]
222
+
223
+ # Country not in static mapping - requires dynamic fetch
224
+ raise ValueError(
225
+ f"Country '{country_human}' not in static mapping. "
226
+ "Dynamic fetching from locale API required."
227
+ )
@@ -1,232 +1,14 @@
1
- """Constants and mappings for Luxpower/EG4 API.
1
+ """Inverter parameter register definitions and mappings.
2
2
 
3
- This module contains mapping tables extracted from the EG4 web interface
4
- to convert between human-readable API values and the enum values required
5
- for configuration updates.
3
+ This module contains all hold register (configuration) and input register
4
+ (runtime data) definitions for Luxpower/EG4 inverters, plus GridBOSS/MID
5
+ device parameters.
6
6
 
7
- These mappings were discovered by analyzing the HTML form at:
8
- /WManage/web/config/plant/edit/{plant_id}
7
+ Source: EG4-18KPV-12LV Modbus Protocol specification
9
8
  """
10
9
 
11
10
  from __future__ import annotations
12
11
 
13
- # Timezone mappings: Human-readable (from API) → Form enum (for POST)
14
- # Source: Analyzed all 28 timezone options from the HTML form
15
- TIMEZONE_MAP: dict[str, str] = {
16
- "GMT -12": "WEST12",
17
- "GMT -11": "WEST11",
18
- "GMT -10": "WEST10",
19
- "GMT -9": "WEST9",
20
- "GMT -8": "WEST8",
21
- "GMT -7": "WEST7",
22
- "GMT -6": "WEST6",
23
- "GMT -5": "WEST5",
24
- "GMT -4": "WEST4",
25
- "GMT -3": "WEST3",
26
- "GMT -2": "WEST2",
27
- "GMT -1": "WEST1",
28
- "GMT 0": "ZERO",
29
- "GMT +1": "EAST1",
30
- "GMT +2": "EAST2",
31
- "GMT +3": "EAST3",
32
- "GMT +3:30": "EAST3_30",
33
- "GMT +4": "EAST4",
34
- "GMT +5": "EAST5",
35
- "GMT +5:30": "EAST5_30",
36
- "GMT +6": "EAST6",
37
- "GMT +6:30": "EAST6_30",
38
- "GMT +7": "EAST7",
39
- "GMT +8": "EAST8",
40
- "GMT +9": "EAST9",
41
- "GMT +10": "EAST10",
42
- "GMT +11": "EAST11",
43
- "GMT +12": "EAST12",
44
- }
45
-
46
- # Reverse mapping: Form enum → Human-readable
47
- TIMEZONE_REVERSE_MAP: dict[str, str] = {v: k for k, v in TIMEZONE_MAP.items()}
48
-
49
- # Country mappings: Human-readable (from API) → Form enum (for POST)
50
- # Source: Analyzed country options from HTML form (North America region shown)
51
- # NOTE: This list is incomplete - only shows North American countries
52
- # Additional countries would appear based on selected continent/region
53
- COUNTRY_MAP: dict[str, str] = {
54
- "Canada": "CANADA",
55
- "United States of America": "UNITED_STATES_OF_AMERICA",
56
- "Mexico": "MEXICO",
57
- "Greenland": "GREENLAND",
58
- }
59
-
60
- # Reverse mapping: Form enum → Human-readable
61
- COUNTRY_REVERSE_MAP: dict[str, str] = {v: k for k, v in COUNTRY_MAP.items()}
62
-
63
- # Continent mappings: Human-readable → Form enum
64
- # Source: All 6 continent options from HTML form
65
- CONTINENT_MAP: dict[str, str] = {
66
- "Africa": "AFRICA",
67
- "Asia": "ASIA",
68
- "Europe": "EUROPE",
69
- "North America": "NORTH_AMERICA",
70
- "Oceania": "OCEANIA",
71
- "South America": "SOUTH_AMERICA",
72
- }
73
-
74
- CONTINENT_REVERSE_MAP: dict[str, str] = {v: k for k, v in CONTINENT_MAP.items()}
75
-
76
- # Region mappings: Human-readable → Form enum
77
- # Source: Region options from HTML form (context: North America continent)
78
- # NOTE: Region options are hierarchical and depend on selected continent
79
- REGION_MAP: dict[str, str] = {
80
- # North America regions (when continent = NORTH_AMERICA)
81
- "Caribbean": "CARIBBEAN",
82
- "Central America": "CENTRAL_AMERICA",
83
- "North America": "NORTH_AMERICA",
84
- # Additional regions would be discovered when exploring other continents
85
- }
86
-
87
- REGION_REVERSE_MAP: dict[str, str] = {v: k for k, v in REGION_MAP.items()}
88
-
89
-
90
- def get_timezone_enum(human_readable: str) -> str:
91
- """Convert human-readable timezone to API enum.
92
-
93
- Args:
94
- human_readable: Timezone string like "GMT -8"
95
-
96
- Returns:
97
- API enum like "WEST8"
98
-
99
- Raises:
100
- ValueError: If timezone is not recognized
101
- """
102
- if human_readable in TIMEZONE_MAP:
103
- return TIMEZONE_MAP[human_readable]
104
- raise ValueError(f"Unknown timezone: {human_readable}")
105
-
106
-
107
- def get_country_enum(human_readable: str) -> str:
108
- """Convert human-readable country to API enum.
109
-
110
- Args:
111
- human_readable: Country string like "United States of America"
112
-
113
- Returns:
114
- API enum like "UNITED_STATES_OF_AMERICA"
115
-
116
- Raises:
117
- ValueError: If country is not recognized
118
- """
119
- if human_readable in COUNTRY_MAP:
120
- return COUNTRY_MAP[human_readable]
121
- raise ValueError(f"Unknown country: {human_readable}")
122
-
123
-
124
- def get_region_enum(human_readable: str) -> str:
125
- """Convert human-readable region to API enum.
126
-
127
- Args:
128
- human_readable: Region string like "North America"
129
-
130
- Returns:
131
- API enum like "NORTH_AMERICA"
132
-
133
- Raises:
134
- ValueError: If region is not recognized
135
- """
136
- if human_readable in REGION_MAP:
137
- return REGION_MAP[human_readable]
138
- raise ValueError(f"Unknown region: {human_readable}")
139
-
140
-
141
- def get_continent_enum(human_readable: str) -> str:
142
- """Convert human-readable continent to API enum.
143
-
144
- Args:
145
- human_readable: Continent string like "North America"
146
-
147
- Returns:
148
- API enum like "NORTH_AMERICA"
149
-
150
- Raises:
151
- ValueError: If continent is not recognized
152
- """
153
- if human_readable in CONTINENT_MAP:
154
- return CONTINENT_MAP[human_readable]
155
- raise ValueError(f"Unknown continent: {human_readable}")
156
-
157
-
158
- # Static mapping for common countries (fast path)
159
- # This covers the most frequently used countries to avoid API calls
160
- COUNTRY_TO_LOCATION_STATIC: dict[str, tuple[str, str]] = {
161
- # North America
162
- "United States of America": ("NORTH_AMERICA", "NORTH_AMERICA"),
163
- "Canada": ("NORTH_AMERICA", "NORTH_AMERICA"),
164
- "Mexico": ("NORTH_AMERICA", "CENTRAL_AMERICA"),
165
- "Greenland": ("NORTH_AMERICA", "NORTH_AMERICA"),
166
- # Europe (common)
167
- "United Kingdom": ("EUROPE", "WESTERN_EUROPE"),
168
- "Germany": ("EUROPE", "CENTRAL_EUROPE"),
169
- "France": ("EUROPE", "WESTERN_EUROPE"),
170
- "Spain": ("EUROPE", "SOUTHERN_EUROPE"),
171
- "Italy": ("EUROPE", "SOUTHERN_EUROPE"),
172
- "The Netherlands": ("EUROPE", "WESTERN_EUROPE"),
173
- "Belgium": ("EUROPE", "WESTERN_EUROPE"),
174
- "Switzerland": ("EUROPE", "CENTRAL_EUROPE"),
175
- "Austria": ("EUROPE", "CENTRAL_EUROPE"),
176
- "Poland": ("EUROPE", "CENTRAL_EUROPE"),
177
- "Sweden": ("EUROPE", "NORDIC_EUROPE"),
178
- "Norway": ("EUROPE", "NORDIC_EUROPE"),
179
- "Denmark": ("EUROPE", "NORDIC_EUROPE"),
180
- # Asia (common)
181
- "China": ("ASIA", "EAST_ASIA"),
182
- "Japan": ("ASIA", "EAST_ASIA"),
183
- "South korea": ("ASIA", "EAST_ASIA"),
184
- "India": ("ASIA", "SOUTH_ASIA"),
185
- "Singapore": ("ASIA", "SOUTHEAST_ASIA"),
186
- "Thailand": ("ASIA", "SOUTHEAST_ASIA"),
187
- "Malaysia": ("ASIA", "SOUTHEAST_ASIA"),
188
- "Indonesia": ("ASIA", "SOUTHEAST_ASIA"),
189
- "Philippines": ("ASIA", "SOUTHEAST_ASIA"),
190
- "Vietnam": ("ASIA", "SOUTHEAST_ASIA"),
191
- # Oceania
192
- "Australia": ("OCEANIA", "OCEANIA"),
193
- "New Zealand": ("OCEANIA", "OCEANIA"),
194
- # South America
195
- "Brazil": ("SOUTH_AMERICA", "SA_EAST"),
196
- "Argentina": ("SOUTH_AMERICA", "SA_SOUTHERN_PART"), # Note: API has "Aregntine" typo
197
- "Chile": ("SOUTH_AMERICA", "SA_SOUTHERN_PART"),
198
- # Africa (common)
199
- "South Africa": ("AFRICA", "SOUTH_AFRICA"),
200
- "Egypt": ("AFRICA", "NORTH_AFRICA"),
201
- }
202
-
203
-
204
- def get_continent_region_from_country(country_human: str) -> tuple[str, str]:
205
- """Derive continent and region enums from country name.
206
-
207
- Uses static mapping for common countries (fast path).
208
- For unknown countries, requires dynamic fetching from locale API.
209
-
210
- Args:
211
- country_human: Human-readable country name from API
212
-
213
- Returns:
214
- Tuple of (continent_enum, region_enum)
215
-
216
- Raises:
217
- ValueError: If country is not in static mapping (requires dynamic fetch)
218
- """
219
- # Fast path: check static mapping
220
- if country_human in COUNTRY_TO_LOCATION_STATIC:
221
- return COUNTRY_TO_LOCATION_STATIC[country_human]
222
-
223
- # Country not in static mapping - requires dynamic fetch
224
- raise ValueError(
225
- f"Country '{country_human}' not in static mapping. "
226
- "Dynamic fetching from locale API required."
227
- )
228
-
229
-
230
12
  # ============================================================================
231
13
  # INVERTER PARAMETER MAPPINGS (Hold Registers)
232
14
  # ============================================================================
@@ -242,7 +24,7 @@ FUNC_EN_BIT_FORCED_DISCHG_EN = 10 # Forced discharge enable
242
24
  FUNC_EN_BIT_FORCED_CHG_EN = 11 # Force charge enable
243
25
 
244
26
  # AC Charge Parameters
245
- HOLD_AC_CHARGE_POWER_CMD = 66 # AC charge power (0-100%)
27
+ HOLD_AC_CHARGE_POWER_CMD = 66 # AC charge power (0.0-15.0 kW)
246
28
  HOLD_AC_CHARGE_SOC_LIMIT = 67 # AC charge SOC limit (0-100%)
247
29
  HOLD_AC_CHARGE_START_HOUR_1 = 68 # Time period 1 start hour (0-23)
248
30
  HOLD_AC_CHARGE_START_MIN_1 = 69 # Time period 1 start minute (0-59)
@@ -327,22 +109,22 @@ INPUT_P_EPS = (30, 31) # EPS output power (W, 2 registers)
327
109
  INPUT_S_EPS = 32 # EPS status
328
110
  INPUT_P_TO_GRID = 33 # Export to grid power (W)
329
111
  INPUT_P_TO_USER = (34, 35) # Load consumption power (W, 2 registers)
330
- INPUT_E_INV_ALL = 36 # Total inverter energy (kWh, /10)
331
- INPUT_E_REC_ALL = 37 # Total grid import energy (kWh, /10)
332
- INPUT_E_CHG_ALL = 38 # Total charge energy (kWh, /10)
333
- INPUT_E_DISCHG_ALL = 39 # Total discharge energy (kWh, /10)
334
- INPUT_E_EPS_ALL = 40 # Total EPS energy (kWh, /10)
335
- INPUT_E_TO_GRID_ALL = 41 # Total export energy (kWh, /10)
336
- INPUT_E_TO_USER_ALL = 42 # Total load energy (kWh, /10)
112
+ INPUT_E_INV_ALL = 36 # Total inverter energy (Wh after /10, divide by 10 for Wh)
113
+ INPUT_E_REC_ALL = 37 # Total grid import energy (Wh after /10, divide by 10 for Wh)
114
+ INPUT_E_CHG_ALL = 38 # Total charge energy (Wh after /10, divide by 10 for Wh)
115
+ INPUT_E_DISCHG_ALL = 39 # Total discharge energy (Wh after /10, divide by 10 for Wh)
116
+ INPUT_E_EPS_ALL = 40 # Total EPS energy (Wh after /10, divide by 10 for Wh)
117
+ INPUT_E_TO_GRID_ALL = 41 # Total export energy (Wh after /10, divide by 10 for Wh)
118
+ INPUT_E_TO_USER_ALL = 42 # Total load energy (Wh after /10, divide by 10 for Wh)
337
119
  INPUT_V_BUS1 = 43 # Bus 1 voltage (V, /10)
338
120
  INPUT_V_BUS2 = 44 # Bus 2 voltage (V, /10)
339
- INPUT_E_INV_DAY = (45, 46) # Daily inverter energy (kWh, /10, 2 registers)
340
- INPUT_E_REC_DAY = (47, 48) # Daily grid import (kWh, /10, 2 registers)
341
- INPUT_E_CHG_DAY = (49, 50) # Daily charge energy (kWh, /10, 2 registers)
342
- INPUT_E_DISCHG_DAY = (51, 52) # Daily discharge energy (kWh, /10, 2 registers)
343
- INPUT_E_EPS_DAY = (53, 54) # Daily EPS energy (kWh, /10, 2 registers)
344
- INPUT_E_TO_GRID_DAY = (55, 56) # Daily export energy (kWh, /10, 2 registers)
345
- INPUT_E_TO_USER_DAY = (57, 58) # Daily load energy (kWh, /10, 2 registers)
121
+ INPUT_E_INV_DAY = (45, 46) # Daily inverter energy (Wh after /10, 2 registers)
122
+ INPUT_E_REC_DAY = (47, 48) # Daily grid import (Wh after /10, 2 registers)
123
+ INPUT_E_CHG_DAY = (49, 50) # Daily charge energy (Wh after /10, 2 registers)
124
+ INPUT_E_DISCHG_DAY = (51, 52) # Daily discharge energy (Wh after /10, 2 registers)
125
+ INPUT_E_EPS_DAY = (53, 54) # Daily EPS energy (Wh after /10, 2 registers)
126
+ INPUT_E_TO_GRID_DAY = (55, 56) # Daily export energy (Wh after /10, 2 registers)
127
+ INPUT_E_TO_USER_DAY = (57, 58) # Daily load energy (Wh after /10, 2 registers)
346
128
  INPUT_V_BAT_LIMIT = 59 # Max charge voltage (V, /100)
347
129
  INPUT_I_BAT_LIMIT = 60 # Max charge current (A, /10)
348
130
 
@@ -1135,6 +917,58 @@ GRIDBOSS_STATS = {
1135
917
  "midbox_specific_parameters": 159, # Smart Load, AC Coupling, Generator control
1136
918
  }
1137
919
 
920
+ # ============================================================================
921
+ # MODEL-SPECIFIC PARAMETER KEYS
922
+ # ============================================================================
923
+ # These parameters are only available on specific inverter model families.
924
+ # The feature detection system uses these to identify model capabilities.
925
+
926
+ # SNA Series (Split-phase, North America) - Device Type Code: 54
927
+ # These parameters are unique to SNA models like SNA12K-US
928
+ SNA_PARAMETERS = [
929
+ # Discharge recovery hysteresis (prevents oscillation at SOC cutoff)
930
+ "HOLD_DISCHG_RECOVERY_LAG_SOC", # SOC hysteresis percentage
931
+ "HOLD_DISCHG_RECOVERY_LAG_VOLT", # Voltage hysteresis (÷10)
932
+ # Quick charge configuration
933
+ "SNA_HOLD_QUICK_CHARGE_MINUTE", # Quick charge duration in minutes
934
+ # Off-grid specific
935
+ "OFF_GRID_HOLD_EPS_VOLT_SET",
936
+ "OFF_GRID_HOLD_EPS_FREQ_SET",
937
+ ]
938
+
939
+ # PV Series (High-voltage DC, US) - Device Type Code: 2092
940
+ # These parameters are available on 18KPV and similar models
941
+ PV_SERIES_PARAMETERS = [
942
+ # Volt-Watt curve parameters
943
+ "HOLD_VW_V1",
944
+ "HOLD_VW_V2",
945
+ "HOLD_VW_V3",
946
+ "HOLD_VW_V4",
947
+ "HOLD_VW_P1",
948
+ "HOLD_VW_P2",
949
+ "HOLD_VW_P3",
950
+ "HOLD_VW_P4",
951
+ # Grid peak shaving
952
+ "_12K_HOLD_GRID_PEAK_SHAVING_POWER",
953
+ # Parallel operation
954
+ "HOLD_PARALLEL_REGISTER",
955
+ ]
956
+
957
+ # LXP-EU Series (European) - Device Type Code: 12
958
+ # These parameters are available on LXP-EU 12K and similar models
959
+ LXP_EU_PARAMETERS = [
960
+ # EU grid compliance
961
+ "HOLD_EU_GRID_CODE",
962
+ "HOLD_EU_COUNTRY_CODE",
963
+ ]
964
+
965
+ # Device Type Code Constants (HOLD_DEVICE_TYPE_CODE register 19)
966
+ # These identify the specific inverter model/variant
967
+ DEVICE_TYPE_CODE_SNA = 54 # SNA Series (e.g., SNA12K-US)
968
+ DEVICE_TYPE_CODE_PV_SERIES = 2092 # PV Series (e.g., 18KPV)
969
+ DEVICE_TYPE_CODE_LXP_EU = 12 # LXP-EU Series (e.g., LXP-EU 12K)
970
+
971
+
1138
972
  # ============================================================================
1139
973
  # HELPER FUNCTIONS FOR PARAMETER OPERATIONS
1140
974
  # ============================================================================