Skip to content

Commit 7f846d0

Browse files
authored
Merge pull request #2111 from rosenrot00/patch-29
Respect Home Assistant entity lifecycle for sensor hub references
2 parents d62f8f5 + 7038ba8 commit 7f846d0

3 files changed

Lines changed: 47 additions & 30 deletions

File tree

custom_components/solax_modbus/__init__.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ def __init__(
554554
self.computedSensors: dict[Any, Any] = {}
555555
self.computedEntities: dict[Any, Any] = {} # buttons and selects with value_function for autorepeat
556556
self.computedSwitches: dict[Any, Any] = {}
557+
self.sensorDescriptions: dict[Any, Any] = {} # all sensor descriptions, indexed by key
557558
self.sensorEntities: dict[Any, Any] = {} # all sensor entities, indexed by key
558559
self.numberEntities: dict[Any, Any] = {} # all number entities, indexed by key
559560
self.selectEntities: dict[Any, Any] = {}
@@ -1729,11 +1730,14 @@ async def async_read_modbus_registers_all(self, group: Any) -> bool:
17291730
self.plugin.localDataCallback(self)
17301731
if not self.localsLoaded:
17311732
await self._hass.async_add_executor_job(self.loadLocalData)
1732-
for key, descr in self.computedSensors.items():
1733-
# Do NOT call modbus_data_updated() from here Race Condition:it calls hub.rebuild_blocks() before async_add_entities is called.
1734-
data[key] = descr.value_function(0, descr, data)
1735-
sens = self.sensorEntities[key]
1736-
_LOGGER.debug(f"{self._name}: quickly updating state for computed sensor {sens} {key} {data[descr.key]} ")
1733+
for key, descr in list(self.computedSensors.items()):
1734+
try:
1735+
data[key] = descr.value_function(0, descr, data)
1736+
except Exception as ex:
1737+
_LOGGER.debug(f"{self._name}: cannot compute value for {key}: {ex}")
1738+
continue
1739+
sens = self.sensorEntities.get(key)
1740+
_LOGGER.debug(f"{self._name}: quickly updating state for computed sensor {sens} {key} {data.get(descr.key)} ")
17371741
if sens and (not descr.internal):
17381742
try:
17391743
sens.modbus_data_updated() # publish state to GUI and automations faster - assuming enabled, otherwise exception
@@ -1823,6 +1827,8 @@ def _is_dependency_for_enabled_control(self, sensor_key: str) -> bool:
18231827
control_entity = self.sensorEntities.get(control_key)
18241828
if control_entity:
18251829
control_descr = control_entity.entity_description
1830+
if not control_descr:
1831+
control_descr = self.sensorDescriptions.get(control_key)
18261832
if control_descr and should_register_be_loaded(self._hass, self, control_descr):
18271833
_LOGGER.debug(f"Sensor '{sensor_key}' is required by enabled control or value_function entity '{control_key}'.")
18281834
return True

custom_components/solax_modbus/energy_dashboard.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,11 @@ def value_function(initval: Any, descr: Any, datadict: dict[str, Any]) -> Any:
501501
source_sensor_desc = None
502502
source_key = sensor_mapping.get_source_key(getattr(data_hub, "data", None) or getattr(data_hub, "datadict", {}))
503503

504-
# Look for source sensor in hub's sensor entities
505-
if hasattr(data_hub, "sensorEntities") and source_key in data_hub.sensorEntities:
504+
# Prefer source descriptors; entity objects are only available for enabled entities.
505+
sensor_descriptions = getattr(data_hub, "sensorDescriptions", {}) or {}
506+
if source_key in sensor_descriptions:
507+
source_sensor_desc = sensor_descriptions[source_key]
508+
elif hasattr(data_hub, "sensorEntities") and source_key in data_hub.sensorEntities:
506509
source_sensor = data_hub.sensorEntities[source_key]
507510
if hasattr(source_sensor, "entity_description"):
508511
source_sensor_desc = source_sensor.entity_description
@@ -836,11 +839,12 @@ def _store_energy_dashboard_last_total_inverter_count(count: int | None) -> None
836839

837840
def _detect_variants(hub_obj: Any, mapping: Any, base_key: str) -> list[int]:
838841
sensor_keys = getattr(hub_obj, "sensorEntities", {}) or {}
842+
sensor_descriptions = getattr(hub_obj, "sensorDescriptions", {}) or {}
839843
hub_data = getattr(hub_obj, "data", None) or getattr(hub_obj, "datadict", {})
840844
variants = []
841845
for n in range(1, mapping.max_variants + 1):
842846
variant_key = f"{base_key}{n}"
843-
if variant_key in sensor_keys or variant_key in hub_data:
847+
if variant_key in sensor_keys or variant_key in sensor_descriptions or variant_key in hub_data:
844848
variants.append(n)
845849
return variants
846850

custom_components/solax_modbus/sensor.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,9 @@ async def readFollowUpBattery(
275275
readFollowUpBattery,
276276
)
277277

278+
hub.computedSensors = computedRegs
278279
async_add_entities(entities)
279280
# now the groups are available
280-
hub.computedSensors = computedRegs
281281
hub.rebuild_blocks(initial_groups) # , computedRegs) # first time call
282282
_LOGGER.info(f"{hub.name}: computedRegs: {hub.computedSensors}")
283283

@@ -416,17 +416,6 @@ async def readFollowUpBattery(
416416
readFollowUp,
417417
)
418418

419-
# Ensure existing Energy Dashboard entities are enabled before adding them.
420-
entity_registry = er.async_get(hass)
421-
for sensor_description in energy_dashboard_sensors:
422-
unique_id = f"{energy_dashboard_platform_name}_{sensor_description.key}"
423-
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
424-
if entity_id:
425-
maybe_entry = entity_registry.async_get(entity_id)
426-
if maybe_entry is not None and maybe_entry.disabled_by:
427-
_LOGGER.debug(f"{hub_name}: Enabling previously disabled Energy Dashboard entity: {entity_id}")
428-
entity_registry.async_update_entity(entity_id, disabled_by=None)
429-
430419
# Add Energy Dashboard entities to main entities list and register them
431420
if energy_dashboard_entities:
432421
_LOGGER.info(f"{hub_name}: Registering {len(energy_dashboard_entities)} Energy Dashboard entities")
@@ -465,6 +454,7 @@ async def async_refresh_energy_dashboard_entities() -> None:
465454
for newdescr in energy_dashboard_sensors:
466455
existing_sensor = hub.sensorEntities.get(newdescr.key)
467456
if existing_sensor and getattr(existing_sensor, "hass", None) is not None:
457+
hub.sensorDescriptions[newdescr.key] = newdescr
468458
existing_sensor.entity_description = newdescr
469459
if hasattr(existing_sensor, "_riemann_mapping") and getattr(newdescr, "_riemann_mapping", None):
470460
existing_sensor._riemann_mapping = newdescr._riemann_mapping
@@ -476,6 +466,7 @@ async def async_refresh_energy_dashboard_entities() -> None:
476466
continue
477467
if existing_sensor:
478468
hub.sensorEntities.pop(newdescr.key, None)
469+
hub.sensorDescriptions.pop(newdescr.key, None)
479470
hub.computedSensors.pop(newdescr.key, None)
480471

481472
entityToListSingle(
@@ -511,7 +502,8 @@ async def async_refresh_energy_dashboard_entities() -> None:
511502

512503
if allow_remove_pv or allow_remove_home or allow_remove_grid:
513504
entity_registry = er.async_get(hass)
514-
for key in list(hub.sensorEntities.keys()):
505+
existing_keys = set(hub.sensorEntities.keys()) | set(hub.sensorDescriptions.keys())
506+
for key in list(existing_keys):
515507
if key in desired_keys:
516508
continue
517509
is_pv_variant = "_pv_power_" in key or "_pv_energy_" in key
@@ -523,17 +515,20 @@ async def async_refresh_energy_dashboard_entities() -> None:
523515
if entity_id:
524516
entity_registry.async_remove(entity_id)
525517
hub.sensorEntities.pop(key, None)
518+
hub.sensorDescriptions.pop(key, None)
526519
hub.computedSensors.pop(key, None)
527520

528521
# Recompute ED values immediately to relink unavailable entities.
529522
for newdescr in energy_dashboard_sensors:
530523
if newdescr.register < 0 and newdescr.value_function:
524+
sens = hub.sensorEntities.get(newdescr.key)
525+
if sens is None and not getattr(newdescr, "internal", False):
526+
continue
531527
try:
532528
hub.data[newdescr.key] = newdescr.value_function(0, newdescr, hub.data)
533529
except Exception as e:
534530
_LOGGER.debug(f"{hub_name}: ED refresh value_function failed for {newdescr.key}: {e}")
535531
continue
536-
sens = hub.sensorEntities.get(newdescr.key)
537532
if sens and not getattr(newdescr, "internal", False):
538533
sens.modbus_data_updated()
539534

@@ -567,8 +562,18 @@ def __init__(
567562
self.entity_description: BaseModbusSensorEntityDescription = description
568563
self._attr_extra_state_attributes = _energy_dashboard_mapping_attrs(self.entity_description, self._hub)
569564

565+
def _register_hub_sensor_entity(self) -> None:
566+
# Only called from async_added_to_hass so disabled entities never enter sensorEntities.
567+
self._hub.sensorEntities[self.entity_description.key] = self
568+
self._hub.sensorDescriptions[self.entity_description.key] = self.entity_description
569+
570+
def _unregister_hub_sensor_entity(self) -> None:
571+
if self._hub.sensorEntities.get(self.entity_description.key) is self:
572+
self._hub.sensorEntities.pop(self.entity_description.key, None)
573+
570574
async def async_added_to_hass(self) -> None:
571575
"""Register callbacks."""
576+
self._register_hub_sensor_entity()
572577
# Skip hub registration for computed/internal sensors (those without modbus registers)
573578
# These sensors don't participate in the polling cycle
574579
if self.entity_description.register < 0:
@@ -587,7 +592,11 @@ async def async_added_to_hass(self) -> None:
587592
await self._hub.async_add_solax_modbus_sensor(self)
588593

589594
async def async_will_remove_from_hass(self) -> None:
590-
await self._hub.async_remove_solax_modbus_sensor(self)
595+
if self.entity_description.register >= 0 or getattr(self.entity_description, "_is_riemann_sum_sensor", False):
596+
await self._hub.async_remove_solax_modbus_sensor(self)
597+
if self.entity_description.register < 0 and not getattr(self.entity_description, "_is_riemann_sum_sensor", False):
598+
self._hub.computedSensors.pop(self.entity_description.key, None)
599+
self._unregister_hub_sensor_entity()
591600

592601
@callback
593602
def modbus_data_updated(self) -> None:
@@ -707,6 +716,7 @@ async def async_added_to_hass(self) -> None:
707716
self.async_write_ha_state()
708717

709718
# Register with hub
719+
self._register_hub_sensor_entity()
710720
await self._hub.async_add_solax_modbus_sensor(self)
711721

712722
@callback
@@ -882,7 +892,7 @@ def entityToListSingle(
882892
newdescr,
883893
)
884894

885-
hub.sensorEntities[newdescr.key] = sensor
895+
hub.sensorDescriptions[newdescr.key] = newdescr
886896
# register dependency chain
887897
deplist = newdescr.depends_on
888898
if deplist is not None:
@@ -898,13 +908,10 @@ def entityToListSingle(
898908
if newdescr.sleepmode == SLEEPMODE_ZERO:
899909
hub.sleepzero.append(newdescr.key)
900910
if newdescr.register < 0: # entity without modbus address
901-
enabled = is_entity_enabled(hub._hass, hub, newdescr, use_default=True, platform_name=hub_name) # dont compute disabled entities anymore
902-
# if not enabled: _LOGGER.info(f"is_entity_enabled called for disabled entity {newdescr.key}")
903-
if newdescr.value_function and (enabled or newdescr.internal): # *** dont compute disabled entities anymore unless internal
911+
if newdescr.value_function and newdescr.internal:
904912
computedRegs[newdescr.key] = newdescr
905-
else:
906-
if enabled:
907-
_LOGGER.warning(f"{hub_name}: entity without modbus register address and without value_function found: {newdescr.key}")
913+
elif not newdescr.value_function and is_entity_enabled(hub._hass, hub, newdescr, use_default=True, platform_name=hub_name):
914+
_LOGGER.warning(f"{hub_name}: entity without modbus register address and without value_function found: {newdescr.key}")
908915
else:
909916
# target group
910917
interval_group = groups.setdefault(hub.scan_group(sensor), empty_input_interval_group_lambda())

0 commit comments

Comments
 (0)