goodwe 0.3.2__py3-none-any.whl → 0.3.4__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")
@@ -95,7 +95,7 @@ class ES(Inverter):
95
95
  Power("pback_up", 81, "Back-up Power", Kind.UPS),
96
96
  # pload + pback_up
97
97
  Calculated("plant_power",
98
- lambda data: round(read_bytes2(data, 47) + read_bytes2(data, 81)),
98
+ lambda data: round(read_bytes2(data, 47, 0) + read_bytes2(data, 81, 0)),
99
99
  "Plant Power", "W", Kind.AC),
100
100
  Decimal("meter_power_factor", 83, 1000, "Meter Power Factor", "", Kind.GRID), # modbus 0x531
101
101
  # Integer("xx85", 85, "Unknown sensor@85"),
@@ -135,7 +135,7 @@ class ES(Inverter):
135
135
  Integer("charge_i", 26, "Charge Current", "A", ),
136
136
  Integer("discharge_i", 28, "Discharge Current", "A", ),
137
137
  Decimal("discharge_v", 30, 10, "Discharge Voltage", "V"),
138
- Calculated("dod", lambda data: 100 - read_bytes2(data, 32), "Depth of Discharge", "%"),
138
+ Calculated("dod", lambda data: 100 - read_bytes2(data, 32, 0), "Depth of Discharge", "%"),
139
139
  Integer("battery_activated", 34, "Battery Activated"),
140
140
  Integer("bp_off_grid_charge", 36, "BP Off-grid Charge"),
141
141
  Integer("bp_pv_discharge", 38, "BP PV Discharge"),
@@ -285,19 +285,25 @@ class ES(Inverter):
285
285
  async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
286
286
  result = [e for e in OperationMode]
287
287
  result.remove(OperationMode.PEAK_SHAVING)
288
+ result.remove(OperationMode.SELF_USE)
288
289
  if not include_emulated:
289
290
  result.remove(OperationMode.ECO_CHARGE)
290
291
  result.remove(OperationMode.ECO_DISCHARGE)
291
292
  return tuple(result)
292
293
 
293
294
  async def get_operation_mode(self) -> OperationMode:
294
- mode = OperationMode(await self.read_setting('work_mode'))
295
+ mode_id = await self.read_setting('work_mode')
296
+ try:
297
+ mode = OperationMode(mode_id)
298
+ except ValueError:
299
+ logger.debug("Unknown work_mode value %d", mode_id)
300
+ return None
295
301
  if OperationMode.ECO != mode:
296
302
  return mode
297
- ecomode = await self.read_setting('eco_mode_1')
298
- if ecomode.is_eco_charge_mode():
303
+ eco_mode = await self.read_setting('eco_mode_1')
304
+ if eco_mode.is_eco_charge_mode():
299
305
  return OperationMode.ECO_CHARGE
300
- elif ecomode.is_eco_discharge_mode():
306
+ elif eco_mode.is_eco_discharge_mode():
301
307
  return OperationMode.ECO_DISCHARGE
302
308
  else:
303
309
  return OperationMode.ECO
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, ...] = (
@@ -35,10 +35,10 @@ class ET(Inverter):
35
35
  # ppv1 + ppv2 + ppv3 + ppv4
36
36
  Calculated("ppv",
37
37
  lambda data:
38
- max(0, read_bytes4(data, 35105)) +
39
- max(0, read_bytes4(data, 35109)) +
40
- max(0, read_bytes4(data, 35113)) +
41
- max(0, read_bytes4(data, 35117)),
38
+ max(0, read_bytes4(data, 35105, 0)) +
39
+ max(0, read_bytes4(data, 35109, 0)) +
40
+ max(0, read_bytes4(data, 35113, 0)) +
41
+ max(0, read_bytes4(data, 35117, 0)),
42
42
  "PV Power", "W", Kind.PV),
43
43
  ByteH("pv4_mode", 35119, "PV4 Mode code", "", Kind.PV),
44
44
  EnumH("pv4_mode_label", 35119, PV_MODES, "PV4 Mode", Kind.PV),
@@ -145,10 +145,10 @@ class ET(Inverter):
145
145
  # ppv1 + ppv2 + ppv3 + ppv4 + pbattery1 - active_power
146
146
  Calculated("house_consumption",
147
147
  lambda data:
148
- read_bytes4(data, 35105) +
149
- read_bytes4(data, 35109) +
150
- read_bytes4(data, 35113) +
151
- read_bytes4(data, 35117) +
148
+ read_bytes4(data, 35105, 0) +
149
+ read_bytes4(data, 35109, 0) +
150
+ read_bytes4(data, 35113, 0) +
151
+ read_bytes4(data, 35117, 0) +
152
152
  read_bytes4_signed(data, 35182) -
153
153
  read_bytes2_signed(data, 35140),
154
154
  "House Consumption", "W", Kind.AC),
@@ -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):
@@ -598,19 +604,26 @@ class ET(Inverter):
598
604
  result = [e for e in OperationMode]
599
605
  if not self._has_peak_shaving:
600
606
  result.remove(OperationMode.PEAK_SHAVING)
607
+ if not is_745_platform(self):
608
+ result.remove(OperationMode.SELF_USE)
601
609
  if not include_emulated:
602
610
  result.remove(OperationMode.ECO_CHARGE)
603
611
  result.remove(OperationMode.ECO_DISCHARGE)
604
612
  return tuple(result)
605
613
 
606
614
  async def get_operation_mode(self) -> OperationMode:
607
- mode = OperationMode(await self.read_setting('work_mode'))
615
+ mode_id = await self.read_setting('work_mode')
616
+ try:
617
+ mode = OperationMode(mode_id)
618
+ except ValueError:
619
+ logger.debug("Unknown work_mode value %d", mode_id)
620
+ return None
608
621
  if OperationMode.ECO != mode:
609
622
  return mode
610
- ecomode = await self.read_setting('eco_mode_1')
611
- if ecomode.is_eco_charge_mode():
623
+ eco_mode = await self.read_setting('eco_mode_1')
624
+ if eco_mode.is_eco_charge_mode():
612
625
  return OperationMode.ECO_CHARGE
613
- elif ecomode.is_eco_discharge_mode():
626
+ elif eco_mode.is_eco_discharge_mode():
614
627
  return OperationMode.ECO_DISCHARGE
615
628
  else:
616
629
  return OperationMode.ECO
@@ -626,6 +639,7 @@ class ET(Inverter):
626
639
  await self._set_offline(True)
627
640
  await self.write_setting('backup_supply', 1)
628
641
  await self.write_setting('cold_start', 4)
642
+ await self._clear_battery_mode_param()
629
643
  elif operation_mode == OperationMode.BACKUP:
630
644
  await self.write_setting('work_mode', 2)
631
645
  await self._set_offline(False)
@@ -636,13 +650,24 @@ class ET(Inverter):
636
650
  elif operation_mode == OperationMode.PEAK_SHAVING:
637
651
  await self.write_setting('work_mode', 4)
638
652
  await self._set_offline(False)
653
+ await self._clear_battery_mode_param()
654
+ elif operation_mode == OperationMode.SELF_USE:
655
+ await self.write_setting('work_mode', 5)
656
+ await self._set_offline(False)
657
+ await self._clear_battery_mode_param()
639
658
  elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE):
640
659
  if eco_mode_power < 0 or eco_mode_power > 100:
641
660
  raise ValueError()
642
661
  if eco_mode_soc < 0 or eco_mode_soc > 100:
643
662
  raise ValueError()
663
+
644
664
  eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
645
- await self._read_setting(eco_mode)
665
+ # Load the current values to try to detect schedule type
666
+ try:
667
+ await self._read_setting(eco_mode)
668
+ except ValueError:
669
+ pass
670
+ eco_mode.set_schedule_type(ScheduleType.ECO_MODE, is_745_platform(self))
646
671
  if operation_mode == OperationMode.ECO_CHARGE:
647
672
  await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
648
673
  else:
goodwe/inverter.py CHANGED
@@ -76,8 +76,9 @@ 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
+ SELF_USE = 5
80
+ ECO_CHARGE = 98
81
+ ECO_DISCHARGE = 99
81
82
 
82
83
 
83
84
  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
@@ -179,7 +183,7 @@ class Energy(Sensor):
179
183
 
180
184
  def read_value(self, data: ProtocolResponse):
181
185
  value = read_bytes2(data)
182
- return float(value) / 10
186
+ return float(value) / 10 if value else None
183
187
 
184
188
 
185
189
  class Energy4(Sensor):
@@ -190,7 +194,7 @@ class Energy4(Sensor):
190
194
 
191
195
  def read_value(self, data: ProtocolResponse):
192
196
  value = read_bytes4(data)
193
- return float(value) / 10
197
+ return float(value) / 10 if value else None
194
198
 
195
199
 
196
200
  class Apparent(Sensor):
@@ -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, None, 0)
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, None, 0)
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):
@@ -384,7 +414,7 @@ class EnumH(Sensor):
384
414
 
385
415
 
386
416
  class EnumL(Sensor):
387
- """Sensor representing label from enumeration encoded in 1 bytes (low 8 bits of 16bit register)"""
417
+ """Sensor representing label from enumeration encoded in 1 byte (low 8 bits of 16bit register)"""
388
418
 
389
419
  def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
390
420
  super().__init__(id_, offset, name, 1, "", kind)
@@ -403,7 +433,7 @@ class Enum2(Sensor):
403
433
  self._labels: Dict = labels
404
434
 
405
435
  def read_value(self, data: ProtocolResponse):
406
- return self._labels.get(read_bytes2(data))
436
+ return self._labels.get(read_bytes2(data, None, 0))
407
437
 
408
438
 
409
439
  class EnumBitmap4(Sensor):
@@ -434,7 +464,8 @@ class EnumBitmap22(Sensor):
434
464
  raise NotImplementedError()
435
465
 
436
466
  def read(self, data: ProtocolResponse):
437
- return decode_bitmap(read_bytes2(data, self.offset) << 16 + read_bytes2(data, self._offsetL), self._labels)
467
+ return decode_bitmap(read_bytes2(data, self.offset, 0) << 16 + read_bytes2(data, self._offsetL, 0),
468
+ self._labels)
438
469
 
439
470
 
440
471
  class EnumCalculated(Sensor):
@@ -476,6 +507,14 @@ class EcoMode(ABC):
476
507
  def is_eco_discharge_mode(self) -> bool:
477
508
  """Answer if it represents the emulated 24/7 fulltime discharge mode"""
478
509
 
510
+ @abstractmethod
511
+ def get_schedule_type(self) -> ScheduleType:
512
+ """Answer the schedule type"""
513
+
514
+ @abstractmethod
515
+ def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
516
+ """Set the schedule type"""
517
+
479
518
 
480
519
  class EcoModeV1(Sensor, EcoMode):
481
520
  """Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
@@ -518,8 +557,6 @@ class EcoModeV1(Sensor, EcoMode):
518
557
  raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
519
558
  self.day_bits = read_byte(data)
520
559
  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
560
  return self
524
561
 
525
562
  def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
@@ -561,6 +598,14 @@ class EcoModeV1(Sensor, EcoMode):
561
598
  and self.day_bits == 127 \
562
599
  and self.power > 0
563
600
 
601
+ def get_schedule_type(self) -> ScheduleType:
602
+ """Answer the schedule type"""
603
+ return ScheduleType.ECO_MODE
604
+
605
+ def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
606
+ """Set the schedule type"""
607
+ pass
608
+
564
609
  def as_eco_mode_v2(self) -> EcoModeV2:
565
610
  """Convert V1 to V2 EcoMode"""
566
611
  result = EcoModeV2(self.id_, self.offset, self.name)
@@ -617,8 +662,6 @@ class Schedule(Sensor, EcoMode):
617
662
  self.schedule_type = ScheduleType.detect_schedule_type(self.on_off)
618
663
  self.day_bits = read_byte(data)
619
664
  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
665
  self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
623
666
  if not self.schedule_type.is_in_range(self.power):
624
667
  raise ValueError(f"{self.id_}: power value {self.power} out of range.")
@@ -680,6 +723,19 @@ class Schedule(Sensor, EcoMode):
680
723
  and self.power > 0 \
681
724
  and (self.month_bits == 0 or self.month_bits == 0x0fff)
682
725
 
726
+ def get_schedule_type(self) -> ScheduleType:
727
+ """Answer the schedule type"""
728
+ return self.schedule_type
729
+
730
+ def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
731
+ """Set the schedule type"""
732
+ if schedule_type == ScheduleType.ECO_MODE:
733
+ # try to keep-reuse the type, use is745 only when necessary
734
+ if self.schedule_type not in (ScheduleType.ECO_MODE, ScheduleType.ECO_MODE_745):
735
+ self.schedule_type = ScheduleType.ECO_MODE_745 if is745 else ScheduleType.ECO_MODE
736
+ else:
737
+ self.schedule_type = schedule_type
738
+
683
739
  def as_eco_mode_v1(self) -> EcoModeV1:
684
740
  """Convert V2 to V1 EcoMode"""
685
741
  result = EcoModeV1(self.id_, self.offset, self.name)
@@ -730,12 +786,12 @@ def read_byte(buffer: ProtocolResponse, offset: int = None) -> int:
730
786
  return int.from_bytes(buffer.read(1), byteorder="big", signed=True)
731
787
 
732
788
 
733
- def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int:
789
+ def read_bytes2(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
734
790
  """Retrieve 2 byte (unsigned int) value from buffer"""
735
791
  if offset is not None:
736
792
  buffer.seek(offset)
737
793
  value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
738
- return value if value != 0xffff else 0
794
+ return undef if value == 0xffff else value
739
795
 
740
796
 
741
797
  def read_bytes2_signed(buffer: ProtocolResponse, offset: int = None) -> int:
@@ -745,12 +801,12 @@ def read_bytes2_signed(buffer: ProtocolResponse, offset: int = None) -> int:
745
801
  return int.from_bytes(buffer.read(2), byteorder="big", signed=True)
746
802
 
747
803
 
748
- def read_bytes4(buffer: ProtocolResponse, offset: int = None) -> int:
804
+ def read_bytes4(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
749
805
  """Retrieve 4 byte (unsigned int) value from buffer"""
750
806
  if offset is not None:
751
807
  buffer.seek(offset)
752
808
  value = int.from_bytes(buffer.read(4), byteorder="big", signed=False)
753
- return value if value != 0xffffffff else 0
809
+ return undef if value == 0xffffffff else value
754
810
 
755
811
 
756
812
  def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
@@ -891,6 +947,10 @@ def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
891
947
 
892
948
 
893
949
  def decode_day_of_week(data: int) -> str:
950
+ if data == -1:
951
+ return "Mon-Sun"
952
+ elif data == 0:
953
+ return ""
894
954
  bits = bin(data)[2:]
895
955
  daynames = list(DAY_NAMES)
896
956
  days = ""
@@ -904,7 +964,7 @@ def decode_day_of_week(data: int) -> str:
904
964
 
905
965
 
906
966
  def decode_months(data: int) -> str | None:
907
- if data == 0 or data == 0x0fff:
967
+ if data <= 0 or data == 0x0fff:
908
968
  return None
909
969
  bits = bin(data)[2:]
910
970
  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.4
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=XBP7txg9d4tMsFmHWs8LB4wdJmspKVD9ALfS9mePiJk,22650
5
+ goodwe/et.py,sha256=Ru227kF66U1P9R-9NvKyadeSCg_9va5nBrVYSMYrIhQ,39703
6
+ goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
+ goodwe/inverter.py,sha256=7DgIzSHimkVAfNyIkzALeukHOHkOuYjVyUIvuT0LHdE,10342
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=vFbsz4Dp0yw0rBNdKqzkMUupuBJWC17YcxvBrjvFAjU,36990
12
+ goodwe-0.3.4.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.3.4.dist-info/METADATA,sha256=pdVfstdVhotsFouFM6MknkbJaWr6YmgQ81kHSoJa3tw,3050
14
+ goodwe-0.3.4.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.3.4.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.3.4.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