goodwe 0.3.2__py3-none-any.whl → 0.3.3__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.
goodwe/es.py CHANGED
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
17
  class ES(Inverter):
18
- """Class representing inverter of ES/EM/BP family"""
18
+ """Class representing inverter of ES/EM/BP family AKA platform 105"""
19
19
 
20
20
  _READ_DEVICE_VERSION_INFO: ProtocolCommand = Aa55ProtocolCommand("010200", "0182")
21
21
  _READ_DEVICE_RUNNING_DATA: ProtocolCommand = Aa55ProtocolCommand("010600", "0186")
goodwe/et.py CHANGED
@@ -7,7 +7,7 @@ from .exceptions import RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
10
- from .model import is_2_battery, is_4_mppt, is_single_phase
10
+ from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
11
11
  from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
12
12
  from .sensor import *
13
13
 
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
17
  class ET(Inverter):
18
- """Class representing inverter of ET/EH/BT/BH or GE's GEH families"""
18
+ """Class representing inverter of ET/EH/BT/BH or GE's GEH families AKA platform 205 or 745"""
19
19
 
20
20
  # Modbus registers from offset 0x891c (35100), count 0x7d (125)
21
21
  __all_sensors: Tuple[Sensor, ...] = (
@@ -384,19 +384,25 @@ class ET(Inverter):
384
384
 
385
385
  Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
386
386
  Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
387
- Integer("load_control_soc", 47596, "Load Control SoC", "", Kind.AC),
387
+ Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
388
388
 
389
389
  Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
390
390
  )
391
391
 
392
392
  # Settings added in ARM firmware 22
393
393
  __settings_arm_fw_22: Tuple[Sensor, ...] = (
394
+ Long("peak_shaving_power_limit", 47542, "Peak Shaving Power Limit"),
395
+ Integer("peak_shaving_soc", 47544, "Peak Shaving SoC"),
394
396
  # EcoModeV2("eco_modeV2_5", 47571, "Eco Mode Version 2 Power Group 5"),
395
397
  # EcoModeV2("eco_modeV2_6", 47577, "Eco Mode Version 2 Power Group 6"),
396
398
  # EcoModeV2("eco_modeV2_7", 47583, "Eco Mode Version 2 Power Group 7"),
397
399
  PeakShavingMode("peak_shaving_mode", 47589, "Peak Shaving Mode"),
398
400
 
399
401
  Integer("dod_holding", 47602, "DoD Holding", "", Kind.BAT),
402
+ Integer("backup_mode_enable", 47605, "Backup Mode Switch"),
403
+ Integer("max_charge_power", 47606, "Max Charge Power"),
404
+ Integer("smart_charging_enable", 47609, "Smart Charging Mode Switch"),
405
+ Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
400
406
  )
401
407
 
402
408
  def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
@@ -626,6 +632,7 @@ class ET(Inverter):
626
632
  await self._set_offline(True)
627
633
  await self.write_setting('backup_supply', 1)
628
634
  await self.write_setting('cold_start', 4)
635
+ await self._clear_battery_mode_param()
629
636
  elif operation_mode == OperationMode.BACKUP:
630
637
  await self.write_setting('work_mode', 2)
631
638
  await self._set_offline(False)
@@ -636,13 +643,20 @@ class ET(Inverter):
636
643
  elif operation_mode == OperationMode.PEAK_SHAVING:
637
644
  await self.write_setting('work_mode', 4)
638
645
  await self._set_offline(False)
646
+ await self._clear_battery_mode_param()
639
647
  elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE):
640
648
  if eco_mode_power < 0 or eco_mode_power > 100:
641
649
  raise ValueError()
642
650
  if eco_mode_soc < 0 or eco_mode_soc > 100:
643
651
  raise ValueError()
652
+
644
653
  eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
645
- await self._read_setting(eco_mode)
654
+ # Load the current values to try to detect schedule type
655
+ try:
656
+ await self._read_setting(eco_mode)
657
+ except ValueError:
658
+ pass
659
+ eco_mode.set_schedule_type(ScheduleType.ECO_MODE, is_745_platform(self))
646
660
  if operation_mode == OperationMode.ECO_CHARGE:
647
661
  await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
648
662
  else:
goodwe/inverter.py CHANGED
@@ -76,8 +76,8 @@ class OperationMode(IntEnum):
76
76
  BACKUP = 2
77
77
  ECO = 3
78
78
  PEAK_SHAVING = 4
79
- ECO_CHARGE = 5
80
- ECO_DISCHARGE = 6
79
+ ECO_CHARGE = 10
80
+ ECO_DISCHARGE = 11
81
81
 
82
82
 
83
83
  class Inverter(ABC):
goodwe/model.py CHANGED
@@ -1,27 +1,32 @@
1
+ # Serial number tags to identify inverter type
1
2
  from .inverter import Inverter
2
3
 
3
- # Serial number tags to identify inverter type
4
- ET_MODEL_TAGS = ["ETU", "ETL", "ETR", "ETC", "EHU", "EHR", "EHB", "BTU", "BTN", "BTC", "BHU", "AES", "ABP", "HHI",
5
- "HSB", "HUA", "CUA",
6
- "ESN", "EMN", "ERN", "EBN", # ES Gen 2
7
- "HLB", "HMB", "HBB", "SPN"] # Gen 2
8
- ES_MODEL_TAGS = ["ESU", "EMU", "ESA", "BPS", "BPU", "EMJ", "IJL"]
9
- DT_MODEL_TAGS = ["DTU", "DTS",
4
+ PLATFORM_105_MODELS = ("ESU", "EMU", "ESA", "BPS", "BPU", "EMJ", "IJL")
5
+ PLATFORM_205_MODELS = ("ETU", "ETL", "ETR", "BHN", "EHU", "BHU", "EHR", "BTU")
6
+ PLATFORM_745_LV_MODELS = ("ESN", "EBN", "EMN", "SPN", "ERN", "ESC", "HLB", "HMB", "HBB", "EOA")
7
+ PLATFORM_745_HV_MODELS = ("ETT", "HTA", "HUB", "AEB", "SPB", "CUB", "EUB", "HEB", "ERB", "BTT", "ETF", "ARB", "URB",
8
+ "EBR")
9
+ PLATFORM_753_MODELS = ("AES", "HHI", "ABP", "EHB", "HSB", "HUA", "CUA")
10
+
11
+ ET_MODEL_TAGS = PLATFORM_205_MODELS + PLATFORM_745_LV_MODELS + PLATFORM_745_HV_MODELS + PLATFORM_753_MODELS + (
12
+ "ETC", "BTC", "BTN") # Qianhai
13
+ ES_MODEL_TAGS = PLATFORM_105_MODELS
14
+ DT_MODEL_TAGS = ("DTU", "DTS",
10
15
  "MSU", "MST", "MSC", "DSN", "DTN", "DST", "NSU", "SSN", "SST", "SSX", "SSY",
11
- "PSB", "PSC"]
16
+ "PSB", "PSC")
12
17
 
13
- SINGLE_PHASE_MODELS = ["DSN", "DST", "NSU", "SSN", "SST", "SSX", "SSY", # DT
18
+ SINGLE_PHASE_MODELS = ("DSN", "DST", "NSU", "SSN", "SST", "SSX", "SSY", # DT
14
19
  "MSU", "MST", "PSB", "PSC",
15
20
  "MSC", # Found on third gen MS
16
21
  "EHU", "EHR", "HSB", # ET
17
- "ESN", "EMN", "ERN", "EBN", "HLB", "HMB", "HBB", "SPN"] # ES Gen 2
22
+ "ESN", "EMN", "ERN", "EBN", "HLB", "HMB", "HBB", "SPN") # ES Gen 2
18
23
 
19
- MPPT3_MODELS = ["MSU", "MST", "PSC", "MSC",
20
- "25KET", "29K9ET"]
24
+ MPPT3_MODELS = ("MSU", "MST", "PSC", "MSC",
25
+ "25KET", "29K9ET")
21
26
 
22
- MPPT4_MODELS = ["HSB"]
27
+ MPPT4_MODELS = ("HSB",)
23
28
 
24
- BAT_2_MODELS = ["25KET", "29K9ET"]
29
+ BAT_2_MODELS = ("25KET", "29K9ET")
25
30
 
26
31
 
27
32
  def is_single_phase(inverter: Inverter) -> bool:
@@ -38,3 +43,8 @@ def is_4_mppt(inverter: Inverter) -> bool:
38
43
 
39
44
  def is_2_battery(inverter: Inverter) -> bool:
40
45
  return any(model in inverter.serial_number for model in BAT_2_MODELS)
46
+
47
+
48
+ def is_745_platform(inverter: Inverter) -> bool:
49
+ return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any(
50
+ model in inverter.serial_number for model in PLATFORM_745_HV_MODELS)
goodwe/sensor.py CHANGED
@@ -21,12 +21,13 @@ class ScheduleType(IntEnum):
21
21
  PEAK_SHAVING = 3,
22
22
  BACKUP_MODE = 4,
23
23
  SMART_CHARGE_MODE = 5,
24
- ECO_MODE_745 = 6
24
+ ECO_MODE_745 = 6,
25
+ NOT_SET = 85
25
26
 
26
27
  @classmethod
27
28
  def detect_schedule_type(cls, value: int) -> ScheduleType:
28
29
  """Detect schedule type from its on/off value"""
29
- if value in (0, -1, 85):
30
+ if value in (0, -1):
30
31
  return ScheduleType.ECO_MODE
31
32
  elif value in (1, -2):
32
33
  return ScheduleType.DRY_CONTACT_LOAD
@@ -40,6 +41,8 @@ class ScheduleType(IntEnum):
40
41
  return ScheduleType.SMART_CHARGE_MODE
41
42
  elif value in (6, -7):
42
43
  return ScheduleType.ECO_MODE_745
44
+ elif value == 85:
45
+ return ScheduleType.NOT_SET
43
46
  else:
44
47
  raise ValueError(f"{value}: on_off value {value} out of range.")
45
48
 
@@ -52,12 +55,13 @@ class ScheduleType(IntEnum):
52
55
 
53
56
  def decode_power(self, value: int) -> int:
54
57
  """Decode human readable value of power parameter"""
55
- if self == ScheduleType.ECO_MODE:
56
- return value
57
- elif self == ScheduleType.PEAK_SHAVING:
58
+ if self == ScheduleType.PEAK_SHAVING:
58
59
  return value * 10
59
- if self == ScheduleType.ECO_MODE_745:
60
+ elif self == ScheduleType.ECO_MODE_745:
60
61
  return int(value / 10)
62
+ elif self == ScheduleType.NOT_SET:
63
+ # Prevent out of range values when changing mode
64
+ return value if -100 <= value <= 100 else int(value / 10)
61
65
  else:
62
66
  return value
63
67
 
@@ -67,7 +71,7 @@ class ScheduleType(IntEnum):
67
71
  return value
68
72
  elif self == ScheduleType.PEAK_SHAVING:
69
73
  return int(value / 10)
70
- if self == ScheduleType.ECO_MODE_745:
74
+ elif self == ScheduleType.ECO_MODE_745:
71
75
  return value * 10
72
76
  else:
73
77
  return value
@@ -76,7 +80,7 @@ class ScheduleType(IntEnum):
76
80
  """Check if the value fits in allowed values range"""
77
81
  if self == ScheduleType.ECO_MODE:
78
82
  return -100 <= value <= 100
79
- if self == ScheduleType.ECO_MODE_745:
83
+ elif self == ScheduleType.ECO_MODE_745:
80
84
  return -1000 <= value <= 1000
81
85
  else:
82
86
  return True
@@ -298,6 +302,19 @@ class ByteL(Byte):
298
302
 
299
303
 
300
304
  class Integer(Sensor):
305
+ """Sensor representing unsigned int value encoded in 2 bytes"""
306
+
307
+ def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
308
+ super().__init__(id_, offset, name, 2, unit, kind)
309
+
310
+ def read_value(self, data: ProtocolResponse):
311
+ return read_bytes2(data)
312
+
313
+ def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
314
+ return int.to_bytes(int(value), length=2, byteorder="big", signed=False)
315
+
316
+
317
+ class IntegerS(Sensor):
301
318
  """Sensor representing signed int value encoded in 2 bytes"""
302
319
 
303
320
  def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
@@ -311,6 +328,19 @@ class Integer(Sensor):
311
328
 
312
329
 
313
330
  class Long(Sensor):
331
+ """Sensor representing unsigned int value encoded in 4 bytes"""
332
+
333
+ def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
334
+ super().__init__(id_, offset, name, 4, unit, kind)
335
+
336
+ def read_value(self, data: ProtocolResponse):
337
+ return read_bytes4(data)
338
+
339
+ def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
340
+ return int.to_bytes(int(value), length=4, byteorder="big", signed=False)
341
+
342
+
343
+ class LongS(Sensor):
314
344
  """Sensor representing signed int value encoded in 4 bytes"""
315
345
 
316
346
  def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
@@ -476,6 +506,14 @@ class EcoMode(ABC):
476
506
  def is_eco_discharge_mode(self) -> bool:
477
507
  """Answer if it represents the emulated 24/7 fulltime discharge mode"""
478
508
 
509
+ @abstractmethod
510
+ def get_schedule_type(self) -> ScheduleType:
511
+ """Answer the schedule type"""
512
+
513
+ @abstractmethod
514
+ def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
515
+ """Set the schedule type"""
516
+
479
517
 
480
518
  class EcoModeV1(Sensor, EcoMode):
481
519
  """Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
@@ -518,8 +556,6 @@ class EcoModeV1(Sensor, EcoMode):
518
556
  raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
519
557
  self.day_bits = read_byte(data)
520
558
  self.days = decode_day_of_week(self.day_bits)
521
- if self.day_bits < 0:
522
- raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
523
559
  return self
524
560
 
525
561
  def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
@@ -561,6 +597,14 @@ class EcoModeV1(Sensor, EcoMode):
561
597
  and self.day_bits == 127 \
562
598
  and self.power > 0
563
599
 
600
+ def get_schedule_type(self) -> ScheduleType:
601
+ """Answer the schedule type"""
602
+ return ScheduleType.ECO_MODE
603
+
604
+ def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
605
+ """Set the schedule type"""
606
+ pass
607
+
564
608
  def as_eco_mode_v2(self) -> EcoModeV2:
565
609
  """Convert V1 to V2 EcoMode"""
566
610
  result = EcoModeV2(self.id_, self.offset, self.name)
@@ -617,8 +661,6 @@ class Schedule(Sensor, EcoMode):
617
661
  self.schedule_type = ScheduleType.detect_schedule_type(self.on_off)
618
662
  self.day_bits = read_byte(data)
619
663
  self.days = decode_day_of_week(self.day_bits)
620
- if self.day_bits < 0:
621
- raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
622
664
  self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
623
665
  if not self.schedule_type.is_in_range(self.power):
624
666
  raise ValueError(f"{self.id_}: power value {self.power} out of range.")
@@ -680,6 +722,19 @@ class Schedule(Sensor, EcoMode):
680
722
  and self.power > 0 \
681
723
  and (self.month_bits == 0 or self.month_bits == 0x0fff)
682
724
 
725
+ def get_schedule_type(self) -> ScheduleType:
726
+ """Answer the schedule type"""
727
+ return self.schedule_type
728
+
729
+ def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
730
+ """Set the schedule type"""
731
+ if schedule_type == ScheduleType.ECO_MODE:
732
+ # try to keep-reuse the type, use is745 only when necessary
733
+ if self.schedule_type not in (ScheduleType.ECO_MODE, ScheduleType.ECO_MODE_745):
734
+ self.schedule_type = ScheduleType.ECO_MODE_745 if is745 else ScheduleType.ECO_MODE
735
+ else:
736
+ self.schedule_type = schedule_type
737
+
683
738
  def as_eco_mode_v1(self) -> EcoModeV1:
684
739
  """Convert V2 to V1 EcoMode"""
685
740
  result = EcoModeV1(self.id_, self.offset, self.name)
@@ -891,6 +946,10 @@ def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
891
946
 
892
947
 
893
948
  def decode_day_of_week(data: int) -> str:
949
+ if data == -1:
950
+ return "Mon-Sun"
951
+ elif data == 0:
952
+ return ""
894
953
  bits = bin(data)[2:]
895
954
  daynames = list(DAY_NAMES)
896
955
  days = ""
@@ -904,7 +963,7 @@ def decode_day_of_week(data: int) -> str:
904
963
 
905
964
 
906
965
  def decode_months(data: int) -> str | None:
907
- if data == 0 or data == 0x0fff:
966
+ if data <= 0 or data == 0x0fff:
908
967
  return None
909
968
  bits = bin(data)[2:]
910
969
  monthnames = list(MONTH_NAMES)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Read data from GoodWe inverter via local network
5
5
  Home-page: https://github.com/marcelblijleven/goodwe
6
6
  Author: Martin Letenay, Marcel Blijleven
@@ -0,0 +1,16 @@
1
+ goodwe/__init__.py,sha256=PInrrZEpTmMOQKk494vIz8EKSaw_qLBNz-6t9eLIUcg,5642
2
+ goodwe/const.py,sha256=Nw-nd4UJuqUOLfbmOrxTHEdS1AuaTDSpZzQqR6tBb8w,7912
3
+ goodwe/dt.py,sha256=bI53MVdZjtxTYU2qJLO8icsvF6UiXrkgH95V3iUwXT0,10581
4
+ goodwe/es.py,sha256=KZLBydRSzzAnbI8-o_-sKiJeunJGuCKr7N8V8_Ft__U,22434
5
+ goodwe/et.py,sha256=P4H5Q17Gx8V8Y9os5HfiECsVIZ1znx13pGRFzc93n54,39228
6
+ goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
+ goodwe/inverter.py,sha256=uiQdti_4lGVLrSG94GgxgALWuevY6hl9ekdHQTomR-0,10325
8
+ goodwe/modbus.py,sha256=ZPib-zKnOVE5zc0RNnhlf0w_26QBees1ScWGo6bAj0o,4685
9
+ goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
+ goodwe/protocol.py,sha256=pUkXTP2DqpKXGO7rbRfHq1x82Y1QM6OiRVx8cAtS0sM,13162
11
+ goodwe/sensor.py,sha256=cPZCC3_9gk5zPVTkzhuDa6byz_dM4xCyE8FtV5nxqgg,36845
12
+ goodwe-0.3.3.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.3.3.dist-info/METADATA,sha256=0sEhEL3Uv3TSMsOTCqZSE1YEpeoDD609fcQodacEIvs,3050
14
+ goodwe-0.3.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.3.3.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.3.3.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- goodwe/__init__.py,sha256=PInrrZEpTmMOQKk494vIz8EKSaw_qLBNz-6t9eLIUcg,5642
2
- goodwe/const.py,sha256=Nw-nd4UJuqUOLfbmOrxTHEdS1AuaTDSpZzQqR6tBb8w,7912
3
- goodwe/dt.py,sha256=bI53MVdZjtxTYU2qJLO8icsvF6UiXrkgH95V3iUwXT0,10581
4
- goodwe/es.py,sha256=d0RyW70dnxaMNVZzSxE7eEoW2dMDzQQxA4MlSADOyuo,22417
5
- goodwe/et.py,sha256=zQUZAhaC4HUV_0hzF6M85ffXasBBKSbpNkT_x37EZjw,38443
6
- goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
- goodwe/inverter.py,sha256=HvVtFBz5Zvl1je1V2AuLqpoB1vsur_gDHu-6SjNbJ8o,10323
8
- goodwe/modbus.py,sha256=ZPib-zKnOVE5zc0RNnhlf0w_26QBees1ScWGo6bAj0o,4685
9
- goodwe/model.py,sha256=Yy662c6VpuLozxo1Q7Llw1g34UhT5qJd9_rITIjmEd4,1523
10
- goodwe/protocol.py,sha256=pUkXTP2DqpKXGO7rbRfHq1x82Y1QM6OiRVx8cAtS0sM,13162
11
- goodwe/sensor.py,sha256=M8v3flB4V7VxY91G1m2a1tdaD5j_lqtVzkxxrcTFHmE,34696
12
- goodwe-0.3.2.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.3.2.dist-info/METADATA,sha256=izdqayJnYLUwJt9DPS2oUrgMUxGzNNIDMmokDNb3RNs,3050
14
- goodwe-0.3.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.3.2.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.3.2.dist-info/RECORD,,
File without changes