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.
- 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 +545 -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 +351 -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 +629 -40
- pylxpweb/transports/__init__.py +78 -0
- pylxpweb/transports/capabilities.py +101 -0
- pylxpweb/transports/data.py +495 -0
- pylxpweb/transports/exceptions.py +59 -0
- pylxpweb/transports/factory.py +119 -0
- pylxpweb/transports/http.py +329 -0
- pylxpweb/transports/modbus.py +557 -0
- pylxpweb/transports/protocol.py +217 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/METADATA +130 -85
- pylxpweb-0.5.0.dist-info/RECORD +52 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/WHEEL +1 -1
- pylxpweb-0.5.0.dist-info/entry_points.txt +3 -0
- pylxpweb-0.1.0.dist-info/RECORD +0 -19
pylxpweb/endpoints/control.py
CHANGED
|
@@ -8,6 +8,7 @@ This module provides device control functionality including:
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import logging
|
|
11
12
|
from typing import TYPE_CHECKING
|
|
12
13
|
|
|
13
14
|
from pylxpweb.endpoints.base import BaseEndpoint
|
|
@@ -20,6 +21,8 @@ from pylxpweb.models import (
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from pylxpweb.client import LuxpowerClient
|
|
22
23
|
|
|
24
|
+
_LOGGER = logging.getLogger(__name__)
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
class ControlEndpoints(BaseEndpoint):
|
|
25
28
|
"""Device control endpoints for parameters, functions, and quick charge."""
|
|
@@ -162,7 +165,72 @@ class ControlEndpoints(BaseEndpoint):
|
|
|
162
165
|
response = await self.client._request(
|
|
163
166
|
"POST", "/WManage/web/maintain/remoteSet/write", data=data
|
|
164
167
|
)
|
|
165
|
-
|
|
168
|
+
result = SuccessResponse.model_validate(response)
|
|
169
|
+
|
|
170
|
+
# Invalidate cache after successful write to ensure fresh data on next read
|
|
171
|
+
if result.success:
|
|
172
|
+
self.client.invalidate_cache_for_device(inverter_sn)
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
async def write_parameters(
|
|
177
|
+
self,
|
|
178
|
+
inverter_sn: str,
|
|
179
|
+
parameters: dict[int, int],
|
|
180
|
+
client_type: str = "WEB",
|
|
181
|
+
) -> SuccessResponse:
|
|
182
|
+
"""Write multiple configuration parameters to the inverter.
|
|
183
|
+
|
|
184
|
+
WARNING: This changes device configuration!
|
|
185
|
+
|
|
186
|
+
This is a convenience method that writes register values directly.
|
|
187
|
+
For named parameters, use write_parameter() instead.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
inverter_sn: Inverter serial number
|
|
191
|
+
parameters: Dict mapping register addresses to values
|
|
192
|
+
client_type: Client type (WEB/APP)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
SuccessResponse: Operation result
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
# Set multiple registers at once
|
|
199
|
+
await client.control.write_parameters(
|
|
200
|
+
"1234567890",
|
|
201
|
+
{21: 512, 66: 50, 67: 100} # Register addresses and values
|
|
202
|
+
)
|
|
203
|
+
"""
|
|
204
|
+
# Note: The API doesn't support batch writes, so we write sequentially
|
|
205
|
+
# For now, just write the first parameter (most common use case is single register)
|
|
206
|
+
# Multi-parameter support would require sequential writes with proper error handling
|
|
207
|
+
if not parameters:
|
|
208
|
+
return SuccessResponse(success=True)
|
|
209
|
+
|
|
210
|
+
# For now, we only support single parameter writes through this method
|
|
211
|
+
# Multi-parameter support would require discovering parameter names from register IDs
|
|
212
|
+
register, value = next(iter(parameters.items()))
|
|
213
|
+
|
|
214
|
+
# This is a simplified implementation - would need register-to-param mapping
|
|
215
|
+
# for production use. For now, used primarily by device classes for single writes.
|
|
216
|
+
await self.client._ensure_authenticated()
|
|
217
|
+
|
|
218
|
+
data = {
|
|
219
|
+
"inverterSn": inverter_sn,
|
|
220
|
+
"data": {str(register): value},
|
|
221
|
+
"clientType": client_type,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
response = await self.client._request(
|
|
225
|
+
"POST", "/WManage/web/maintain/remoteSet/write", data=data
|
|
226
|
+
)
|
|
227
|
+
result = SuccessResponse.model_validate(response)
|
|
228
|
+
|
|
229
|
+
# Invalidate cache after successful write to ensure fresh data on next read
|
|
230
|
+
if result.success:
|
|
231
|
+
self.client.invalidate_cache_for_device(inverter_sn)
|
|
232
|
+
|
|
233
|
+
return result
|
|
166
234
|
|
|
167
235
|
async def control_function(
|
|
168
236
|
self,
|
|
@@ -219,7 +287,13 @@ class ControlEndpoints(BaseEndpoint):
|
|
|
219
287
|
response = await self.client._request(
|
|
220
288
|
"POST", "/WManage/web/maintain/remoteSet/functionControl", data=data
|
|
221
289
|
)
|
|
222
|
-
|
|
290
|
+
result = SuccessResponse.model_validate(response)
|
|
291
|
+
|
|
292
|
+
# Invalidate cache after successful write to ensure fresh data on next read
|
|
293
|
+
if result.success:
|
|
294
|
+
self.client.invalidate_cache_for_device(inverter_sn)
|
|
295
|
+
|
|
296
|
+
return result
|
|
223
297
|
|
|
224
298
|
async def start_quick_charge(
|
|
225
299
|
self, inverter_sn: str, client_type: str = "WEB"
|
|
@@ -304,3 +378,907 @@ class ControlEndpoints(BaseEndpoint):
|
|
|
304
378
|
cache_endpoint="quick_charge_status",
|
|
305
379
|
)
|
|
306
380
|
return QuickChargeStatus.model_validate(response)
|
|
381
|
+
|
|
382
|
+
async def start_quick_discharge(
|
|
383
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
384
|
+
) -> SuccessResponse:
|
|
385
|
+
"""Start quick discharge operation.
|
|
386
|
+
|
|
387
|
+
WARNING: This starts discharging!
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
inverter_sn: Inverter serial number
|
|
391
|
+
client_type: Client type (WEB/APP)
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
SuccessResponse: Operation result
|
|
395
|
+
|
|
396
|
+
Example:
|
|
397
|
+
result = await client.control.start_quick_discharge("1234567890")
|
|
398
|
+
if result.success:
|
|
399
|
+
print("Quick discharge started successfully")
|
|
400
|
+
"""
|
|
401
|
+
await self.client._ensure_authenticated()
|
|
402
|
+
|
|
403
|
+
data = {"inverterSn": inverter_sn, "clientType": client_type}
|
|
404
|
+
|
|
405
|
+
response = await self.client._request(
|
|
406
|
+
"POST", "/WManage/web/config/quickDischarge/start", data=data
|
|
407
|
+
)
|
|
408
|
+
return SuccessResponse.model_validate(response)
|
|
409
|
+
|
|
410
|
+
async def stop_quick_discharge(
|
|
411
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
412
|
+
) -> SuccessResponse:
|
|
413
|
+
"""Stop quick discharge operation.
|
|
414
|
+
|
|
415
|
+
WARNING: This stops discharging!
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
inverter_sn: Inverter serial number
|
|
419
|
+
client_type: Client type (WEB/APP)
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
SuccessResponse: Operation result
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
result = await client.control.stop_quick_discharge("1234567890")
|
|
426
|
+
if result.success:
|
|
427
|
+
print("Quick discharge stopped successfully")
|
|
428
|
+
"""
|
|
429
|
+
await self.client._ensure_authenticated()
|
|
430
|
+
|
|
431
|
+
data = {"inverterSn": inverter_sn, "clientType": client_type}
|
|
432
|
+
|
|
433
|
+
response = await self.client._request(
|
|
434
|
+
"POST", "/WManage/web/config/quickDischarge/stop", data=data
|
|
435
|
+
)
|
|
436
|
+
return SuccessResponse.model_validate(response)
|
|
437
|
+
|
|
438
|
+
# ============================================================================
|
|
439
|
+
# Convenience Helper Methods
|
|
440
|
+
# ============================================================================
|
|
441
|
+
|
|
442
|
+
async def enable_battery_backup(
|
|
443
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
444
|
+
) -> SuccessResponse:
|
|
445
|
+
"""Enable battery backup (EPS) mode.
|
|
446
|
+
|
|
447
|
+
Convenience wrapper for control_function(..., "FUNC_EPS_EN", True).
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
inverter_sn: Inverter serial number
|
|
451
|
+
client_type: Client type (WEB/APP)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
SuccessResponse: Operation result
|
|
455
|
+
|
|
456
|
+
Example:
|
|
457
|
+
>>> result = await client.control.enable_battery_backup("1234567890")
|
|
458
|
+
>>> result.success
|
|
459
|
+
True
|
|
460
|
+
"""
|
|
461
|
+
return await self.control_function(
|
|
462
|
+
inverter_sn, "FUNC_EPS_EN", True, client_type=client_type
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
async def disable_battery_backup(
|
|
466
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
467
|
+
) -> SuccessResponse:
|
|
468
|
+
"""Disable battery backup (EPS) mode.
|
|
469
|
+
|
|
470
|
+
Convenience wrapper for control_function(..., "FUNC_EPS_EN", False).
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
inverter_sn: Inverter serial number
|
|
474
|
+
client_type: Client type (WEB/APP)
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
SuccessResponse: Operation result
|
|
478
|
+
|
|
479
|
+
Example:
|
|
480
|
+
>>> result = await client.control.disable_battery_backup("1234567890")
|
|
481
|
+
>>> result.success
|
|
482
|
+
True
|
|
483
|
+
"""
|
|
484
|
+
return await self.control_function(
|
|
485
|
+
inverter_sn, "FUNC_EPS_EN", False, client_type=client_type
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
async def enable_battery_backup_ctrl(
|
|
489
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
490
|
+
) -> SuccessResponse:
|
|
491
|
+
"""Enable battery backup control mode (working mode).
|
|
492
|
+
|
|
493
|
+
This controls FUNC_BATTERY_BACKUP_CTRL, which is distinct from
|
|
494
|
+
FUNC_EPS_EN (EPS/off-grid mode). Battery backup control is a
|
|
495
|
+
working mode setting that affects how the inverter manages
|
|
496
|
+
battery reserves for backup power.
|
|
497
|
+
|
|
498
|
+
Convenience wrapper for control_function(..., "FUNC_BATTERY_BACKUP_CTRL", True).
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
inverter_sn: Inverter serial number
|
|
502
|
+
client_type: Client type (WEB/APP)
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
SuccessResponse: Operation result
|
|
506
|
+
|
|
507
|
+
Example:
|
|
508
|
+
>>> result = await client.control.enable_battery_backup_ctrl("1234567890")
|
|
509
|
+
>>> result.success
|
|
510
|
+
True
|
|
511
|
+
"""
|
|
512
|
+
return await self.control_function(
|
|
513
|
+
inverter_sn, "FUNC_BATTERY_BACKUP_CTRL", True, client_type=client_type
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
async def disable_battery_backup_ctrl(
|
|
517
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
518
|
+
) -> SuccessResponse:
|
|
519
|
+
"""Disable battery backup control mode (working mode).
|
|
520
|
+
|
|
521
|
+
This controls FUNC_BATTERY_BACKUP_CTRL, which is distinct from
|
|
522
|
+
FUNC_EPS_EN (EPS/off-grid mode). Battery backup control is a
|
|
523
|
+
working mode setting that affects how the inverter manages
|
|
524
|
+
battery reserves for backup power.
|
|
525
|
+
|
|
526
|
+
Convenience wrapper for control_function(..., "FUNC_BATTERY_BACKUP_CTRL", False).
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
inverter_sn: Inverter serial number
|
|
530
|
+
client_type: Client type (WEB/APP)
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
SuccessResponse: Operation result
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
>>> result = await client.control.disable_battery_backup_ctrl("1234567890")
|
|
537
|
+
>>> result.success
|
|
538
|
+
True
|
|
539
|
+
"""
|
|
540
|
+
return await self.control_function(
|
|
541
|
+
inverter_sn, "FUNC_BATTERY_BACKUP_CTRL", False, client_type=client_type
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
async def enable_normal_mode(
|
|
545
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
546
|
+
) -> SuccessResponse:
|
|
547
|
+
"""Enable normal operating mode (power on).
|
|
548
|
+
|
|
549
|
+
Convenience wrapper for control_function(..., "FUNC_SET_TO_STANDBY", True).
|
|
550
|
+
Note: FUNC_SET_TO_STANDBY = True means NOT in standby (normal mode).
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
inverter_sn: Inverter serial number
|
|
554
|
+
client_type: Client type (WEB/APP)
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
SuccessResponse: Operation result
|
|
558
|
+
|
|
559
|
+
Example:
|
|
560
|
+
>>> result = await client.control.enable_normal_mode("1234567890")
|
|
561
|
+
>>> result.success
|
|
562
|
+
True
|
|
563
|
+
"""
|
|
564
|
+
return await self.control_function(
|
|
565
|
+
inverter_sn, "FUNC_SET_TO_STANDBY", True, client_type=client_type
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
async def enable_standby_mode(
|
|
569
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
570
|
+
) -> SuccessResponse:
|
|
571
|
+
"""Enable standby mode (power off).
|
|
572
|
+
|
|
573
|
+
Convenience wrapper for control_function(..., "FUNC_SET_TO_STANDBY", False).
|
|
574
|
+
Note: FUNC_SET_TO_STANDBY = False means standby mode is active.
|
|
575
|
+
|
|
576
|
+
WARNING: This powers off the inverter!
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
inverter_sn: Inverter serial number
|
|
580
|
+
client_type: Client type (WEB/APP)
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
SuccessResponse: Operation result
|
|
584
|
+
|
|
585
|
+
Example:
|
|
586
|
+
>>> result = await client.control.enable_standby_mode("1234567890")
|
|
587
|
+
>>> result.success
|
|
588
|
+
True
|
|
589
|
+
"""
|
|
590
|
+
return await self.control_function(
|
|
591
|
+
inverter_sn, "FUNC_SET_TO_STANDBY", False, client_type=client_type
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
async def enable_grid_peak_shaving(
|
|
595
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
596
|
+
) -> SuccessResponse:
|
|
597
|
+
"""Enable grid peak shaving mode.
|
|
598
|
+
|
|
599
|
+
Convenience wrapper for control_function(..., "FUNC_GRID_PEAK_SHAVING", True).
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
inverter_sn: Inverter serial number
|
|
603
|
+
client_type: Client type (WEB/APP)
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
SuccessResponse: Operation result
|
|
607
|
+
|
|
608
|
+
Example:
|
|
609
|
+
>>> result = await client.control.enable_grid_peak_shaving("1234567890")
|
|
610
|
+
>>> result.success
|
|
611
|
+
True
|
|
612
|
+
"""
|
|
613
|
+
return await self.control_function(
|
|
614
|
+
inverter_sn, "FUNC_GRID_PEAK_SHAVING", True, client_type=client_type
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
async def disable_grid_peak_shaving(
|
|
618
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
619
|
+
) -> SuccessResponse:
|
|
620
|
+
"""Disable grid peak shaving mode.
|
|
621
|
+
|
|
622
|
+
Convenience wrapper for control_function(..., "FUNC_GRID_PEAK_SHAVING", False).
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
inverter_sn: Inverter serial number
|
|
626
|
+
client_type: Client type (WEB/APP)
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
SuccessResponse: Operation result
|
|
630
|
+
|
|
631
|
+
Example:
|
|
632
|
+
>>> result = await client.control.disable_grid_peak_shaving("1234567890")
|
|
633
|
+
>>> result.success
|
|
634
|
+
True
|
|
635
|
+
"""
|
|
636
|
+
return await self.control_function(
|
|
637
|
+
inverter_sn, "FUNC_GRID_PEAK_SHAVING", False, client_type=client_type
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
async def get_battery_backup_status(self, inverter_sn: str) -> bool:
|
|
641
|
+
"""Get battery backup (EPS) enabled status.
|
|
642
|
+
|
|
643
|
+
Reads register 21 (function enable) and extracts FUNC_EPS_EN bit.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
inverter_sn: Inverter serial number
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
bool: True if EPS mode is enabled, False otherwise
|
|
650
|
+
|
|
651
|
+
Example:
|
|
652
|
+
>>> enabled = await client.control.get_battery_backup_status("1234567890")
|
|
653
|
+
>>> if enabled:
|
|
654
|
+
>>> print("EPS mode is active")
|
|
655
|
+
"""
|
|
656
|
+
response = await self.read_parameters(inverter_sn, 21, 1)
|
|
657
|
+
value = response.parameters.get("FUNC_EPS_EN", False)
|
|
658
|
+
return bool(value)
|
|
659
|
+
|
|
660
|
+
# ============================================================================
|
|
661
|
+
# Working Mode Controls (Issue #16)
|
|
662
|
+
# ============================================================================
|
|
663
|
+
|
|
664
|
+
async def enable_ac_charge_mode(
|
|
665
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
666
|
+
) -> SuccessResponse:
|
|
667
|
+
"""Enable AC charge mode to allow battery charging from grid.
|
|
668
|
+
|
|
669
|
+
Convenience wrapper for control_function(..., "FUNC_AC_CHARGE", True).
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
inverter_sn: Inverter serial number
|
|
673
|
+
client_type: Client type (WEB/APP)
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
SuccessResponse: Operation result
|
|
677
|
+
|
|
678
|
+
Example:
|
|
679
|
+
>>> result = await client.control.enable_ac_charge_mode("1234567890")
|
|
680
|
+
>>> result.success
|
|
681
|
+
True
|
|
682
|
+
"""
|
|
683
|
+
return await self.control_function(
|
|
684
|
+
inverter_sn, "FUNC_AC_CHARGE", True, client_type=client_type
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
async def disable_ac_charge_mode(
|
|
688
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
689
|
+
) -> SuccessResponse:
|
|
690
|
+
"""Disable AC charge mode.
|
|
691
|
+
|
|
692
|
+
Convenience wrapper for control_function(..., "FUNC_AC_CHARGE", False).
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
inverter_sn: Inverter serial number
|
|
696
|
+
client_type: Client type (WEB/APP)
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
SuccessResponse: Operation result
|
|
700
|
+
|
|
701
|
+
Example:
|
|
702
|
+
>>> result = await client.control.disable_ac_charge_mode("1234567890")
|
|
703
|
+
>>> result.success
|
|
704
|
+
True
|
|
705
|
+
"""
|
|
706
|
+
return await self.control_function(
|
|
707
|
+
inverter_sn, "FUNC_AC_CHARGE", False, client_type=client_type
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
async def get_ac_charge_mode_status(self, inverter_sn: str) -> bool:
|
|
711
|
+
"""Get current AC charge mode status.
|
|
712
|
+
|
|
713
|
+
Reads register 21 (function enable) and extracts FUNC_AC_CHARGE bit.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
inverter_sn: Inverter serial number
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
bool: True if AC charge mode is enabled, False otherwise
|
|
720
|
+
|
|
721
|
+
Example:
|
|
722
|
+
>>> enabled = await client.control.get_ac_charge_mode_status("1234567890")
|
|
723
|
+
>>> if enabled:
|
|
724
|
+
>>> print("AC charge mode is active")
|
|
725
|
+
"""
|
|
726
|
+
response = await self.read_parameters(inverter_sn, 21, 1)
|
|
727
|
+
value = response.parameters.get("FUNC_AC_CHARGE", False)
|
|
728
|
+
return bool(value)
|
|
729
|
+
|
|
730
|
+
async def enable_pv_charge_priority(
|
|
731
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
732
|
+
) -> SuccessResponse:
|
|
733
|
+
"""Enable PV charge priority mode during specified hours.
|
|
734
|
+
|
|
735
|
+
Convenience wrapper for control_function(..., "FUNC_FORCED_CHG_EN", True).
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
inverter_sn: Inverter serial number
|
|
739
|
+
client_type: Client type (WEB/APP)
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
SuccessResponse: Operation result
|
|
743
|
+
|
|
744
|
+
Example:
|
|
745
|
+
>>> result = await client.control.enable_pv_charge_priority("1234567890")
|
|
746
|
+
>>> result.success
|
|
747
|
+
True
|
|
748
|
+
"""
|
|
749
|
+
return await self.control_function(
|
|
750
|
+
inverter_sn, "FUNC_FORCED_CHG_EN", True, client_type=client_type
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
async def disable_pv_charge_priority(
|
|
754
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
755
|
+
) -> SuccessResponse:
|
|
756
|
+
"""Disable PV charge priority mode.
|
|
757
|
+
|
|
758
|
+
Convenience wrapper for control_function(..., "FUNC_FORCED_CHG_EN", False).
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
inverter_sn: Inverter serial number
|
|
762
|
+
client_type: Client type (WEB/APP)
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
SuccessResponse: Operation result
|
|
766
|
+
|
|
767
|
+
Example:
|
|
768
|
+
>>> result = await client.control.disable_pv_charge_priority("1234567890")
|
|
769
|
+
>>> result.success
|
|
770
|
+
True
|
|
771
|
+
"""
|
|
772
|
+
return await self.control_function(
|
|
773
|
+
inverter_sn, "FUNC_FORCED_CHG_EN", False, client_type=client_type
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
async def get_pv_charge_priority_status(self, inverter_sn: str) -> bool:
|
|
777
|
+
"""Get current PV charge priority status.
|
|
778
|
+
|
|
779
|
+
Reads register 21 (function enable) and extracts FUNC_FORCED_CHG_EN bit.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
inverter_sn: Inverter serial number
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
bool: True if PV charge priority is enabled, False otherwise
|
|
786
|
+
|
|
787
|
+
Example:
|
|
788
|
+
>>> enabled = await client.control.get_pv_charge_priority_status("1234567890")
|
|
789
|
+
>>> if enabled:
|
|
790
|
+
>>> print("PV charge priority mode is active")
|
|
791
|
+
"""
|
|
792
|
+
response = await self.read_parameters(inverter_sn, 21, 1)
|
|
793
|
+
value = response.parameters.get("FUNC_FORCED_CHG_EN", False)
|
|
794
|
+
return bool(value)
|
|
795
|
+
|
|
796
|
+
async def enable_forced_discharge(
|
|
797
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
798
|
+
) -> SuccessResponse:
|
|
799
|
+
"""Enable forced discharge mode for grid export.
|
|
800
|
+
|
|
801
|
+
Convenience wrapper for control_function(..., "FUNC_FORCED_DISCHG_EN", True).
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
inverter_sn: Inverter serial number
|
|
805
|
+
client_type: Client type (WEB/APP)
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
SuccessResponse: Operation result
|
|
809
|
+
|
|
810
|
+
Example:
|
|
811
|
+
>>> result = await client.control.enable_forced_discharge("1234567890")
|
|
812
|
+
>>> result.success
|
|
813
|
+
True
|
|
814
|
+
"""
|
|
815
|
+
return await self.control_function(
|
|
816
|
+
inverter_sn, "FUNC_FORCED_DISCHG_EN", True, client_type=client_type
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
async def disable_forced_discharge(
|
|
820
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
821
|
+
) -> SuccessResponse:
|
|
822
|
+
"""Disable forced discharge mode.
|
|
823
|
+
|
|
824
|
+
Convenience wrapper for control_function(..., "FUNC_FORCED_DISCHG_EN", False).
|
|
825
|
+
|
|
826
|
+
Args:
|
|
827
|
+
inverter_sn: Inverter serial number
|
|
828
|
+
client_type: Client type (WEB/APP)
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
SuccessResponse: Operation result
|
|
832
|
+
|
|
833
|
+
Example:
|
|
834
|
+
>>> result = await client.control.disable_forced_discharge("1234567890")
|
|
835
|
+
>>> result.success
|
|
836
|
+
True
|
|
837
|
+
"""
|
|
838
|
+
return await self.control_function(
|
|
839
|
+
inverter_sn, "FUNC_FORCED_DISCHG_EN", False, client_type=client_type
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
async def get_forced_discharge_status(self, inverter_sn: str) -> bool:
|
|
843
|
+
"""Get current forced discharge status.
|
|
844
|
+
|
|
845
|
+
Reads register 21 (function enable) and extracts FUNC_FORCED_DISCHG_EN bit.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
inverter_sn: Inverter serial number
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
bool: True if forced discharge is enabled, False otherwise
|
|
852
|
+
|
|
853
|
+
Example:
|
|
854
|
+
>>> enabled = await client.control.get_forced_discharge_status("1234567890")
|
|
855
|
+
>>> if enabled:
|
|
856
|
+
>>> print("Forced discharge mode is active")
|
|
857
|
+
"""
|
|
858
|
+
response = await self.read_parameters(inverter_sn, 21, 1)
|
|
859
|
+
value = response.parameters.get("FUNC_FORCED_DISCHG_EN", False)
|
|
860
|
+
return bool(value)
|
|
861
|
+
|
|
862
|
+
async def enable_peak_shaving_mode(
|
|
863
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
864
|
+
) -> SuccessResponse:
|
|
865
|
+
"""Enable grid peak shaving mode.
|
|
866
|
+
|
|
867
|
+
Convenience wrapper for control_function(..., "FUNC_GRID_PEAK_SHAVING", True).
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
inverter_sn: Inverter serial number
|
|
871
|
+
client_type: Client type (WEB/APP)
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
SuccessResponse: Operation result
|
|
875
|
+
|
|
876
|
+
Example:
|
|
877
|
+
>>> result = await client.control.enable_peak_shaving_mode("1234567890")
|
|
878
|
+
>>> result.success
|
|
879
|
+
True
|
|
880
|
+
"""
|
|
881
|
+
return await self.control_function(
|
|
882
|
+
inverter_sn, "FUNC_GRID_PEAK_SHAVING", True, client_type=client_type
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
async def disable_peak_shaving_mode(
|
|
886
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
887
|
+
) -> SuccessResponse:
|
|
888
|
+
"""Disable grid peak shaving mode.
|
|
889
|
+
|
|
890
|
+
Convenience wrapper for control_function(..., "FUNC_GRID_PEAK_SHAVING", False).
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
inverter_sn: Inverter serial number
|
|
894
|
+
client_type: Client type (WEB/APP)
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
SuccessResponse: Operation result
|
|
898
|
+
|
|
899
|
+
Example:
|
|
900
|
+
>>> result = await client.control.disable_peak_shaving_mode("1234567890")
|
|
901
|
+
>>> result.success
|
|
902
|
+
True
|
|
903
|
+
"""
|
|
904
|
+
return await self.control_function(
|
|
905
|
+
inverter_sn, "FUNC_GRID_PEAK_SHAVING", False, client_type=client_type
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
async def get_peak_shaving_mode_status(self, inverter_sn: str) -> bool:
|
|
909
|
+
"""Get current peak shaving mode status.
|
|
910
|
+
|
|
911
|
+
Reads register 21 (function enable) and extracts FUNC_GRID_PEAK_SHAVING bit.
|
|
912
|
+
|
|
913
|
+
Args:
|
|
914
|
+
inverter_sn: Inverter serial number
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
bool: True if peak shaving mode is enabled, False otherwise
|
|
918
|
+
|
|
919
|
+
Example:
|
|
920
|
+
>>> enabled = await client.control.get_peak_shaving_mode_status("1234567890")
|
|
921
|
+
>>> if enabled:
|
|
922
|
+
>>> print("Peak shaving mode is active")
|
|
923
|
+
"""
|
|
924
|
+
response = await self.read_parameters(inverter_sn, 21, 1)
|
|
925
|
+
value = response.parameters.get("FUNC_GRID_PEAK_SHAVING", False)
|
|
926
|
+
return bool(value)
|
|
927
|
+
|
|
928
|
+
# ============================================================================
|
|
929
|
+
# Green Mode Controls (Off-Grid Mode in Web Monitor)
|
|
930
|
+
# ============================================================================
|
|
931
|
+
|
|
932
|
+
async def enable_green_mode(
|
|
933
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
934
|
+
) -> SuccessResponse:
|
|
935
|
+
"""Enable green mode (off-grid mode in the web monitoring display).
|
|
936
|
+
|
|
937
|
+
Green Mode controls the off-grid operating mode toggle visible in the
|
|
938
|
+
EG4 web monitoring interface. When enabled, the inverter operates in
|
|
939
|
+
an off-grid optimized configuration.
|
|
940
|
+
|
|
941
|
+
Note: This is FUNC_GREEN_EN in register 110, distinct from FUNC_EPS_EN
|
|
942
|
+
(battery backup/EPS mode) in register 21.
|
|
943
|
+
|
|
944
|
+
Convenience wrapper for control_function(..., "FUNC_GREEN_EN", True).
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
inverter_sn: Inverter serial number
|
|
948
|
+
client_type: Client type (WEB/APP)
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
SuccessResponse: Operation result
|
|
952
|
+
|
|
953
|
+
Example:
|
|
954
|
+
>>> result = await client.control.enable_green_mode("1234567890")
|
|
955
|
+
>>> result.success
|
|
956
|
+
True
|
|
957
|
+
"""
|
|
958
|
+
return await self.control_function(
|
|
959
|
+
inverter_sn, "FUNC_GREEN_EN", True, client_type=client_type
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
async def disable_green_mode(
|
|
963
|
+
self, inverter_sn: str, client_type: str = "WEB"
|
|
964
|
+
) -> SuccessResponse:
|
|
965
|
+
"""Disable green mode (off-grid mode in the web monitoring display).
|
|
966
|
+
|
|
967
|
+
Green Mode controls the off-grid operating mode toggle visible in the
|
|
968
|
+
EG4 web monitoring interface. When disabled, the inverter operates in
|
|
969
|
+
standard grid-tied configuration.
|
|
970
|
+
|
|
971
|
+
Note: This is FUNC_GREEN_EN in register 110, distinct from FUNC_EPS_EN
|
|
972
|
+
(battery backup/EPS mode) in register 21.
|
|
973
|
+
|
|
974
|
+
Convenience wrapper for control_function(..., "FUNC_GREEN_EN", False).
|
|
975
|
+
|
|
976
|
+
Args:
|
|
977
|
+
inverter_sn: Inverter serial number
|
|
978
|
+
client_type: Client type (WEB/APP)
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
SuccessResponse: Operation result
|
|
982
|
+
|
|
983
|
+
Example:
|
|
984
|
+
>>> result = await client.control.disable_green_mode("1234567890")
|
|
985
|
+
>>> result.success
|
|
986
|
+
True
|
|
987
|
+
"""
|
|
988
|
+
return await self.control_function(
|
|
989
|
+
inverter_sn, "FUNC_GREEN_EN", False, client_type=client_type
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
async def get_green_mode_status(self, inverter_sn: str) -> bool:
|
|
993
|
+
"""Get current green mode (off-grid mode) status.
|
|
994
|
+
|
|
995
|
+
Green Mode controls the off-grid operating mode toggle visible in the
|
|
996
|
+
EG4 web monitoring interface.
|
|
997
|
+
|
|
998
|
+
Reads register 110 (system functions) and extracts FUNC_GREEN_EN bit.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
inverter_sn: Inverter serial number
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
bool: True if green mode is enabled, False otherwise
|
|
1005
|
+
|
|
1006
|
+
Example:
|
|
1007
|
+
>>> enabled = await client.control.get_green_mode_status("1234567890")
|
|
1008
|
+
>>> if enabled:
|
|
1009
|
+
>>> print("Green mode (off-grid) is active")
|
|
1010
|
+
"""
|
|
1011
|
+
response = await self.read_parameters(inverter_sn, 110, 1)
|
|
1012
|
+
value = response.parameters.get("FUNC_GREEN_EN", False)
|
|
1013
|
+
return bool(value)
|
|
1014
|
+
|
|
1015
|
+
async def read_device_parameters_ranges(self, inverter_sn: str) -> dict[str, int | bool]:
|
|
1016
|
+
"""Read all device parameters across three common register ranges.
|
|
1017
|
+
|
|
1018
|
+
This method combines three read_parameters() calls:
|
|
1019
|
+
- Range 1: 0-126 (System config, grid protection)
|
|
1020
|
+
- Range 2: 127-253 (Additional config)
|
|
1021
|
+
- Range 3: 240-366 (Extended parameters)
|
|
1022
|
+
|
|
1023
|
+
Args:
|
|
1024
|
+
inverter_sn: Inverter serial number
|
|
1025
|
+
|
|
1026
|
+
Returns:
|
|
1027
|
+
dict: Combined parameters from all three ranges
|
|
1028
|
+
|
|
1029
|
+
Example:
|
|
1030
|
+
>>> params = await client.control.read_device_parameters_ranges("1234567890")
|
|
1031
|
+
>>> params["HOLD_AC_CHARGE_POWER_CMD"]
|
|
1032
|
+
50
|
|
1033
|
+
>>> params["FUNC_EPS_EN"]
|
|
1034
|
+
True
|
|
1035
|
+
"""
|
|
1036
|
+
import asyncio
|
|
1037
|
+
|
|
1038
|
+
# Read all three ranges concurrently
|
|
1039
|
+
range1_task = self.read_parameters(inverter_sn, 0, 127)
|
|
1040
|
+
range2_task = self.read_parameters(inverter_sn, 127, 127)
|
|
1041
|
+
range3_task = self.read_parameters(inverter_sn, 240, 127)
|
|
1042
|
+
|
|
1043
|
+
range1, range2, range3 = await asyncio.gather(
|
|
1044
|
+
range1_task, range2_task, range3_task, return_exceptions=True
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
# Combine parameters from all ranges
|
|
1048
|
+
combined: dict[str, int | bool] = {}
|
|
1049
|
+
|
|
1050
|
+
if not isinstance(range1, BaseException):
|
|
1051
|
+
combined.update(range1.parameters)
|
|
1052
|
+
|
|
1053
|
+
if not isinstance(range2, BaseException):
|
|
1054
|
+
combined.update(range2.parameters)
|
|
1055
|
+
|
|
1056
|
+
if not isinstance(range3, BaseException):
|
|
1057
|
+
combined.update(range3.parameters)
|
|
1058
|
+
|
|
1059
|
+
return combined
|
|
1060
|
+
|
|
1061
|
+
# ============================================================================
|
|
1062
|
+
# Battery Current Control (Added in v0.3)
|
|
1063
|
+
# ============================================================================
|
|
1064
|
+
|
|
1065
|
+
async def set_battery_charge_current(
|
|
1066
|
+
self,
|
|
1067
|
+
inverter_sn: str,
|
|
1068
|
+
amperes: int,
|
|
1069
|
+
*,
|
|
1070
|
+
validate_battery_limits: bool = True,
|
|
1071
|
+
) -> SuccessResponse:
|
|
1072
|
+
"""Set battery charge current limit.
|
|
1073
|
+
|
|
1074
|
+
Controls the maximum current allowed to charge batteries.
|
|
1075
|
+
|
|
1076
|
+
Common use cases:
|
|
1077
|
+
- Prevent inverter throttling during high solar production
|
|
1078
|
+
- Time-of-use optimization (reduce charge during peak rates)
|
|
1079
|
+
- Battery health management (gentle charging)
|
|
1080
|
+
- Weather-based automation (reduce on sunny days, maximize on cloudy)
|
|
1081
|
+
|
|
1082
|
+
Power Calculation (48V nominal system):
|
|
1083
|
+
- 50A = ~2.4kW
|
|
1084
|
+
- 100A = ~4.8kW
|
|
1085
|
+
- 150A = ~7.2kW
|
|
1086
|
+
- 200A = ~9.6kW
|
|
1087
|
+
- 250A = ~12kW
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
inverter_sn: Inverter serial number
|
|
1091
|
+
amperes: Charge current limit (0-250 A)
|
|
1092
|
+
validate_battery_limits: Warn if value exceeds typical battery limits
|
|
1093
|
+
|
|
1094
|
+
Returns:
|
|
1095
|
+
SuccessResponse: Operation result
|
|
1096
|
+
|
|
1097
|
+
Raises:
|
|
1098
|
+
ValueError: If amperes not in valid range (0-250 A)
|
|
1099
|
+
|
|
1100
|
+
Warning:
|
|
1101
|
+
CRITICAL: Never exceed your battery's maximum charge current rating.
|
|
1102
|
+
Check battery manufacturer specifications before setting high values.
|
|
1103
|
+
Monitor battery temperature during high current operations.
|
|
1104
|
+
|
|
1105
|
+
Example:
|
|
1106
|
+
>>> # Prevent throttling on sunny days (limit to ~4kW charge at 48V)
|
|
1107
|
+
>>> await client.control.set_battery_charge_current("1234567890", 80)
|
|
1108
|
+
SuccessResponse(success=True)
|
|
1109
|
+
|
|
1110
|
+
>>> # Maximum charge on cloudy days
|
|
1111
|
+
>>> await client.control.set_battery_charge_current("1234567890", 200)
|
|
1112
|
+
SuccessResponse(success=True)
|
|
1113
|
+
"""
|
|
1114
|
+
if not (0 <= amperes <= 250):
|
|
1115
|
+
raise ValueError(f"Battery charge current must be between 0-250 A, got {amperes}")
|
|
1116
|
+
|
|
1117
|
+
if validate_battery_limits and amperes > 200:
|
|
1118
|
+
_LOGGER.warning(
|
|
1119
|
+
"Setting battery charge current to %d A. "
|
|
1120
|
+
"Ensure this does not exceed your battery's maximum rating. "
|
|
1121
|
+
"Typical limits: 200A for 10kWh, 150A for 7.5kWh, 100A for 5kWh.",
|
|
1122
|
+
amperes,
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
return await self.write_parameter(inverter_sn, "HOLD_LEAD_ACID_CHARGE_RATE", str(amperes))
|
|
1126
|
+
|
|
1127
|
+
async def set_battery_discharge_current(
|
|
1128
|
+
self,
|
|
1129
|
+
inverter_sn: str,
|
|
1130
|
+
amperes: int,
|
|
1131
|
+
*,
|
|
1132
|
+
validate_battery_limits: bool = True,
|
|
1133
|
+
) -> SuccessResponse:
|
|
1134
|
+
"""Set battery discharge current limit.
|
|
1135
|
+
|
|
1136
|
+
Controls the maximum current allowed to discharge from batteries.
|
|
1137
|
+
|
|
1138
|
+
Common use cases:
|
|
1139
|
+
- Preserve battery capacity during grid outages
|
|
1140
|
+
- Extend battery lifespan (conservative discharge)
|
|
1141
|
+
- Emergency power management
|
|
1142
|
+
- Peak load management
|
|
1143
|
+
|
|
1144
|
+
Args:
|
|
1145
|
+
inverter_sn: Inverter serial number
|
|
1146
|
+
amperes: Discharge current limit (0-250 A)
|
|
1147
|
+
validate_battery_limits: Warn if value exceeds typical battery limits
|
|
1148
|
+
|
|
1149
|
+
Returns:
|
|
1150
|
+
SuccessResponse: Operation result
|
|
1151
|
+
|
|
1152
|
+
Raises:
|
|
1153
|
+
ValueError: If amperes not in valid range (0-250 A)
|
|
1154
|
+
|
|
1155
|
+
Warning:
|
|
1156
|
+
Never exceed your battery's maximum discharge current rating.
|
|
1157
|
+
Check battery manufacturer specifications.
|
|
1158
|
+
|
|
1159
|
+
Example:
|
|
1160
|
+
>>> # Conservative discharge for battery longevity
|
|
1161
|
+
>>> await client.control.set_battery_discharge_current("1234567890", 150)
|
|
1162
|
+
SuccessResponse(success=True)
|
|
1163
|
+
|
|
1164
|
+
>>> # Minimal discharge during grid outage
|
|
1165
|
+
>>> await client.control.set_battery_discharge_current("1234567890", 50)
|
|
1166
|
+
SuccessResponse(success=True)
|
|
1167
|
+
"""
|
|
1168
|
+
if not (0 <= amperes <= 250):
|
|
1169
|
+
raise ValueError(f"Battery discharge current must be between 0-250 A, got {amperes}")
|
|
1170
|
+
|
|
1171
|
+
if validate_battery_limits and amperes > 200:
|
|
1172
|
+
_LOGGER.warning(
|
|
1173
|
+
"Setting battery discharge current to %d A. "
|
|
1174
|
+
"Ensure this does not exceed your battery's maximum rating.",
|
|
1175
|
+
amperes,
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
return await self.write_parameter(
|
|
1179
|
+
inverter_sn, "HOLD_LEAD_ACID_DISCHARGE_RATE", str(amperes)
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
async def get_battery_charge_current(self, inverter_sn: str) -> int:
|
|
1183
|
+
"""Get current battery charge current limit.
|
|
1184
|
+
|
|
1185
|
+
Args:
|
|
1186
|
+
inverter_sn: Inverter serial number
|
|
1187
|
+
|
|
1188
|
+
Returns:
|
|
1189
|
+
int: Current charge current limit in Amperes (0-250 A)
|
|
1190
|
+
|
|
1191
|
+
Example:
|
|
1192
|
+
>>> current = await client.control.get_battery_charge_current("1234567890")
|
|
1193
|
+
>>> print(f"Charge limit: {current} A (~{current * 0.048:.1f} kW at 48V)")
|
|
1194
|
+
Charge limit: 200 A (~9.6 kW at 48V)
|
|
1195
|
+
"""
|
|
1196
|
+
params = await self.read_device_parameters_ranges(inverter_sn)
|
|
1197
|
+
return int(params.get("HOLD_LEAD_ACID_CHARGE_RATE", 200))
|
|
1198
|
+
|
|
1199
|
+
async def get_battery_discharge_current(self, inverter_sn: str) -> int:
|
|
1200
|
+
"""Get current battery discharge current limit.
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
inverter_sn: Inverter serial number
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
int: Current discharge current limit in Amperes (0-250 A)
|
|
1207
|
+
|
|
1208
|
+
Example:
|
|
1209
|
+
>>> current = await client.control.get_battery_discharge_current("1234567890")
|
|
1210
|
+
>>> print(f"Discharge limit: {current} A")
|
|
1211
|
+
Discharge limit: 200 A
|
|
1212
|
+
"""
|
|
1213
|
+
params = await self.read_device_parameters_ranges(inverter_sn)
|
|
1214
|
+
return int(params.get("HOLD_LEAD_ACID_DISCHARGE_RATE", 200))
|
|
1215
|
+
|
|
1216
|
+
# ============================================================================
|
|
1217
|
+
# System SOC Limit Controls
|
|
1218
|
+
# ============================================================================
|
|
1219
|
+
|
|
1220
|
+
async def set_system_charge_soc_limit(
|
|
1221
|
+
self,
|
|
1222
|
+
inverter_sn: str,
|
|
1223
|
+
percent: int,
|
|
1224
|
+
) -> SuccessResponse:
|
|
1225
|
+
"""Set the system charge SOC limit.
|
|
1226
|
+
|
|
1227
|
+
Controls the maximum State of Charge (SOC) percentage the battery will
|
|
1228
|
+
charge to during normal operation.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
inverter_sn: Inverter serial number
|
|
1232
|
+
percent: Target SOC limit (0-101%)
|
|
1233
|
+
- 0-100: Stop charging when battery reaches this SOC
|
|
1234
|
+
- 101: Special value to enable top balancing (allows full charge
|
|
1235
|
+
with cell balancing for lithium batteries)
|
|
1236
|
+
|
|
1237
|
+
Returns:
|
|
1238
|
+
SuccessResponse: Operation result
|
|
1239
|
+
|
|
1240
|
+
Raises:
|
|
1241
|
+
ValueError: If percent not in valid range (0-101)
|
|
1242
|
+
|
|
1243
|
+
Note:
|
|
1244
|
+
Setting 101% enables top balancing mode, which allows the battery
|
|
1245
|
+
management system to fully charge and balance individual cells.
|
|
1246
|
+
This is recommended periodically for lithium battery health.
|
|
1247
|
+
|
|
1248
|
+
Example:
|
|
1249
|
+
>>> # Limit charging to 90% for daily use (extends battery life)
|
|
1250
|
+
>>> await client.control.set_system_charge_soc_limit("1234567890", 90)
|
|
1251
|
+
SuccessResponse(success=True)
|
|
1252
|
+
|
|
1253
|
+
>>> # Enable top balancing (charge to 100% with cell balancing)
|
|
1254
|
+
>>> await client.control.set_system_charge_soc_limit("1234567890", 101)
|
|
1255
|
+
SuccessResponse(success=True)
|
|
1256
|
+
"""
|
|
1257
|
+
if not (0 <= percent <= 101):
|
|
1258
|
+
raise ValueError(
|
|
1259
|
+
f"System charge SOC limit must be between 0-101%, got {percent}. "
|
|
1260
|
+
"Use 101 for top balancing mode."
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
return await self.write_parameter(inverter_sn, "HOLD_SYSTEM_CHARGE_SOC_LIMIT", str(percent))
|
|
1264
|
+
|
|
1265
|
+
async def get_system_charge_soc_limit(self, inverter_sn: str) -> int:
|
|
1266
|
+
"""Get the current system charge SOC limit.
|
|
1267
|
+
|
|
1268
|
+
Args:
|
|
1269
|
+
inverter_sn: Inverter serial number
|
|
1270
|
+
|
|
1271
|
+
Returns:
|
|
1272
|
+
int: Current charge SOC limit (0-101%)
|
|
1273
|
+
- 0-100: Normal SOC limit
|
|
1274
|
+
- 101: Top balancing mode enabled
|
|
1275
|
+
|
|
1276
|
+
Example:
|
|
1277
|
+
>>> limit = await client.control.get_system_charge_soc_limit("1234567890")
|
|
1278
|
+
>>> if limit == 101:
|
|
1279
|
+
>>> print("Top balancing enabled")
|
|
1280
|
+
>>> else:
|
|
1281
|
+
>>> print(f"Charge limit: {limit}%")
|
|
1282
|
+
"""
|
|
1283
|
+
params = await self.read_device_parameters_ranges(inverter_sn)
|
|
1284
|
+
return int(params.get("HOLD_SYSTEM_CHARGE_SOC_LIMIT", 100))
|