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
85 changes: 69 additions & 16 deletions homeassistant/components/fronius/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from homeassistant.components.modbus import async_get_unit
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_send
Expand All @@ -30,23 +30,25 @@
CONF_MODBUS_PORT,
DEFAULT_MODBUS_PORT,
DOMAIN,
SOLAR_NET_DISCOVERY_NEW,
SOLAR_NET_ID_SYSTEM,
SOLAR_NET_RESCAN_TIMER,
FroniusDeviceInfo,
SolarNetId,
discovery_signal,
)
from .coordinator import (
FroniusCoordinatorBase,
FroniusInverterUpdateCoordinator,
FroniusLoggerUpdateCoordinator,
FroniusMeterUpdateCoordinator,
FroniusModbusCoordinatorBase,
FroniusModbusInverterUpdateCoordinator,
FroniusModbusSettingsUpdateCoordinator,
FroniusOhmpilotUpdateCoordinator,
FroniusPowerFlowUpdateCoordinator,
FroniusStorageUpdateCoordinator,
)
from .sensor import MODBUS_INVERTER_ENTITY_DESCRIPTIONS

_LOGGER: Final = logging.getLogger(__name__)
PLATFORMS: Final = [
Expand All @@ -58,9 +60,38 @@

type FroniusConfigEntry = ConfigEntry[FroniusSolarNet]

MODBUS_SENSOR_KEYS: Final = {
description.key for description in MODBUS_INVERTER_ENTITY_DESCRIPTIONS
}


@callback
def _async_fix_modbus_sensor_unique_ids(
hass: HomeAssistant, entry: FroniusConfigEntry
) -> None:
"""Move sensors that were registered with the SolarAPI unique ID format.

A Modbus coordinator found by a re-scan reached the sensor platform
before it could be told from a SolarAPI one, so 2026.9 built its entities
as SolarAPI ones: `<inverter>-<key>` instead of `<inverter>-modbus-<key>`.
The keys of the two are distinct, so a Modbus key without the marker can
only come from that.
"""
registry = er.async_get(hass)
for entity in er.async_entries_for_config_entry(registry, entry.entry_id):
inverter_id, _, key = entity.unique_id.rpartition("-")
if key not in MODBUS_SENSOR_KEYS or inverter_id.endswith("-modbus"):
continue
unique_id = f"{inverter_id}-modbus-{key}"
if registry.async_get_entity_id(entity.domain, DOMAIN, unique_id):
continue
_LOGGER.debug("Migrating unique ID of %s to %s", entity.entity_id, unique_id)
registry.async_update_entity(entity.entity_id, new_unique_id=unique_id)


async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool:
"""Set up fronius from a config entry."""
_async_fix_modbus_sensor_unique_ids(hass, entry)
host = entry.data[CONF_HOST]
fronius = Fronius(
async_get_clientsession(
Expand Down Expand Up @@ -260,7 +291,11 @@ async def _init_devices_inverter(self, _now: datetime | None = None) -> None:
# Only for re-scans. Initial setup adds entities
# through sensor.async_setup_entry
if self.config_entry.state is ConfigEntryState.LOADED:
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
async_dispatcher_send(
self.hass,
discovery_signal(self.config_entry.entry_id),
_coordinator,
)

_LOGGER.debug(
"New inverter added (UID: %s)",
Expand Down Expand Up @@ -336,10 +371,17 @@ def _modbus_params(self) -> ModbusTcpParams | None:

async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
"""Set up a Modbus coordinator for an inverter exposing SunSpec MPPT data."""
if inverter_info.solar_net_id in [
# each coordinator is retried on its own: a device may answer for one
# of them and not the other, and recover on a later re-scan
needs_readings = inverter_info.solar_net_id not in {
coordinator.inverter_info.solar_net_id
for coordinator in self.modbus_inverter_coordinators
]:
}
needs_settings = inverter_info.solar_net_id not in {
coordinator.inverter_info.solar_net_id
for coordinator in self.modbus_settings_coordinators
}
if not needs_readings and not needs_settings:
return
if (unit_id := self._modbus_unit_id(inverter_info.solar_net_id)) is None:
return
Expand Down Expand Up @@ -367,7 +409,7 @@ async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
err,
)
return
if modbus_inverter.mppt is not None:
if needs_readings and modbus_inverter.mppt is not None:
readings = FroniusModbusInverterUpdateCoordinator(
hass=self.hass,
solar_net=self,
Expand All @@ -377,16 +419,19 @@ async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
modbus_inverter=modbus_inverter,
config_entry=self.config_entry,
)
if await self._start_modbus_coordinator(readings):
self.modbus_inverter_coordinators.append(readings)
else:
await self._start_modbus_coordinator(
readings, self.modbus_inverter_coordinators
)
elif needs_readings:
_LOGGER.debug(
"No MPPT model exposed by inverter %s at Modbus unit %s",
inverter_info.solar_net_id,
unit_id,
)

if await self._modbus_control_allowed(modbus_inverter, unit_id):
if needs_settings and await self._modbus_control_allowed(
modbus_inverter, unit_id
):
settings = FroniusModbusSettingsUpdateCoordinator(
hass=self.hass,
solar_net=self,
Expand All @@ -396,8 +441,9 @@ async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
modbus_inverter=modbus_inverter,
config_entry=self.config_entry,
)
if await self._start_modbus_coordinator(settings):
self.modbus_settings_coordinators.append(settings)
await self._start_modbus_coordinator(
settings, self.modbus_settings_coordinators
)
Comment thread
farmio marked this conversation as resolved.

_LOGGER.debug(
"Modbus enabled for inverter %s (UID: %s, unit ID: %s)",
Expand All @@ -406,8 +452,10 @@ async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
unit_id,
)

async def _start_modbus_coordinator(
self, coordinator: FroniusCoordinatorBase
async def _start_modbus_coordinator[
_ModbusCoordinatorT: FroniusModbusCoordinatorBase
](
self, coordinator: _ModbusCoordinatorT, coordinators: list[_ModbusCoordinatorT]
) -> bool:
"""Do the first refresh of a Modbus coordinator, reporting success.

Expand All @@ -418,10 +466,15 @@ async def _start_modbus_coordinator(
await coordinator.async_refresh()
if not coordinator.last_update_success:
return False
# the platforms tell the coordinators apart by the list they are in,
# so it is kept before they are told about this one
coordinators.append(coordinator)
Comment thread
farmio marked this conversation as resolved.
# Only for re-scans. Initial setup adds entities through the
# platforms' async_setup_entry.
if self.config_entry.state is ConfigEntryState.LOADED:
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, coordinator)
async_dispatcher_send(
self.hass, discovery_signal(self.config_entry.entry_id), coordinator
)
return True

async def _modbus_control_allowed(
Expand Down
13 changes: 12 additions & 1 deletion homeassistant/components/fronius/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@
DEFAULT_MODBUS_PORT: Final = 502

type SolarNetId = str
SOLAR_NET_DISCOVERY_NEW: Final = "fronius_discovery_new"
_SOLAR_NET_DISCOVERY_NEW: Final = "fronius_discovery_new"


def discovery_signal(entry_id: str) -> str:
"""Return the signal carrying coordinators of an entry found after setup.

One signal per config entry: a device found by one entry's re-scan has
nothing to do with the platforms of another.
"""
return f"{_SOLAR_NET_DISCOVERY_NEW}_{entry_id}"


SOLAR_NET_ID_POWER_FLOW: SolarNetId = "power_flow"
SOLAR_NET_ID_SYSTEM: SolarNetId = "system"
SOLAR_NET_RESCAN_TIMER: Final = 60
Expand Down
27 changes: 24 additions & 3 deletions homeassistant/components/fronius/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@

from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import PERCENTAGE, EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import discovery_signal
from .entity import FroniusEntity, FroniusEntityDescription, ModbusComponentFn

if TYPE_CHECKING:
from . import FroniusConfigEntry
from .coordinator import FroniusModbusSettingsUpdateCoordinator
from .coordinator import (
FroniusCoordinatorBase,
FroniusModbusSettingsUpdateCoordinator,
)

# writes go to one device at a time
PARALLEL_UPDATES: Final = 1
Expand Down Expand Up @@ -84,11 +89,27 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fronius number entities based on a config entry."""
for coordinator in config_entry.runtime_data.modbus_settings_coordinators:
solar_net = config_entry.runtime_data
for coordinator in solar_net.modbus_settings_coordinators:
coordinator.add_entities_for_seen_keys(
async_add_entities, Platform.NUMBER, ModbusSetpointNumber
)

@callback
def async_add_new_entities(coordinator: FroniusCoordinatorBase) -> None:
"""Add the entities of a coordinator found after setup."""
if Platform.NUMBER not in coordinator.valid_descriptions:
return
coordinator.add_entities_for_seen_keys(
async_add_entities, Platform.NUMBER, ModbusSetpointNumber
)

config_entry.async_on_unload(
async_dispatcher_connect(
hass, discovery_signal(config_entry.entry_id), async_add_new_entities
)
)


class ModbusSetpointNumber(FroniusEntity, NumberEntity):
"""A writable setpoint of an inverters Modbus interface."""
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/fronius/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
from .const import (
DOMAIN,
INVERTER_ERROR_CODES,
SOLAR_NET_DISCOVERY_NEW,
InverterStatusCodeOption,
MeterLocationCodeOption,
OhmPilotStateCodeOption,
discovery_signal,
get_inverter_status_message,
get_meter_location_description,
get_ohmpilot_state_message,
Expand Down Expand Up @@ -101,6 +101,8 @@ async def async_setup_entry(
@callback
def async_add_new_entities(coordinator: FroniusCoordinatorBase) -> None:
"""Add newly found inverter entities."""
if Platform.SENSOR not in coordinator.valid_descriptions:
return
constructor = (
ModbusInverterSensor
if coordinator in solar_net.modbus_inverter_coordinators
Expand All @@ -113,7 +115,7 @@ def async_add_new_entities(coordinator: FroniusCoordinatorBase) -> None:
config_entry.async_on_unload(
async_dispatcher_connect(
hass,
SOLAR_NET_DISCOVERY_NEW,
discovery_signal(config_entry.entry_id),
async_add_new_entities,
)
)
Expand Down
27 changes: 24 additions & 3 deletions homeassistant/components/fronius/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import discovery_signal
from .entity import FroniusEntity, FroniusEntityDescription, ModbusComponentFn

if TYPE_CHECKING:
from . import FroniusConfigEntry
from .coordinator import FroniusModbusSettingsUpdateCoordinator
from .coordinator import (
FroniusCoordinatorBase,
FroniusModbusSettingsUpdateCoordinator,
)

# writes go to one device at a time
PARALLEL_UPDATES: Final = 1
Expand Down Expand Up @@ -63,11 +68,27 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fronius switch entities based on a config entry."""
for coordinator in config_entry.runtime_data.modbus_settings_coordinators:
solar_net = config_entry.runtime_data
for coordinator in solar_net.modbus_settings_coordinators:
coordinator.add_entities_for_seen_keys(
async_add_entities, Platform.SWITCH, ModbusControlSwitch
)

@callback
def async_add_new_entities(coordinator: FroniusCoordinatorBase) -> None:
"""Add the entities of a coordinator found after setup."""
if Platform.SWITCH not in coordinator.valid_descriptions:
return
coordinator.add_entities_for_seen_keys(
async_add_entities, Platform.SWITCH, ModbusControlSwitch
)

config_entry.async_on_unload(
async_dispatcher_connect(
hass, discovery_signal(config_entry.entry_id), async_add_new_entities
)
)


class ModbusControlSwitch(FroniusEntity, SwitchEntity):
"""A control of an inverters Modbus interface that is on or off.
Expand Down
Loading
Loading