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/__init__.py +49 -14
- goodwe/const.py +23 -17
- goodwe/dt.py +230 -110
- goodwe/es.py +235 -106
- goodwe/et.py +532 -201
- goodwe/exceptions.py +6 -3
- goodwe/inverter.py +209 -24
- goodwe/modbus.py +1 -0
- goodwe/model.py +106 -22
- goodwe/protocol.py +26 -34
- goodwe/sensor.py +83 -67
- {goodwe-0.4.8.dist-info → goodwe-0.4.9.dist-info}/METADATA +4 -4
- goodwe-0.4.9.dist-info/RECORD +16 -0
- {goodwe-0.4.8.dist-info → goodwe-0.4.9.dist-info}/WHEEL +1 -1
- goodwe-0.4.8.dist-info/RECORD +0 -16
- {goodwe-0.4.8.dist-info → goodwe-0.4.9.dist-info/licenses}/LICENSE +0 -0
- {goodwe-0.4.8.dist-info → goodwe-0.4.9.dist-info}/top_level.txt +0 -0
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
542
|
+
if self.request[5] == 6:
|
|
547
543
|
return f'READ runtime data ({self.request.hex()})'
|
|
548
|
-
|
|
544
|
+
if self.request[5] == 9:
|
|
549
545
|
return f'READ settings ({self.request.hex()})'
|
|
550
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
if value in (1, -2):
|
|
33
33
|
return ScheduleType.DRY_CONTACT_LOAD
|
|
34
|
-
|
|
34
|
+
if value in (2, -3):
|
|
35
35
|
return ScheduleType.DRY_CONTACT_SMART_LOAD
|
|
36
|
-
|
|
36
|
+
if value in (3, -4):
|
|
37
37
|
return ScheduleType.PEAK_SHAVING
|
|
38
|
-
|
|
38
|
+
if value in (4, -5):
|
|
39
39
|
return ScheduleType.BACKUP_MODE
|
|
40
|
-
|
|
40
|
+
if value in (5, -6):
|
|
41
41
|
return ScheduleType.SMART_CHARGE_MODE
|
|
42
|
-
|
|
42
|
+
if value in (6, -7):
|
|
43
43
|
return ScheduleType.ECO_MODE_745
|
|
44
|
-
|
|
44
|
+
if value == 85:
|
|
45
45
|
return ScheduleType.NOT_SET
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
if self == ScheduleType.ECO_MODE_745:
|
|
61
59
|
return int(value / 10)
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
if self == ScheduleType.PEAK_SHAVING:
|
|
73
70
|
return int(value / 10)
|
|
74
|
-
|
|
71
|
+
if self == ScheduleType.ECO_MODE_745:
|
|
75
72
|
return value * 10
|
|
76
|
-
|
|
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
|
-
|
|
79
|
+
if self == ScheduleType.ECO_MODE_745:
|
|
84
80
|
return -1000 <= value <= 1000
|
|
85
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
990
|
+
if value >= 90:
|
|
974
991
|
return 1
|
|
975
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: goodwe
|
|
3
|
-
Version: 0.4.
|
|
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:
|
|
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,,
|
goodwe-0.4.8.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|