goodwe 0.4.8__py3-none-any.whl → 0.4.9__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/protocol.py CHANGED
@@ -1,3 +1,4 @@
1
+ """Low level IP communication protocol implementation."""
1
2
  from __future__ import annotations
2
3
 
3
4
  import asyncio
@@ -6,7 +7,7 @@ import logging
6
7
  import platform
7
8
  import socket
8
9
  from asyncio.futures import Future
9
- from typing import Tuple, Optional, Callable
10
+ from typing import Optional, Callable
10
11
 
11
12
  from .exceptions import MaxRetriesException, PartialResponseException, RequestFailedException, RequestRejectedException
12
13
  from .modbus import create_modbus_rtu_request, create_modbus_rtu_multi_request, create_modbus_tcp_request, \
@@ -55,12 +56,11 @@ class InverterProtocol:
55
56
  """
56
57
  if self._lock and self._running_loop == asyncio.get_event_loop():
57
58
  return self._lock
58
- else:
59
- logger.debug("Creating lock instance for current event loop.")
60
- self._lock = asyncio.Lock()
61
- self._running_loop = asyncio.get_event_loop()
62
- self._close_transport()
63
- return self._lock
59
+ logger.debug("Creating lock instance for current event loop.")
60
+ self._lock = asyncio.Lock()
61
+ self._running_loop = asyncio.get_event_loop()
62
+ self._close_transport()
63
+ return self._lock
64
64
 
65
65
  def _max_retries_reached(self) -> Future:
66
66
  logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
@@ -121,9 +121,11 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
121
121
 
122
122
  async def _connect(self) -> None:
123
123
  if not self._transport or self._transport.is_closing():
124
+ allow_broadcast = platform.system() == "Darwin" and self._host == "255.255.255.255"
124
125
  self._transport, self.protocol = await asyncio.get_running_loop().create_datagram_endpoint(
125
126
  lambda: self,
126
127
  remote_addr=(self._host, self._port),
128
+ allow_broadcast=allow_broadcast,
127
129
  )
128
130
 
129
131
  def connection_made(self, transport: asyncio.DatagramTransport) -> None:
@@ -138,7 +140,7 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
138
140
  logger.debug("Socket closed.")
139
141
  self._close_transport()
140
142
 
141
- def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
143
+ def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
142
144
  """On datagram received"""
143
145
  if self._timer:
144
146
  self._timer.cancel()
@@ -192,8 +194,7 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
192
194
  if not self.keep_alive:
193
195
  self._close_transport()
194
196
  return await self.send_request(command)
195
- else:
196
- return self._max_retries_reached()
197
+ return self._max_retries_reached()
197
198
  finally:
198
199
  if self._lock and self._lock.locked():
199
200
  self._lock.release()
@@ -271,7 +272,6 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
271
272
  def connection_made(self, transport: asyncio.DatagramTransport) -> None:
272
273
  """On connection made"""
273
274
  logger.debug("Connection opened.")
274
- pass
275
275
 
276
276
  def eof_received(self) -> None:
277
277
  logger.debug("EOF received.")
@@ -340,8 +340,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
340
340
  self._lock.release()
341
341
  self._close_transport()
342
342
  return await self.send_request(command)
343
- else:
344
- return self._max_retries_reached()
343
+ return self._max_retries_reached()
345
344
  except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError):
346
345
  if self._retry < self.retries:
347
346
  logger.debug("Connection refused error.")
@@ -349,8 +348,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
349
348
  if self._lock and self._lock.locked():
350
349
  self._lock.release()
351
350
  return await self.send_request(command)
352
- else:
353
- return self._max_retries_reached()
351
+ return self._max_retries_reached()
354
352
  finally:
355
353
  if self._lock and self._lock.locked():
356
354
  self._lock.release()
@@ -402,8 +400,7 @@ class ProtocolResponse:
402
400
  def response_data(self) -> bytes:
403
401
  if self.command is not None:
404
402
  return self.command.trim_response(self.raw_data)
405
- else:
406
- return self.raw_data
403
+ return self.raw_data
407
404
 
408
405
  def seek(self, address: int) -> None:
409
406
  if self.command is not None:
@@ -457,10 +454,9 @@ class ProtocolCommand:
457
454
  result = response_future.result()
458
455
  if result is not None:
459
456
  return ProtocolResponse(result, self)
460
- else:
461
- raise RequestFailedException(
462
- "No response received to '" + self.request.hex() + "' request."
463
- )
457
+ raise RequestFailedException(
458
+ "No response received to '" + self.request.hex() + "' request."
459
+ )
464
460
  except (asyncio.CancelledError, ConnectionRefusedError):
465
461
  raise RequestFailedException(
466
462
  "No valid response received to '" + self.request.hex() + "' request."
@@ -543,12 +539,11 @@ class Aa55ProtocolCommand(ProtocolCommand):
543
539
  if self.request[4] == 1:
544
540
  if self.request[5] == 2:
545
541
  return f'READ device info ({self.request.hex()})'
546
- elif self.request[5] == 6:
542
+ if self.request[5] == 6:
547
543
  return f'READ runtime data ({self.request.hex()})'
548
- elif self.request[5] == 9:
544
+ if self.request[5] == 9:
549
545
  return f'READ settings ({self.request.hex()})'
550
- else:
551
- return self.request.hex()
546
+ return self.request.hex()
552
547
 
553
548
 
554
549
  class Aa55ReadCommand(Aa55ProtocolCommand):
@@ -557,13 +552,12 @@ class Aa55ReadCommand(Aa55ProtocolCommand):
557
552
  """
558
553
 
559
554
  def __init__(self, offset: int, count: int):
560
- super().__init__("011A03" + "{:04x}".format(offset) + "{:02x}".format(count), "019A", offset, count)
555
+ super().__init__(f"011A03{offset:04x}{count:02x}", "019A", offset, count)
561
556
 
562
557
  def __repr__(self):
563
558
  if self.value > 1:
564
559
  return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
565
- else:
566
- return f'READ register {self.first_address} ({self.request.hex()})'
560
+ return f'READ register {self.first_address} ({self.request.hex()})'
567
561
 
568
562
 
569
563
  class Aa55WriteCommand(Aa55ProtocolCommand):
@@ -572,7 +566,7 @@ class Aa55WriteCommand(Aa55ProtocolCommand):
572
566
  """
573
567
 
574
568
  def __init__(self, register: int, value: int):
575
- super().__init__("023905" + "{:04x}".format(register) + "01" + "{:04x}".format(value), "02B9", register, value)
569
+ super().__init__(f"023905{register:04x}01{value:04x}", "02B9", register, value)
576
570
 
577
571
  def __repr__(self):
578
572
  return f'WRITE {self.value} to register {self.first_address} ({self.request.hex()})'
@@ -584,7 +578,7 @@ class Aa55WriteMultiCommand(Aa55ProtocolCommand):
584
578
  """
585
579
 
586
580
  def __init__(self, offset: int, values: bytes):
587
- super().__init__("02390B" + "{:04x}".format(offset) + "{:02x}".format(len(values)) + values.hex(),
581
+ super().__init__(f"02390B{offset:04x}{len(values):02x}{values.hex()}",
588
582
  "02B9", offset, len(values) // 2)
589
583
 
590
584
 
@@ -640,8 +634,7 @@ class ModbusRtuReadCommand(ModbusRtuProtocolCommand):
640
634
  def __repr__(self):
641
635
  if self.value > 1:
642
636
  return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
643
- else:
644
- return f'READ register {self.first_address} ({self.request.hex()})'
637
+ return f'READ register {self.first_address} ({self.request.hex()})'
645
638
 
646
639
 
647
640
  class ModbusRtuWriteCommand(ModbusRtuProtocolCommand):
@@ -710,8 +703,7 @@ class ModbusTcpReadCommand(ModbusTcpProtocolCommand):
710
703
  def __repr__(self):
711
704
  if self.value > 1:
712
705
  return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
713
- else:
714
- return f'READ register {self.first_address} ({self.request.hex()})'
706
+ return f'READ register {self.first_address} ({self.request.hex()})'
715
707
 
716
708
 
717
709
  class ModbusTcpWriteCommand(ModbusTcpProtocolCommand):
goodwe/sensor.py CHANGED
@@ -1,3 +1,4 @@
1
+ """Inverter sensor types."""
1
2
  from __future__ import annotations
2
3
 
3
4
  from abc import ABC, abstractmethod
@@ -6,7 +7,6 @@ from enum import IntEnum
6
7
  from struct import unpack
7
8
  from typing import Any, Callable, Optional
8
9
 
9
- from .const import *
10
10
  from .inverter import Sensor, SensorKind
11
11
  from .protocol import ProtocolResponse
12
12
 
@@ -15,13 +15,13 @@ MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "O
15
15
 
16
16
 
17
17
  class ScheduleType(IntEnum):
18
- ECO_MODE = 0,
19
- DRY_CONTACT_LOAD = 1,
20
- DRY_CONTACT_SMART_LOAD = 2,
21
- PEAK_SHAVING = 3,
22
- BACKUP_MODE = 4,
23
- SMART_CHARGE_MODE = 5,
24
- ECO_MODE_745 = 6,
18
+ ECO_MODE = 0
19
+ DRY_CONTACT_LOAD = 1
20
+ DRY_CONTACT_SMART_LOAD = 2
21
+ PEAK_SHAVING = 3
22
+ BACKUP_MODE = 4
23
+ SMART_CHARGE_MODE = 5
24
+ ECO_MODE_745 = 6
25
25
  NOT_SET = 85
26
26
 
27
27
  @classmethod
@@ -29,61 +29,56 @@ class ScheduleType(IntEnum):
29
29
  """Detect schedule type from its on/off value"""
30
30
  if value in (0, -1):
31
31
  return ScheduleType.ECO_MODE
32
- elif value in (1, -2):
32
+ if value in (1, -2):
33
33
  return ScheduleType.DRY_CONTACT_LOAD
34
- elif value in (2, -3):
34
+ if value in (2, -3):
35
35
  return ScheduleType.DRY_CONTACT_SMART_LOAD
36
- elif value in (3, -4):
36
+ if value in (3, -4):
37
37
  return ScheduleType.PEAK_SHAVING
38
- elif value in (4, -5):
38
+ if value in (4, -5):
39
39
  return ScheduleType.BACKUP_MODE
40
- elif value in (5, -6):
40
+ if value in (5, -6):
41
41
  return ScheduleType.SMART_CHARGE_MODE
42
- elif value in (6, -7):
42
+ if value in (6, -7):
43
43
  return ScheduleType.ECO_MODE_745
44
- elif value == 85:
44
+ if value == 85:
45
45
  return ScheduleType.NOT_SET
46
- else:
47
- raise ValueError(f"{value}: on_off value {value} out of range.")
46
+ raise ValueError(f"{value}: on_off value {value} out of range.")
48
47
 
49
48
  def power_unit(self):
50
49
  """Return unit of power parameter"""
51
50
  if self == ScheduleType.PEAK_SHAVING:
52
51
  return "W"
53
- else:
54
- return "%"
52
+ return "%"
55
53
 
56
54
  def decode_power(self, value: int) -> int:
57
55
  """Decode human readable value of power parameter"""
58
56
  if self == ScheduleType.PEAK_SHAVING:
59
57
  return value * 10
60
- elif self == ScheduleType.ECO_MODE_745:
58
+ if self == ScheduleType.ECO_MODE_745:
61
59
  return int(value / 10)
62
- elif self == ScheduleType.NOT_SET:
60
+ if self == ScheduleType.NOT_SET:
63
61
  # Prevent out of range values when changing mode
64
62
  return value if -100 <= value <= 100 else int(value / 10)
65
- else:
66
- return value
63
+ return value
67
64
 
68
65
  def encode_power(self, value: int) -> int:
69
66
  """Encode human readable value of power parameter"""
70
67
  if self == ScheduleType.ECO_MODE:
71
68
  return value
72
- elif self == ScheduleType.PEAK_SHAVING:
69
+ if self == ScheduleType.PEAK_SHAVING:
73
70
  return int(value / 10)
74
- elif self == ScheduleType.ECO_MODE_745:
71
+ if self == ScheduleType.ECO_MODE_745:
75
72
  return value * 10
76
- else:
77
- return value
73
+ return value
78
74
 
79
75
  def is_in_range(self, value: int) -> bool:
80
76
  """Check if the value fits in allowed values range"""
81
77
  if self == ScheduleType.ECO_MODE:
82
78
  return -100 <= value <= 100
83
- elif self == ScheduleType.ECO_MODE_745:
79
+ if self == ScheduleType.ECO_MODE_745:
84
80
  return -1000 <= value <= 1000
85
- else:
86
- return True
81
+ return True
87
82
 
88
83
 
89
84
  class Voltage(Sensor):
@@ -125,6 +120,19 @@ class CurrentS(Sensor):
125
120
  return encode_current_signed(value)
126
121
 
127
122
 
123
+ class CurrentSmA(Sensor):
124
+ """Sensor representing current [mA] value encoded in 2 (signed) bytes"""
125
+
126
+ def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
127
+ super().__init__(id_, offset, name, 2, "mA", kind)
128
+
129
+ def read_value(self, data: ProtocolResponse):
130
+ return read_current_signed(data)
131
+
132
+ def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
133
+ return encode_current_signed(value)
134
+
135
+
128
136
  class Frequency(Sensor):
129
137
  """Sensor representing frequency [Hz] value encoded in 2 bytes"""
130
138
 
@@ -197,6 +205,17 @@ class Energy4(Sensor):
197
205
  return float(value) / 10 if value is not None else None
198
206
 
199
207
 
208
+ class Energy4W(Sensor):
209
+ """Sensor representing meter energy [kWh] value encoded in 4 bytes"""
210
+
211
+ def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
212
+ super().__init__(id_, offset, name, 4, "kWh", kind)
213
+
214
+ def read_value(self, data: ProtocolResponse):
215
+ value = read_bytes4(data)
216
+ return float(value) / 1000 if value is not None else None
217
+
218
+
200
219
  class Energy8(Sensor):
201
220
  """Sensor representing energy [kWh] value encoded in 8 bytes"""
202
221
 
@@ -405,9 +424,9 @@ class Timestamp(Sensor):
405
424
  class Enum(Sensor):
406
425
  """Sensor representing label from enumeration encoded in 1 bytes"""
407
426
 
408
- def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
427
+ def __init__(self, id_: str, offset: int, labels: dict[int, str], name: str, kind: Optional[SensorKind] = None):
409
428
  super().__init__(id_, offset, name, 1, "", kind)
410
- self._labels: Dict = labels
429
+ self._labels: dict[int, str] = labels
411
430
 
412
431
  def read_value(self, data: ProtocolResponse):
413
432
  return self._labels.get(read_byte(data))
@@ -416,9 +435,9 @@ class Enum(Sensor):
416
435
  class EnumH(Sensor):
417
436
  """Sensor representing label from enumeration encoded in 1 (high 8 bits of 16bit register)"""
418
437
 
419
- def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
438
+ def __init__(self, id_: str, offset: int, labels: dict[int, str], name: str, kind: Optional[SensorKind] = None):
420
439
  super().__init__(id_, offset, name, 1, "", kind)
421
- self._labels: Dict = labels
440
+ self._labels: dict[int, str] = labels
422
441
 
423
442
  def read_value(self, data: ProtocolResponse):
424
443
  return self._labels.get(read_byte(data))
@@ -427,9 +446,9 @@ class EnumH(Sensor):
427
446
  class EnumL(Sensor):
428
447
  """Sensor representing label from enumeration encoded in 1 byte (low 8 bits of 16bit register)"""
429
448
 
430
- def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
449
+ def __init__(self, id_: str, offset: int, labels: dict[int, str], name: str, kind: Optional[SensorKind] = None):
431
450
  super().__init__(id_, offset, name, 1, "", kind)
432
- self._labels: Dict = labels
451
+ self._labels: dict[int, str] = labels
433
452
 
434
453
  def read_value(self, data: ProtocolResponse):
435
454
  read_byte(data)
@@ -439,9 +458,9 @@ class EnumL(Sensor):
439
458
  class Enum2(Sensor):
440
459
  """Sensor representing label from enumeration encoded in 2 bytes"""
441
460
 
442
- def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
461
+ def __init__(self, id_: str, offset: int, labels: dict[int, str], name: str, kind: Optional[SensorKind] = None):
443
462
  super().__init__(id_, offset, name, 2, "", kind)
444
- self._labels: Dict = labels
463
+ self._labels: dict[int, str] = labels
445
464
 
446
465
  def read_value(self, data: ProtocolResponse):
447
466
  return self._labels.get(read_bytes2(data, None, 0))
@@ -450,9 +469,9 @@ class Enum2(Sensor):
450
469
  class EnumBitmap4(Sensor):
451
470
  """Sensor representing label from bitmap encoded in 4 bytes"""
452
471
 
453
- def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
472
+ def __init__(self, id_: str, offset: int, labels: dict[int, str], name: str, kind: Optional[SensorKind] = None):
454
473
  super().__init__(id_, offset, name, 4, "", kind)
455
- self._labels: Dict = labels
474
+ self._labels: dict[int, str] = labels
456
475
 
457
476
  def read_value(self, data: ProtocolResponse) -> Any:
458
477
  raise NotImplementedError()
@@ -465,10 +484,10 @@ class EnumBitmap4(Sensor):
465
484
  class EnumBitmap22(Sensor):
466
485
  """Sensor representing label from bitmap encoded in 2+2 bytes"""
467
486
 
468
- def __init__(self, id_: str, offsetH: int, offsetL: int, labels: Dict, name: str,
487
+ def __init__(self, id_: str, offsetH: int, offsetL: int, labels: dict[int, str], name: str,
469
488
  kind: Optional[SensorKind] = None):
470
489
  super().__init__(id_, offsetH, name, 2, "", kind)
471
- self._labels: Dict = labels
490
+ self._labels: dict[int, str] = labels
472
491
  self._offsetL: int = offsetL
473
492
 
474
493
  def read_value(self, data: ProtocolResponse) -> Any:
@@ -482,11 +501,11 @@ class EnumBitmap22(Sensor):
482
501
  class EnumCalculated(Sensor):
483
502
  """Sensor representing label from enumeration of calculated value"""
484
503
 
485
- def __init__(self, id_: str, getter: Callable[[ProtocolResponse], Any], labels: Dict, name: str,
504
+ def __init__(self, id_: str, getter: Callable[[ProtocolResponse], Any], labels: dict[int, str], name: str,
486
505
  kind: Optional[SensorKind] = None):
487
506
  super().__init__(id_, 0, name, 0, "", kind)
488
507
  self._getter: Callable[[ProtocolResponse], Any] = getter
489
- self._labels: Dict = labels
508
+ self._labels: dict[int, str] = labels
490
509
 
491
510
  def read_value(self, data: ProtocolResponse) -> Any:
492
511
  raise NotImplementedError()
@@ -500,23 +519,23 @@ class EcoMode(ABC):
500
519
 
501
520
  @abstractmethod
502
521
  def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes:
503
- """Answer bytes representing all the time enabled charging eco mode group"""
522
+ """Answer bytes representing all the time enabled charging eco-mode group"""
504
523
 
505
524
  @abstractmethod
506
525
  def encode_discharge(self, eco_mode_power: int) -> bytes:
507
- """Answer bytes representing all the time enabled discharging eco mode group"""
526
+ """Answer bytes representing all the time enabled discharging eco-mode group"""
508
527
 
509
528
  @abstractmethod
510
529
  def encode_off(self) -> bytes:
511
- """Answer bytes representing empty and disabled eco mode group"""
530
+ """Answer bytes representing empty and disabled eco-mode group"""
512
531
 
513
532
  @abstractmethod
514
533
  def is_eco_charge_mode(self) -> bool:
515
- """Answer if it represents the emulated 24/7 fulltime discharge mode"""
534
+ """Answer if it represents the emulated 24/7 full-time discharge mode"""
516
535
 
517
536
  @abstractmethod
518
537
  def is_eco_discharge_mode(self) -> bool:
519
- """Answer if it represents the emulated 24/7 fulltime discharge mode"""
538
+ """Answer if it represents the emulated 24/7 full-time discharge mode"""
520
539
 
521
540
  @abstractmethod
522
541
  def get_schedule_type(self) -> ScheduleType:
@@ -586,19 +605,19 @@ class EcoModeV1(Sensor, EcoMode):
586
605
  raise ValueError
587
606
 
588
607
  def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes:
589
- """Answer bytes representing all the time enabled charging eco mode group"""
608
+ """Answer bytes representing all the time enabled charging eco-mode group"""
590
609
  return bytes.fromhex("0000173b{:04x}ff7f".format((-1 * abs(eco_mode_power)) & (2 ** 16 - 1)))
591
610
 
592
611
  def encode_discharge(self, eco_mode_power: int) -> bytes:
593
- """Answer bytes representing all the time enabled discharging eco mode group"""
612
+ """Answer bytes representing all the time enabled discharging eco-mode group"""
594
613
  return bytes.fromhex("0000173b{:04x}ff7f".format(abs(eco_mode_power)))
595
614
 
596
615
  def encode_off(self) -> bytes:
597
- """Answer bytes representing empty and disabled eco mode group"""
616
+ """Answer bytes representing empty and disabled eco-mode group"""
598
617
  return bytes.fromhex("3000300000640000")
599
618
 
600
619
  def is_eco_charge_mode(self) -> bool:
601
- """Answer if it represents the emulated 24/7 fulltime discharge mode"""
620
+ """Answer if it represents the emulated 24/7 full-time discharge mode"""
602
621
  return self.start_h == 0 \
603
622
  and self.start_m == 0 \
604
623
  and self.end_h == 23 \
@@ -608,7 +627,7 @@ class EcoModeV1(Sensor, EcoMode):
608
627
  and self.power < 0
609
628
 
610
629
  def is_eco_discharge_mode(self) -> bool:
611
- """Answer if it represents the emulated 24/7 fulltime discharge mode"""
630
+ """Answer if it represents the emulated 24/7 full-time discharge mode"""
612
631
  return self.start_h == 0 \
613
632
  and self.start_m == 0 \
614
633
  and self.end_h == 23 \
@@ -707,7 +726,7 @@ class Schedule(Sensor, EcoMode):
707
726
  raise ValueError
708
727
 
709
728
  def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes:
710
- """Answer bytes representing all the time enabled charging eco mode group"""
729
+ """Answer bytes representing all the time enabled charging eco-mode group"""
711
730
  return bytes.fromhex(
712
731
  "0000173b{:02x}7f{:04x}{:04x}{:04x}".format(
713
732
  255 - self.schedule_type,
@@ -716,7 +735,7 @@ class Schedule(Sensor, EcoMode):
716
735
  0 if self.schedule_type != ScheduleType.ECO_MODE_745 else 0x0fff))
717
736
 
718
737
  def encode_discharge(self, eco_mode_power: int) -> bytes:
719
- """Answer bytes representing all the time enabled discharging eco mode group"""
738
+ """Answer bytes representing all the time enabled discharging eco-mode group"""
720
739
  return bytes.fromhex("0000173b{:02x}7f{:04x}0064{:04x}".format(
721
740
  255 - self.schedule_type,
722
741
  abs(self.schedule_type.encode_power(eco_mode_power)),
@@ -729,7 +748,7 @@ class Schedule(Sensor, EcoMode):
729
748
  self.schedule_type.encode_power(100)))
730
749
 
731
750
  def is_eco_charge_mode(self) -> bool:
732
- """Answer if it represents the emulated 24/7 fulltime discharge mode"""
751
+ """Answer if it represents the emulated 24/7 full-time discharge mode"""
733
752
  return self.start_h == 0 \
734
753
  and self.start_m == 0 \
735
754
  and self.end_h == 23 \
@@ -740,7 +759,7 @@ class Schedule(Sensor, EcoMode):
740
759
  and (self.month_bits == 0 or self.month_bits == 0x0fff)
741
760
 
742
761
  def is_eco_discharge_mode(self) -> bool:
743
- """Answer if it represents the emulated 24/7 fulltime discharge mode"""
762
+ """Answer if it represents the emulated 24/7 full-time discharge mode"""
744
763
  return self.start_h == 0 \
745
764
  and self.start_m == 0 \
746
765
  and self.end_h == 23 \
@@ -873,8 +892,7 @@ def read_float4(buffer: ProtocolResponse, offset: int = None) -> float:
873
892
  data = buffer.read(4)
874
893
  if len(data) == 4:
875
894
  return unpack('>f', data)[0]
876
- else:
877
- return float(0)
895
+ return float(0)
878
896
 
879
897
 
880
898
  def read_voltage(buffer: ProtocolResponse, offset: int = None) -> float:
@@ -931,8 +949,7 @@ def read_temp(buffer: ProtocolResponse, offset: int = None) -> float | None:
931
949
  value = int.from_bytes(buffer.read(2), byteorder="big", signed=True)
932
950
  if value == -1 or value == 32767:
933
951
  return None
934
- else:
935
- return float(value) / 10
952
+ return float(value) / 10
936
953
 
937
954
 
938
955
  def read_datetime(buffer: ProtocolResponse, offset: int = None) -> datetime:
@@ -970,10 +987,9 @@ def read_grid_mode(buffer: ProtocolResponse, offset: int = None) -> int:
970
987
  value = read_bytes2_signed(buffer, offset)
971
988
  if value < -90:
972
989
  return 2
973
- elif value >= 90:
990
+ if value >= 90:
974
991
  return 1
975
- else:
976
- return 0
992
+ return 0
977
993
 
978
994
 
979
995
  def read_unsigned_int(data: bytes, offset: int) -> int:
@@ -981,7 +997,7 @@ def read_unsigned_int(data: bytes, offset: int) -> int:
981
997
  return int.from_bytes(data[offset:offset + 2], byteorder="big", signed=False)
982
998
 
983
999
 
984
- def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
1000
+ def decode_bitmap(value: int, bitmap: dict[int, str]) -> str:
985
1001
  bits = value
986
1002
  result = []
987
1003
  for i in range(32):
@@ -995,7 +1011,7 @@ def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
995
1011
  def decode_day_of_week(data: int) -> str:
996
1012
  if data == -1:
997
1013
  return "Mon-Sun"
998
- elif data == 0:
1014
+ if data == 0:
999
1015
  return ""
1000
1016
  bits = bin(data)[2:]
1001
1017
  daynames = list(DAY_NAMES)
@@ -1,16 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: goodwe
3
- Version: 0.4.8
3
+ Version: 0.4.9
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
7
- Author-email: 'marcelblijleven@gmail.com
7
+ Author-email: marcelblijleven@gmail.com
8
8
  License: MIT
9
9
  Keywords: GoodWe,Solar Panel,Inverter,Photovoltaics,PV
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
- Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Programming Language :: Python :: 3
15
14
  Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
@@ -21,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
21
20
  Requires-Python: >=3.8
22
21
  Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
23
+ Dynamic: license-file
24
24
 
25
25
  # GoodWe
26
26
 
@@ -0,0 +1,16 @@
1
+ goodwe/__init__.py,sha256=z3tGJH2PxQx8tmmkQ-4r0Y0Z6gFFilut2Il3_TL5LM4,6339
2
+ goodwe/const.py,sha256=g8AtKwrqcoC0YoQrBqWzQb72mt4Jxcn4N4CuWy3zr9k,8062
3
+ goodwe/dt.py,sha256=UIwrqi1eJ7O0S4hB9nzNNilQZ40sLIlH-jGJ4JlwAOs,18434
4
+ goodwe/es.py,sha256=4yWqxJgi8AYhktuHomcjieRjRchzInbZmA0Mbx8_W5Y,24463
5
+ goodwe/et.py,sha256=2kqlrXuoW5D3zjIX1TxiQhNVRbrkAPe9LCpT_nJJZSc,52596
6
+ goodwe/exceptions.py,sha256=Rw2R9SY1T6pDQ1OCQ-dZf-WcG2_WhM5Ensd4ZtYYykk,1436
7
+ goodwe/inverter.py,sha256=_BRjqVnz6dwS0OLbb_3IyyEmV9GuQJQlasNgiCoe4CM,16394
8
+ goodwe/modbus.py,sha256=qWkoxfdnCHvkdD15dt3emzpTPv2liQ_jfTDqrUrBMcU,8440
9
+ goodwe/model.py,sha256=Mw1u2FMA7RKwniL-baO4dydnfwmNbK_jI17AagVAmb8,2515
10
+ goodwe/protocol.py,sha256=TdvjnahOuH-9rXWUnWdKT49VV7xq67FGw4EvB9SHK9Q,30035
11
+ goodwe/sensor.py,sha256=WPVaaHKsLSu8xJdS6lYTVCVkzoRvo80Qh9CxwOUid5o,39287
12
+ goodwe-0.4.9.dist-info/licenses/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.4.9.dist-info/METADATA,sha256=ixBMzrLyiksoFS_1IB8XUdxMBr57PRHoj-GbevIUaPA,3346
14
+ goodwe-0.4.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ goodwe-0.4.9.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.4.9.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- goodwe/__init__.py,sha256=8fFGBBvBpCo6Ew4puTtW0kYo2hVPKUx6z5A-TA4Tbvc,5795
2
- goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
- goodwe/dt.py,sha256=IJxLDajuu2psYE5ZpwA2HFJDFdK5JIST6kUqWyRVBko,13663
4
- goodwe/es.py,sha256=vvHmxcFykp8nhR1I8p7SF0YcYpvdCKBYacgcolbVHXI,23009
5
- goodwe/et.py,sha256=Sdgqj13DXIg36NptkHMKxuP78oo4aUQ_6zlToyt78qI,46002
6
- goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
7
- goodwe/inverter.py,sha256=86aMJzJjNOr1I_tCF5H6mBwzDTjLbGDKUL2hbi0XSxg,10459
8
- goodwe/modbus.py,sha256=Mg_s_v8kbZgqXZM6ZUUxkZx2boAG8LkuDG5OiFKK2X4,8402
9
- goodwe/model.py,sha256=OAKfw6ggClgLR9JIdNd7tQ4pnh_7o_UqVdm1KOVsm-Y,2200
10
- goodwe/protocol.py,sha256=2xRo1H53G6T0ANSuYKPK_KTNfCVTctIU2ZHVu-CvMPk,30163
11
- goodwe/sensor.py,sha256=xeDZIwjJ_176ULrRXVCTYvVXx6o2_pWgS0KuR3PPQdg,38435
12
- goodwe-0.4.8.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.4.8.dist-info/METADATA,sha256=kla7IF_7dMZl-uEdQnGqMBXOkOyNlKt9Inwmi4BWCWQ,3376
14
- goodwe-0.4.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.4.8.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.4.8.dist-info/RECORD,,