Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 42 additions & 55 deletions custom_components/marstek_modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import DOMAIN
from .coordinator import MarstekCoordinator
Expand All @@ -23,107 +24,93 @@
"button",
"number",
"binary_sensor",
]
]


async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""
General setup of the integration.

This is called once when Home Assistant starts.
It does not perform any configuration and always returns True.

Args:
hass: Home Assistant instance.
config: Configuration dict.

Returns:
True always.
"""
"""General setup – called once when Home Assistant starts."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""
Set up a config entry.

Initializes the coordinator for this entry and stores it in hass.data.
Forwards setup to platforms (e.g., sensor, select) used by this integration.

Args:
hass: Home Assistant instance.
entry: ConfigEntry to setup.

Returns:
True if setup successful, False otherwise.
Order of operations:
1. Load register YAML for the configured device version.
2. Connect to the Modbus gateway (raises ConfigEntryNotReady on failure).
3. Forward setup to all entity platforms so entities are created and
added to HA (async_added_to_hass is complete for all of them).
4. Query the entity registry for any user-enabled non-default entities
and add them to the coordinator's polling groups dynamically.
5. Run the first coordinator refresh so every entity has a value
immediately (tick 1 polls all groups).
"""
try:
# Migrate legacy device_version tokens in existing config entries to
# the canonical SUPPORTED_VERSIONS strings. This handles older
# installations that used tokens like 'v1/v2' or 'v3'.
# Warn about unsupported device_version strings in existing entries
raw_version = (entry.data.get("device_version") or "").strip()
if raw_version:
normalized = raw_version.lower()
# Consider anything not listed in SUPPORTED_VERSIONS as legacy/unsupported.
allowed = {s.lower() for s in SUPPORTED_VERSIONS}
if normalized not in allowed:
_LOGGER.warning(
"Config entry %s uses unsupported device_version '%s'. Please remove and re-add the device with the correct device version. Supported versions: %s",
entry.entry_id,
raw_version,
", ".join(SUPPORTED_VERSIONS),
"Config entry %s uses unsupported device_version '%s'. "
"Please remove and re-add the device. Supported: %s",
entry.entry_id, raw_version, ", ".join(SUPPORTED_VERSIONS),
)
# Create the coordinator for data management and attempt an initial
# connection before forwarding platform setup so the client is ready.

coordinator = MarstekCoordinator(hass, entry)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

# Load register definitions off the event loop to avoid blocking
# 1 – Load register definitions (blocking I/O runs in executor)
try:
await coordinator.async_load_registers(entry.data.get("device_version"))
except Exception as err:
_LOGGER.warning("Failed loading register definitions for entry %s: %s", entry.entry_id, err)

# Establish the Modbus connection upfront so the first refresh does not
# lazily reconnect on individual sensor reads, and failure is properly
# tracked from the start.
_LOGGER.warning(
"Failed loading register definitions for entry %s: %s",
entry.entry_id, err,
)

# 2 – Connect to Modbus gateway.
# ConfigEntryNotReady is intentionally NOT caught here – it must
# propagate to HA so the built-in retry mechanism (exponential
# backoff: 5 s → 10 s → 30 s → 60 s → …) kicks in automatically.
# This handles temporary failures such as a disconnected LAN cable
# or the device being in the middle of a reboot.
await coordinator.async_init()

# Forward setup to all platforms defined in PLATFORMS
# 3 – Create all entity platforms (async_added_to_hass runs here)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

# Perform first refresh to ensure coordinator has up-to-date data
# 4 – Register user-enabled non-default entities for polling
await coordinator.async_register_enabled_entities()

# 5 – First refresh: tick 1 polls ALL groups so every entity has
# a value immediately without waiting for slow intervals.
# ConfigEntryNotReady from here also propagates so HA retries.
await coordinator.async_config_entry_first_refresh()

return True
except ConfigEntryNotReady:
# Re-raise so HA schedules an automatic retry.
# The platforms forwarded in step 3 are cleaned up by HA automatically.
raise
except Exception as err:
_LOGGER.error("Error setting up entry %s: %s", entry.entry_id, err)
return False


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""
Unload a config entry and its associated platforms.

Args:
hass: Home Assistant instance.
entry: ConfigEntry to unload.

Returns:
True if unload successful, False otherwise.
"""
"""Unload a config entry and its associated platforms."""
try:
# Unload all platforms for the entry
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
# Retrieve the coordinator and close it before removing
coordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.async_close()
# Remove coordinator reference from hass data
hass.data[DOMAIN].pop(entry.entry_id, None)

return unload_ok
except Exception as err:
_LOGGER.error("Error unloading entry %s: %s", entry.entry_id, err)
return False
return False
Loading