goodwe-modbus 0.1.0__tar.gz
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_modbus-0.1.0/PKG-INFO +10 -0
- goodwe_modbus-0.1.0/README.md +0 -0
- goodwe_modbus-0.1.0/pyproject.toml +15 -0
- goodwe_modbus-0.1.0/pyproject.toml.orig +16 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/__init__.py +15 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/device.py +72 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/energy.py +14 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/enums.py +48 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/faults.py +66 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/inverter.py +66 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/pv.py +25 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/py.typed +0 -0
- goodwe_modbus-0.1.0/src/goodwe_modbus/settings.py +17 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: goodwe-modbus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library to communicate to Goodwe inverter over modbus.
|
|
5
|
+
Author: theneweinstein
|
|
6
|
+
Author-email: theneweinstein <theneweinstein@mail.equust.nl>
|
|
7
|
+
Requires-Dist: modbus-connection>=4.12.3
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "goodwe-modbus"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python library to communicate to Goodwe inverter over modbus."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = ["modbus-connection>=4.12.3"]
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "theneweinstein"
|
|
11
|
+
email = "theneweinstein@mail.equust.nl"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["uv_build>=0.12.23,<0.13.0"]
|
|
15
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "goodwe-modbus"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python library to communicate to Goodwe inverter over modbus."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "theneweinstein", email = "theneweinstein@mail.equust.nl" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"modbus-connection>=4.12.3",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build>=0.12.23,<0.13.0"]
|
|
16
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .device import GoodweInverter
|
|
2
|
+
from .pv import PvStrings
|
|
3
|
+
from .inverter import InverterState, Identity, GridOutput
|
|
4
|
+
from .energy import EnergyTotals
|
|
5
|
+
from .settings import ActivePowerControl
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"GoodweInverter",
|
|
9
|
+
"InverterState",
|
|
10
|
+
"Identity",
|
|
11
|
+
"GridOutput",
|
|
12
|
+
"PvStrings",
|
|
13
|
+
"EnergyTotals",
|
|
14
|
+
"ActivePowerControl"
|
|
15
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""The top-level object for a current-generation Sofar inverter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from modbus_connection.model import (
|
|
9
|
+
Device,
|
|
10
|
+
Raw,
|
|
11
|
+
UpdateReport,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .energy import EnergyTotals
|
|
15
|
+
from .inverter import InverterState, Identity, GridOutput
|
|
16
|
+
from .pv import PvStrings
|
|
17
|
+
from .settings import ActivePowerControl
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from modbus_connection import ModbusUnit
|
|
22
|
+
|
|
23
|
+
# Attribute names of the sub-systems each update method reads.
|
|
24
|
+
READINGS = ("pv","state","grid","energy")
|
|
25
|
+
SETTINGS = ("powercontrol",)
|
|
26
|
+
ALL = ("identity", *READINGS, *SETTINGS)
|
|
27
|
+
|
|
28
|
+
class GoodweInverter(Device):
|
|
29
|
+
"""A current-generation Goodwe inverter reached through a ``ModbusUnit``."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
unit: ModbusUnit,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Set up the sub-systems.
|
|
37
|
+
|
|
38
|
+
``read_pm`` reads parallel-system registers an inverter refuses.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(unit)
|
|
41
|
+
|
|
42
|
+
self.energy = EnergyTotals(unit)
|
|
43
|
+
self.grid = GridOutput(unit)
|
|
44
|
+
self.identity = Identity(unit)
|
|
45
|
+
self.state = InverterState(unit)
|
|
46
|
+
self.pv = PvStrings(unit)
|
|
47
|
+
self.powercontrol = ActivePowerControl(unit)
|
|
48
|
+
|
|
49
|
+
async def _async_setup(self) -> None:
|
|
50
|
+
"""Set up the sub-systems."""
|
|
51
|
+
await self.identity.async_update()
|
|
52
|
+
|
|
53
|
+
async def async_update_readings(self) -> UpdateReport:
|
|
54
|
+
"""Update the readings sub-systems."""
|
|
55
|
+
return await self.async_poll(READINGS)
|
|
56
|
+
|
|
57
|
+
async def async_update_settings(self) -> UpdateReport:
|
|
58
|
+
"""Refresh what the controller has been configured to do."""
|
|
59
|
+
return await self.async_poll(SETTINGS)
|
|
60
|
+
|
|
61
|
+
async def async_update(self) -> UpdateReport:
|
|
62
|
+
"""Refresh all components."""
|
|
63
|
+
report = await self.async_poll(READINGS)
|
|
64
|
+
return await self.async_poll(SETTINGS, report)
|
|
65
|
+
|
|
66
|
+
async def async_read_raw(self, names: Iterable[str] = ALL) -> Raw:
|
|
67
|
+
"""Every register this device reads, undecoded, for diagnostics."""
|
|
68
|
+
return await super().async_read_raw(names)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Daily and lifetime energy counters"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from modbus_connection.model import Component, uint32, gauge
|
|
6
|
+
|
|
7
|
+
class EnergyTotals(Component):
|
|
8
|
+
"""Daily and lifetime energy counters"""
|
|
9
|
+
|
|
10
|
+
daily_energy = gauge(566, 0.1, signed=False, unit="kWh")
|
|
11
|
+
"""Daily energy in kWh."""
|
|
12
|
+
|
|
13
|
+
lifetime_energy = uint32(546, scale = 0.1, unit="kWh")
|
|
14
|
+
"""Lifetime energy in kWh."""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Option maps the plugin declares, as enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum, IntFlag
|
|
6
|
+
|
|
7
|
+
class SystemState(IntEnum):
|
|
8
|
+
"""System state"""
|
|
9
|
+
|
|
10
|
+
WAIT = 0
|
|
11
|
+
NORMAL = 1
|
|
12
|
+
FAULT = 2
|
|
13
|
+
|
|
14
|
+
class Fault1(IntFlag):
|
|
15
|
+
"""Fault flags"""
|
|
16
|
+
|
|
17
|
+
ID001_GFCI_CHECK = 0x00000001
|
|
18
|
+
ID002_AC_HCT_CHECK = 0x00000002
|
|
19
|
+
ID003_REF_V = 0x00000004
|
|
20
|
+
ID004_DC_SWITCH = 0x00000008
|
|
21
|
+
ID005_DC_SPD = 0x00000010
|
|
22
|
+
ID006_SALES1 = 0x00000020
|
|
23
|
+
ID007_GFCI = 0x00000040
|
|
24
|
+
ID008_RELAY = 0x00000080
|
|
25
|
+
ID009_AC_HCT = 0x00000100
|
|
26
|
+
ID010_UTILITY_LOSS = 0x00000200
|
|
27
|
+
ID011_GROUND = 0x00000400
|
|
28
|
+
ID012_DC_BUS = 0x00000800
|
|
29
|
+
ID013_INTERNAL_FAN = 0x00001000
|
|
30
|
+
ID014_TEMPERATURE = 0x00002000
|
|
31
|
+
ID015_SALES2 = 0x00004000
|
|
32
|
+
ID016_PV_OVER_VOLTAGE = 0x00008000
|
|
33
|
+
ID017_EXTERNAL_FAN = 0x00010000
|
|
34
|
+
ID018_VAC = 0x00020000
|
|
35
|
+
ID019_ISOLATION = 0x00040000
|
|
36
|
+
ID020_HIGH_DC_COMPONENT = 0x00080000
|
|
37
|
+
ID021_LCD_COMMUNICATION = 0x00100000
|
|
38
|
+
ID022_STRING_OVERCURRENT = 0x00200000
|
|
39
|
+
ID023_PV_REVERSE = 0x00400000
|
|
40
|
+
ID024_EXPORT_POWER_LIMIT = 0x00800000
|
|
41
|
+
ID025_NPE = 0x01000000
|
|
42
|
+
ID026_RELAY_CHECK = 0x02000000
|
|
43
|
+
ID027_LPE_SHORT_CIRCUIT = 0x04000000
|
|
44
|
+
ID028_NIGHT_SPS = 0x08000000
|
|
45
|
+
ID029_AFCI = 0x10000000
|
|
46
|
+
ID030_GRID_FREQUENCY = 0x20000000
|
|
47
|
+
ID031_EEPROM = 0x40000000
|
|
48
|
+
ID032_SPI = 0x80000000
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Fault definitions, keyed by vendor ID rather than register."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
class FaultCategory(StrEnum):
|
|
9
|
+
"""The subsystem a fault belongs to."""
|
|
10
|
+
|
|
11
|
+
AC_OUTPUT = "ac_output"
|
|
12
|
+
COMMUNICATION = "communication"
|
|
13
|
+
DC_BUS = "dc_bus"
|
|
14
|
+
FAN = "fan"
|
|
15
|
+
GRID = "grid"
|
|
16
|
+
INSULATION = "insulation"
|
|
17
|
+
INTERNAL = "internal"
|
|
18
|
+
PV = "pv"
|
|
19
|
+
SALES = "sales"
|
|
20
|
+
THERMAL = "thermal"
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class Fault:
|
|
24
|
+
"""One fault the inverter can report."""
|
|
25
|
+
|
|
26
|
+
id: int
|
|
27
|
+
key: str
|
|
28
|
+
category: FaultCategory
|
|
29
|
+
|
|
30
|
+
FAULTS: tuple[Fault, ...] = (
|
|
31
|
+
Fault(1, "gfci_check_fail", FaultCategory.INTERNAL),
|
|
32
|
+
Fault(2, "ac_hct_check_fail", FaultCategory.INTERNAL),
|
|
33
|
+
Fault(3, "ref_v", FaultCategory.INTERNAL),
|
|
34
|
+
Fault(4, "dc_switch_fault", FaultCategory.DC_BUS),
|
|
35
|
+
Fault(5, "dc_spd_fault", FaultCategory.DC_BUS),
|
|
36
|
+
Fault(6, "sales1", FaultCategory.SALES),
|
|
37
|
+
Fault(7, "gfci_fault", FaultCategory.INTERNAL),
|
|
38
|
+
Fault(8, "relay_fault", FaultCategory.INTERNAL),
|
|
39
|
+
Fault(9, "ac_hct", FaultCategory.INTERNAL),
|
|
40
|
+
Fault(10, "utility_loss", FaultCategory.GRID),
|
|
41
|
+
Fault(11, "ground_fault", FaultCategory.INSULATION),
|
|
42
|
+
Fault(12, "dc_bus_high", FaultCategory.DC_BUS),
|
|
43
|
+
Fault(13, "internal_fan_fault", FaultCategory.FAN),
|
|
44
|
+
Fault(14, "over_temperature", FaultCategory.THERMAL),
|
|
45
|
+
Fault(15, "sales2", FaultCategory.SALES),
|
|
46
|
+
Fault(16, "pv_overvoltage", FaultCategory.PV),
|
|
47
|
+
Fault(17, "external_fan_fault", FaultCategory.FAN),
|
|
48
|
+
Fault(18, "vac_fault", FaultCategory.GRID),
|
|
49
|
+
Fault(19, "isolation_fault", FaultCategory.INSULATION),
|
|
50
|
+
Fault(20, "high_dc_component", FaultCategory.GRID),
|
|
51
|
+
Fault(21, "lcd_communication_fault", FaultCategory.COMMUNICATION),
|
|
52
|
+
Fault(22, "string_over_current", FaultCategory.PV),
|
|
53
|
+
Fault(23, "pv_reverse_fault", FaultCategory.PV),
|
|
54
|
+
Fault(24, "export_power_limit_fault", FaultCategory.GRID),
|
|
55
|
+
Fault(25, "npe", FaultCategory.GRID),
|
|
56
|
+
Fault(26, "relay_check_fault", FaultCategory.INTERNAL),
|
|
57
|
+
Fault(27, "lpe", FaultCategory.GRID),
|
|
58
|
+
Fault(28, "night_sps_fault", FaultCategory.INTERNAL),
|
|
59
|
+
Fault(29, "afci_fault", FaultCategory.INTERNAL),
|
|
60
|
+
Fault(30, "grid_frequency_overrun", FaultCategory.GRID),
|
|
61
|
+
Fault(31, "eeprom", FaultCategory.COMMUNICATION),
|
|
62
|
+
Fault(32, "spi_fault", FaultCategory.COMMUNICATION)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
FAULTS_BY_ID: dict[int, Fault] = {fault.id: fault for fault in FAULTS}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Inverter state, identity and on-grid output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from modbus_connection.model import Component, enum, flags, gauge, integer, string, uint32
|
|
6
|
+
|
|
7
|
+
from .enums import (
|
|
8
|
+
Fault1,
|
|
9
|
+
SystemState,
|
|
10
|
+
)
|
|
11
|
+
from .faults import FAULTS_BY_ID, Fault
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InverterState(Component):
|
|
15
|
+
"""Inverter state, identity and on-grid output."""
|
|
16
|
+
|
|
17
|
+
system_state = enum(564, SystemState, signed = False)
|
|
18
|
+
fault = flags(544, Fault1, count=2, signed = False)
|
|
19
|
+
generation_time_total = uint32(548, unit = "h")
|
|
20
|
+
inverter_temperature = gauge(565, 0.1, signed = False, unit = "°C")
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def active_faults(self) -> frozenset[Fault]:
|
|
24
|
+
"""Every fault the inverter currently reports."""
|
|
25
|
+
return frozenset(
|
|
26
|
+
fault
|
|
27
|
+
for member in self.fault
|
|
28
|
+
if (fault := FAULTS_BY_ID(member.value)) is not None
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
class Identity(Component):
|
|
32
|
+
"""Inverter identity."""
|
|
33
|
+
|
|
34
|
+
type = string(528, 5)
|
|
35
|
+
"""Inverter type """
|
|
36
|
+
serial = string(512,8)
|
|
37
|
+
"""Inverter serial number."""
|
|
38
|
+
|
|
39
|
+
class GridOutput(Component):
|
|
40
|
+
"""Inverter grid output."""
|
|
41
|
+
|
|
42
|
+
voltage_l1 = gauge(554, 0.1, unit="V")
|
|
43
|
+
voltage_l2 = gauge(555, 0.1, unit="V")
|
|
44
|
+
voltage_l3 = gauge(556, 0.1, unit="V")
|
|
45
|
+
current_l1 = gauge(557, 0.1, unit="A")
|
|
46
|
+
current_l2 = gauge(558, 0.1, unit="A")
|
|
47
|
+
current_l3 = gauge(559, 0.1, unit="A")
|
|
48
|
+
frequency_l1 = gauge(560, 0.01, unit="Hz")
|
|
49
|
+
frequency_l2 = gauge(561, 0.01, unit="Hz")
|
|
50
|
+
frequency_l3 = gauge(562, 0.01, unit="Hz")
|
|
51
|
+
power = integer(563, unit="W")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def power_l1(self) -> float:
|
|
55
|
+
"""Grid output power on L1 in W."""
|
|
56
|
+
return self.voltage_l1 * self.current_l1
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def power_l2(self) -> float:
|
|
60
|
+
"""Grid output power on L2 in W."""
|
|
61
|
+
return self.voltage_l2 * self.current_l2
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def power_l3(self) -> float:
|
|
65
|
+
"""Grid output power on L3 in W."""
|
|
66
|
+
return self.voltage_l3 * self.current_l3
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""PV strings, one component per MPPT tier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from modbus_connection.model import Component, gauge, repeating_group
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PvString(Component):
|
|
10
|
+
"""A PV string, one component per MPPT tier."""
|
|
11
|
+
|
|
12
|
+
pv_voltage = gauge(550,0.1, stride = 1, unit="V")
|
|
13
|
+
"""PV voltage in V."""
|
|
14
|
+
pv_current = gauge(552,0.1, stride = 1, unit="A")
|
|
15
|
+
"""PV current in A."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def pv_power(self) -> float:
|
|
19
|
+
"""PV power in W."""
|
|
20
|
+
return self.pv_voltage * self.pv_current
|
|
21
|
+
|
|
22
|
+
class PvStrings(Component):
|
|
23
|
+
"""A PV string, one component per MPPT tier."""
|
|
24
|
+
|
|
25
|
+
strings = repeating_group(2, PvString, stride = 1)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Settings and commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from modbus_connection.model import Component, integer
|
|
6
|
+
|
|
7
|
+
class ActivePowerControl(Component):
|
|
8
|
+
"""Active power control settings."""
|
|
9
|
+
|
|
10
|
+
active_power_export_limit = integer(256, signed = False, unit = "%")
|
|
11
|
+
"""Active power export limit in W."""
|
|
12
|
+
|
|
13
|
+
async def async_set_active_power_export_limit(self, limit: float) -> None:
|
|
14
|
+
"""Set the active power export limit in W."""
|
|
15
|
+
if not 0 <= limit <= 100:
|
|
16
|
+
raise ValueError(f"active power limit {limit}% is outside 0-100")
|
|
17
|
+
await self._unit.write_register(256, round(limit))
|