Skip to content

Commit 4905bf5

Browse files
committed
release prep
1 parent 02adad5 commit 4905bf5

10 files changed

Lines changed: 264 additions & 108 deletions

File tree

custom_components/growatt_modbus/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from homeassistant.const import Platform
77
from homeassistant.core import HomeAssistant
88
from homeassistant.helpers.typing import ConfigType
9+
from homeassistant.helpers import config_validation as cv
10+
911

1012
from .const import DOMAIN
1113
from .coordinator import GrowattModbusCoordinator
@@ -14,7 +16,7 @@
1416

1517
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
1618

17-
PLATFORMS: list[Platform] = [Platform.SENSOR]
19+
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.BINARY_SENSOR]
1820

1921

2022
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Binary sensor platform for Growatt Modbus Integration."""
2+
import logging
3+
from datetime import datetime, timedelta
4+
from typing import Any
5+
6+
from homeassistant.components.binary_sensor import (
7+
BinarySensorDeviceClass,
8+
BinarySensorEntity,
9+
)
10+
from homeassistant.config_entries import ConfigEntry
11+
from homeassistant.core import HomeAssistant
12+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
13+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
14+
15+
from .const import DOMAIN
16+
from .coordinator import GrowattModbusCoordinator
17+
18+
_LOGGER = logging.getLogger(__name__)
19+
20+
21+
async def async_setup_entry(
22+
hass: HomeAssistant,
23+
config_entry: ConfigEntry,
24+
async_add_entities: AddEntitiesCallback,
25+
) -> None:
26+
"""Set up Growatt Modbus binary sensors."""
27+
coordinator = hass.data[DOMAIN][config_entry.entry_id]
28+
29+
entities = [
30+
GrowattInverterOnlineSensor(coordinator, config_entry),
31+
]
32+
33+
async_add_entities(entities)
34+
35+
36+
class GrowattInverterOnlineSensor(CoordinatorEntity, BinarySensorEntity):
37+
"""Binary sensor for inverter online status."""
38+
39+
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
40+
41+
def __init__(
42+
self,
43+
coordinator: GrowattModbusCoordinator,
44+
config_entry: ConfigEntry,
45+
) -> None:
46+
"""Initialize the binary sensor."""
47+
super().__init__(coordinator)
48+
49+
self._config_entry = config_entry
50+
self._attr_name = f"{config_entry.data['name']} Inverter Online"
51+
self._attr_unique_id = f"{config_entry.entry_id}_inverter_online"
52+
self._attr_icon = "mdi:solar-power-variant"
53+
54+
@property
55+
def device_info(self) -> dict[str, Any]:
56+
"""Return device information."""
57+
return self.coordinator.device_info
58+
59+
@property
60+
def is_on(self) -> bool:
61+
"""Return true if inverter is online (responding to Modbus)."""
62+
if self.coordinator.last_successful_update is None:
63+
return False
64+
65+
# Consider online if last update was within 5 minutes
66+
time_since_update = datetime.now() - self.coordinator.last_successful_update
67+
return time_since_update < timedelta(minutes=5)
68+
69+
@property
70+
def extra_state_attributes(self) -> dict[str, Any] | None:
71+
"""Return additional state attributes."""
72+
if self.coordinator.last_successful_update is None:
73+
return None
74+
75+
time_since_update = datetime.now() - self.coordinator.last_successful_update
76+
77+
return {
78+
"last_successful_update": self.coordinator.last_successful_update.isoformat(),
79+
"seconds_since_update": int(time_since_update.total_seconds()),
80+
}

custom_components/growatt_modbus/config_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@
3434
vol.Required(CONF_HOST): str,
3535
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
3636
vol.Required(CONF_SLAVE_ID, default=DEFAULT_SLAVE_ID): int,
37-
vol.Required(CONF_REGISTER_MAP, default="MIN_10000_VARIANT_A"): vol.In(list(REGISTER_MAPS.keys())),
37+
vol.Required(CONF_REGISTER_MAP, default="MIN_10000_TL_X_OFFICIAL"): vol.In(list(REGISTER_MAPS.keys())), # ← Changed here
3838
})
3939

4040
STEP_SERIAL_DATA_SCHEMA = vol.Schema({
4141
vol.Required(CONF_DEVICE_PATH, default="/dev/ttyUSB0"): str,
4242
vol.Required(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): int,
4343
vol.Required(CONF_SLAVE_ID, default=DEFAULT_SLAVE_ID): int,
44-
vol.Required(CONF_REGISTER_MAP, default="MIN_10000_VARIANT_A"): vol.In(list(REGISTER_MAPS.keys())),
44+
vol.Required(CONF_REGISTER_MAP, default="MIN_10000_TL_X_OFFICIAL"): vol.In(list(REGISTER_MAPS.keys())), # ← Changed here
4545
})
4646

4747

custom_components/growatt_modbus/const.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
MIN_10000_TL_X_OFFICIAL = {
3434
'name': 'MIN-10000-TL-X Official (Protocol V1.39)',
3535
'description': 'Official register mapping from Growatt Protocol V1.39 documentation',
36-
'notes': 'Power registers are 32-bit pairs (HIGH/LOW). Grid frequency uses raw register 37.',
36+
'notes': 'Power registers are 32-bit pairs (HIGH/LOW). Supports PV1-PV3 strings.',
3737
'input_registers': {
3838
# System Status
3939
3000: {'name': 'inverter_status', 'scale': 1, 'unit': '', 'desc': '0=Waiting, 1=Normal, 3=Fault'},
@@ -54,6 +54,12 @@
5454
3009: {'name': 'pv2_power_high', 'scale': 1, 'unit': '', 'desc': 'PV2 power HIGH word', 'pair': 3010},
5555
3010: {'name': 'pv2_power_low', 'scale': 1, 'unit': '', 'desc': 'PV2 power LOW word', 'pair': 3009, 'combined_scale': 0.1, 'combined_unit': 'W'},
5656

57+
# PV String 3 (if available on model)
58+
3011: {'name': 'pv3_voltage', 'scale': 0.1, 'unit': 'V', 'desc': 'PV3 DC voltage'},
59+
3012: {'name': 'pv3_current', 'scale': 0.1, 'unit': 'A', 'desc': 'PV3 DC current'},
60+
3013: {'name': 'pv3_power_high', 'scale': 1, 'unit': '', 'desc': 'PV3 power HIGH word', 'pair': 3014},
61+
3014: {'name': 'pv3_power_low', 'scale': 1, 'unit': '', 'desc': 'PV3 power LOW word', 'pair': 3013, 'combined_scale': 0.1, 'combined_unit': 'W'},
62+
5763
# System Output Power (32-bit)
5864
3019: {'name': 'system_output_power_high', 'scale': 1, 'unit': '', 'desc': 'System output power HIGH', 'pair': 3020},
5965
3020: {'name': 'system_output_power_low', 'scale': 1, 'unit': '', 'desc': 'System output power LOW', 'pair': 3019, 'combined_scale': 0.1, 'combined_unit': 'W'},
@@ -159,6 +165,10 @@
159165
8: {'name': 'pv2_current', 'scale': 0.1, 'unit': 'A'},
160166
9: {'name': 'pv2_power_high', 'scale': 1, 'unit': '', 'pair': 10},
161167
10: {'name': 'pv2_power_low', 'scale': 1, 'unit': '', 'pair': 9, 'combined_scale': 0.1, 'combined_unit': 'W'},
168+
11: {'name': 'pv3_voltage', 'scale': 0.1, 'unit': 'V'},
169+
12: {'name': 'pv3_current', 'scale': 0.1, 'unit': 'A'},
170+
13: {'name': 'pv3_power_high', 'scale': 1, 'unit': '', 'pair': 14},
171+
14: {'name': 'pv3_power_low', 'scale': 1, 'unit': '', 'pair': 13, 'combined_scale': 0.1, 'combined_unit': 'W'},
162172
35: {'name': 'output_power_high', 'scale': 1, 'unit': '', 'pair': 36},
163173
36: {'name': 'output_power_low', 'scale': 1, 'unit': '', 'pair': 35, 'combined_scale': 0.1, 'combined_unit': 'W'},
164174
37: {'name': 'grid_frequency', 'scale': 0.01, 'unit': 'Hz'},

custom_components/growatt_modbus/growatt_modbus.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class GrowattData:
6262
pv2_voltage: float = 0.0 # V
6363
pv2_current: float = 0.0 # A
6464
pv2_power: float = 0.0 # W
65+
pv3_voltage: float = 0.0 # V
66+
pv3_current: float = 0.0 # A
67+
pv3_power: float = 0.0 # W
6568
pv_total_power: float = 0.0 # W
6669

6770
# AC Output
@@ -401,45 +404,46 @@ def read_all_data(self) -> Optional[GrowattData]:
401404
# PV String 1
402405
pv1_voltage_addr = self._find_register_by_name('pv1_voltage')
403406
pv1_current_addr = self._find_register_by_name('pv1_current')
404-
pv1_power_addr = self._find_register_by_name('pv1_power')
407+
pv1_power_low_addr = self._find_register_by_name('pv1_power_low')
405408

406409
if pv1_voltage_addr:
407410
data.pv1_voltage = self._get_register_value(pv1_voltage_addr) or 0.0
408411
if pv1_current_addr:
409412
data.pv1_current = self._get_register_value(pv1_current_addr) or 0.0
410-
if pv1_power_addr:
411-
# For 32-bit power, read the LOW word (which has combined_scale)
412-
pv1_power_low_addr = self._find_register_by_name('pv1_power_low')
413-
if pv1_power_low_addr:
414-
data.pv1_power = self._get_register_value(pv1_power_low_addr) or 0.0
415-
else:
416-
# Fallback for single register power
417-
data.pv1_power = self._get_register_value(pv1_power_addr) or 0.0
413+
if pv1_power_low_addr:
414+
data.pv1_power = self._get_register_value(pv1_power_low_addr) or 0.0
418415

419416
# PV String 2
420417
pv2_voltage_addr = self._find_register_by_name('pv2_voltage')
421418
pv2_current_addr = self._find_register_by_name('pv2_current')
422-
pv2_power_addr = self._find_register_by_name('pv2_power')
419+
pv2_power_low_addr = self._find_register_by_name('pv2_power_low')
423420

424421
if pv2_voltage_addr:
425422
data.pv2_voltage = self._get_register_value(pv2_voltage_addr) or 0.0
426423
if pv2_current_addr:
427424
data.pv2_current = self._get_register_value(pv2_current_addr) or 0.0
428-
if pv2_power_addr:
429-
# For 32-bit power, read the LOW word
430-
pv2_power_low_addr = self._find_register_by_name('pv2_power_low')
431-
if pv2_power_low_addr:
432-
data.pv2_power = self._get_register_value(pv2_power_low_addr) or 0.0
433-
else:
434-
data.pv2_power = self._get_register_value(pv2_power_addr) or 0.0
425+
if pv2_power_low_addr:
426+
data.pv2_power = self._get_register_value(pv2_power_low_addr) or 0.0
427+
428+
# PV String 3 (if available)
429+
pv3_voltage_addr = self._find_register_by_name('pv3_voltage')
430+
pv3_current_addr = self._find_register_by_name('pv3_current')
431+
pv3_power_low_addr = self._find_register_by_name('pv3_power_low')
432+
433+
if pv3_voltage_addr:
434+
data.pv3_voltage = self._get_register_value(pv3_voltage_addr) or 0.0
435+
if pv3_current_addr:
436+
data.pv3_current = self._get_register_value(pv3_current_addr) or 0.0
437+
if pv3_power_low_addr:
438+
data.pv3_power = self._get_register_value(pv3_power_low_addr) or 0.0
435439

436440
# Total PV Power
437441
pv_total_addr = self._find_register_by_name('pv_total_power_low')
438442
if pv_total_addr:
439443
data.pv_total_power = self._get_register_value(pv_total_addr) or 0.0
440444
else:
441445
# Calculate from strings if not available
442-
data.pv_total_power = data.pv1_power + data.pv2_power
446+
data.pv_total_power = data.pv1_power + data.pv2_power + data.pv3_power
443447

444448
# AC Output
445449
ac_voltage_addr = self._find_register_by_name('ac_voltage')
@@ -505,7 +509,7 @@ def read_all_data(self) -> Optional[GrowattData]:
505509
if warning_addr:
506510
data.warning_code = int(self._get_register_value(warning_addr) or 0)
507511

508-
logger.debug(f"Read data: PV={data.pv_total_power}W, AC={data.ac_power}W, Temp={data.inverter_temp}°C")
512+
logger.debug(f"Read data: PV={data.pv_total_power}W (PV1={data.pv1_power}W, PV2={data.pv2_power}W, PV3={data.pv3_power}W), AC={data.ac_power}W, Temp={data.inverter_temp}°C")
509513

510514
except Exception as e:
511515
logger.error(f"Error parsing register data: {e}", exc_info=True)

0 commit comments

Comments
 (0)