Skip to content

Commit 9876574

Browse files
committed
Hybrid Gen4: dry-contact section as a sub-device with dependency-driven availability
The Gen4 dry-contact load-management controls become their own HA device ("<hub> Dry Contact", linked to the inverter via via_device, same pattern as the battery pack devices), and each entity is only available while its full parent chain in the app's dependency tree is active: Dry Contact Mode (0xC3 -> 0x12F): Generator Control | Load Management Load Management (0xC2 -> 0x12E): Disabled | Manual Mode | Smart Save Manual Mode switch (0xB6 -> 0x122): only in Manual Mode Threshold On Feed-in Power (0xB7 -> 0x123) Threshold Off Consumption (0xB9 -> 0x125) | Threshold Off Battery SOC (0xBA -> 0x126) | only in Smart Save Minimum Per On Signal (0xBB -> 0x127) | Maximum Per Day (0xBC -> 0x128) | Schedule (0xBD -> 0x129) / Work Start/End Time 1 and 2 (0xBE-0xC1 -> 0x12A-0x12D): only while Schedule is enabled All register mappings were verified live on an X3-Hybrid-G4 15kW by changing each setting in the SolaX app and tracking the readback. New generic infrastructure, reusable by any plugin: entity descriptions gain device_group (assigns the entity to a named sub-device) and active_when ({sensor_key: allowed values} evaluated against polled hub.data; keys not polled on a model never block availability). The number, select, switch and time platforms honour both. The "Read Dry Contact Box" setup question is removed: the dry-contact entities are managed dynamically by the availability tree, so the DCB feature flag is no longer needed for them and no longer gates anything in the SolaX plugin (other brand plugins never had DCB-gated entities; their unused boilerplate is untouched). Display names follow the app: Work Mode -> Load Management (option Manual -> Manual Mode), Manual Mode Control -> Manual Mode, Feedin On Power -> Threshold On Feed-in Power, Consume Off Power -> Threshold Off Consumption, Switch Off SOC -> Threshold Off Battery SOC, Maximum Per Day On -> Maximum Per Day. Keys and entity ids are unchanged. Generator management entities are intentionally untouched and stay on the main inverter device.
1 parent 27875b3 commit 9876574

17 files changed

Lines changed: 265 additions & 165 deletions

File tree

custom_components/solax_modbus/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,15 @@ def _warn_duplicate_inverter_configuration(self, interval: int) -> None:
918918
describe_modbus_connection(identity),
919919
)
920920

921+
def group_device_info(self, group: str) -> DeviceInfo:
922+
"""Return the DeviceInfo for a named sub-device (e.g. "dry_contact")."""
923+
return DeviceInfo(
924+
identifiers=cast(set[tuple[str, str]], {(DOMAIN, self._name, group)}),
925+
name=f"{self._name} {group.replace('_', ' ').title()}",
926+
manufacturer=self.plugin.plugin_manufacturer,
927+
via_device=cast(tuple[str, str], (DOMAIN, self._name, INVERTER_IDENT)),
928+
)
929+
921930
def device_group_key(self, device_info: DeviceInfo) -> str:
922931
"""Extract device group key from device_info identifiers.
923932

custom_components/solax_modbus/config_flow.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
CONF_MODBUS_ADDR,
4545
CONF_PLUGIN,
4646
CONF_READ_BATTERY,
47-
CONF_READ_DCB,
4847
CONF_READ_EPS,
4948
CONF_READ_PM,
5049
CONF_SCAN_INTERVAL_FAST,
@@ -62,7 +61,6 @@
6261
DEFAULT_PLUGIN,
6362
DEFAULT_PORT,
6463
DEFAULT_READ_BATTERY,
65-
DEFAULT_READ_DCB,
6664
DEFAULT_READ_EPS,
6765
DEFAULT_READ_PM,
6866
DEFAULT_SCAN_INTERVAL,
@@ -154,7 +152,6 @@ def _configured_hub_names(handler: SchemaCommonFlowHandler) -> set[str]:
154152
vol.Optional(CONF_INVERTER_POWER_KW, default=DEFAULT_INVERTER_POWER_KW): cv.positive_int,
155153
vol.Optional(CONF_ENERGY_DASHBOARD_DEVICE, default=DEFAULT_ENERGY_DASHBOARD_DEVICE): bool,
156154
vol.Optional(CONF_READ_EPS, default=DEFAULT_READ_EPS): bool,
157-
vol.Optional(CONF_READ_DCB, default=DEFAULT_READ_DCB): bool,
158155
vol.Optional(CONF_READ_PM, default=DEFAULT_READ_PM): bool,
159156
vol.Optional(CONF_TIME_OUT, default=DEFAULT_TIME_OUT): int,
160157
}
@@ -176,7 +173,6 @@ def _configured_hub_names(handler: SchemaCommonFlowHandler) -> set[str]:
176173
vol.Optional(CONF_INVERTER_POWER_KW, default=DEFAULT_INVERTER_POWER_KW): cv.positive_int,
177174
vol.Optional(CONF_ENERGY_DASHBOARD_DEVICE, default=DEFAULT_ENERGY_DASHBOARD_DEVICE): bool,
178175
vol.Optional(CONF_READ_EPS, default=DEFAULT_READ_EPS): bool,
179-
vol.Optional(CONF_READ_DCB, default=DEFAULT_READ_DCB): bool,
180176
vol.Optional(CONF_READ_PM, default=DEFAULT_READ_PM): bool,
181177
vol.Optional(CONF_TIME_OUT, default=DEFAULT_TIME_OUT): int,
182178
}

custom_components/solax_modbus/const.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ class BaseModbusSensorEntityDescription(SensorEntityDescription):
238238
"""Base class for modbus sensor declarations."""
239239

240240
allowedtypes: int = 0 # overload with ALLDEFAULT from plugin
241+
device_group: str | None = None # assign the entity to a named sub-device instead of the main inverter device
242+
active_when: dict[str, tuple[Any, ...]] | None = None # {sensor_key: allowed values}; entity is unavailable unless every polled key matches
241243
order32: str | None = None # per-sensor 32-bit word order override ("big"/"little"); None = plugin default
242244
modbus_min: int | None = None # Minimum protocol version as reported by register 0x82 (e.g. 100 for V001.00); not the document revision.
243245
modbus_max: int | None = None # Maximum protocol version as reported by register 0x82.
@@ -305,6 +307,8 @@ class BaseModbusSelectEntityDescription(SelectEntityDescription):
305307
"""Base class for modbus select declarations."""
306308

307309
allowedtypes: int = 0 # overload with ALLDEFAULT from plugin
310+
device_group: str | None = None # assign the entity to a named sub-device instead of the main inverter device
311+
active_when: dict[str, tuple[Any, ...]] | None = None # {sensor_key: allowed values}; entity is unavailable unless every polled key matches
308312
modbus_min: int | None = None # Minimum protocol version as reported by register 0x82 (e.g. 100 for V001.00); not the document revision.
309313
modbus_max: int | None = None # Maximum protocol version as reported by register 0x82.
310314
register: int | None = None
@@ -325,6 +329,8 @@ class BaseModbusSwitchEntityDescription(SwitchEntityDescription):
325329
"""Base class for modbus switch declarations."""
326330

327331
allowedtypes: int = 0 # overload with ALLDEFAULT from plugin
332+
device_group: str | None = None # assign the entity to a named sub-device instead of the main inverter device
333+
active_when: dict[str, tuple[Any, ...]] | None = None # {sensor_key: allowed values}; entity is unavailable unless every polled key matches
328334
modbus_min: int | None = None # Minimum protocol version as reported by register 0x82 (e.g. 100 for V001.00); not the document revision.
329335
modbus_max: int | None = None # Maximum protocol version as reported by register 0x82.
330336
register: int | None = None
@@ -344,6 +350,8 @@ class BaseModbusTimeEntityDescription(TimeEntityDescription):
344350
"""Base class for modbus time declarations."""
345351

346352
allowedtypes: int = 0 # overload with ALLDEFAULT from plugin
353+
device_group: str | None = None # assign the entity to a named sub-device instead of the main inverter device
354+
active_when: dict[str, tuple[Any, ...]] | None = None # {sensor_key: allowed values}; entity is unavailable unless every polled key matches
347355
modbus_min: int | None = None # Minimum protocol version as reported by register 0x82 (e.g. 100 for V001.00); not the document revision.
348356
modbus_max: int | None = None # Maximum protocol version as reported by register 0x82.
349357
scale: float | dict[Any, Any] | Callable[[Any, Any, dict[str, Any]], Any] = 1
@@ -373,6 +381,8 @@ class BaseModbusNumberEntityDescription(NumberEntityDescription):
373381
"""Base class for modbus number declarations."""
374382

375383
allowedtypes: int = 0 # overload with ALLDEFAULT from plugin
384+
device_group: str | None = None # assign the entity to a named sub-device instead of the main inverter device
385+
active_when: dict[str, tuple[Any, ...]] | None = None # {sensor_key: allowed values}; entity is unavailable unless every polled key matches
376386
modbus_min: int | None = None # Minimum protocol version as reported by register 0x82 (e.g. 100 for V001.00); not the document revision.
377387
modbus_max: int | None = None # Maximum protocol version as reported by register 0x82.
378388
register: int | None = None
@@ -406,6 +416,22 @@ def modbus_protocol_version(hub: Any) -> int:
406416
return 0
407417

408418

419+
def matches_active_when(hub: Any, description: Any) -> bool:
420+
"""Return whether an entity's active_when conditions are currently met.
421+
422+
Conditions reference polled hub.data keys; a key that is absent from
423+
hub.data (not polled on this model) does not block availability.
424+
"""
425+
conditions = getattr(description, "active_when", None)
426+
if not conditions:
427+
return True
428+
data = getattr(hub, "data", {})
429+
for key, allowed in conditions.items():
430+
if key in data and data[key] not in allowed:
431+
return False
432+
return True
433+
434+
409435
def matches_modbus_protocol(hub: Any, description: Any) -> bool:
410436
"""Return whether a description applies to the detected Modbus protocol version.
411437

custom_components/solax_modbus/number.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
WRITE_MULTISINGLE_MODBUS,
2222
WRITE_SINGLE_MODBUS,
2323
BaseModbusNumberEntityDescription,
24+
matches_active_when,
2425
matches_modbus_protocol,
2526
)
2627

@@ -55,7 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
5556
if plugin.matchInverterWithMask(hub._invertertype, newdescr.allowedtypes, hub.seriesnumber, newdescr.blacklist) and matches_modbus_protocol(
5657
hub, newdescr
5758
):
58-
number = SolaXModbusNumber(hub_name, hub, modbus_addr, hub.device_info, newdescr)
59+
device_info = hub.group_device_info(newdescr.device_group) if newdescr.device_group else hub.device_info
60+
number = SolaXModbusNumber(hub_name, hub, modbus_addr, device_info, newdescr)
5961
if newdescr.write_method == WRITE_DATA_LOCAL:
6062
hub.writeLocals[newdescr.key] = newdescr
6163
# Use the explicit sensor_key if provided, otherwise fall back to the number's own key.
@@ -175,6 +177,10 @@ def should_poll(self) -> bool:
175177
"""Data is delivered by the hub."""
176178
return False
177179

180+
@property
181+
def available(self) -> bool:
182+
return matches_active_when(self._hub, self.entity_description)
183+
178184
@property
179185
def name(self) -> str:
180186
"""Return the entity name (description name only — the device name provides context)."""

0 commit comments

Comments
 (0)