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
25 changes: 20 additions & 5 deletions homeassistant/components/fronius/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from homeassistant.helpers.event import async_track_time_interval

from .const import (
CONF_AUTO_REVERT_POWER_LIMIT,
CONF_MODBUS_PORT,
DEFAULT_MODBUS_PORT,
DOMAIN,
Expand Down Expand Up @@ -85,6 +86,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: FroniusConfigEntry) ->
# add the Modbus port setting
data = {CONF_MODBUS_PORT: DEFAULT_MODBUS_PORT, **entry.data}
hass.config_entries.async_update_entry(entry, data=data, minor_version=2)
if entry.minor_version < 3:
# add the Modbus setpoint fallback setting
data = {CONF_AUTO_REVERT_POWER_LIMIT: False, **entry.data}
hass.config_entries.async_update_entry(entry, data=data, minor_version=3)
return True


Expand Down Expand Up @@ -336,10 +341,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 +379,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 @@ -379,14 +391,16 @@ async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
)
if await self._start_modbus_coordinator(readings):
self.modbus_inverter_coordinators.append(readings)
else:
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
):
Comment thread
farmio marked this conversation as resolved.
settings = FroniusModbusSettingsUpdateCoordinator(
hass=self.hass,
solar_net=self,
Expand All @@ -398,6 +412,7 @@ async def _init_modbus_inverter(self, inverter_info: FroniusDeviceInfo) -> None:
)
if await self._start_modbus_coordinator(settings):
self.modbus_settings_coordinators.append(settings)
await settings.async_start_heartbeat()

_LOGGER.debug(
"Modbus enabled for inverter %s (UID: %s, unit ID: %s)",
Expand Down
48 changes: 27 additions & 21 deletions homeassistant/components/fronius/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,25 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo

from .const import CONF_MODBUS_PORT, DEFAULT_MODBUS_PORT, DOMAIN, FroniusConfigEntryData
from .const import (
CONF_AUTO_REVERT_POWER_LIMIT,
CONF_MODBUS_PORT,
DEFAULT_MODBUS_PORT,
DOMAIN,
FroniusConfigEntryData,
)

_LOGGER: Final = logging.getLogger(__name__)

DHCP_REQUEST_DELAY: Final = 60

MODBUS_PORT_SELECTOR: Final = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
# the settings that are not the host - shown when adding and reconfiguring
SETTINGS_SCHEMA: Final = {
vol.Required(CONF_MODBUS_PORT, default=DEFAULT_MODBUS_PORT): vol.All(
vol.Coerce(int), vol.Range(min=1, max=65535)
),
vol.Required(CONF_AUTO_REVERT_POWER_LIMIT, default=False): bool,
}


def create_title(info: FroniusConfigEntryData) -> str:
Expand All @@ -32,7 +44,10 @@ def create_title(info: FroniusConfigEntryData) -> str:


async def validate_host(
hass: HomeAssistant, host: str, modbus_port: int = DEFAULT_MODBUS_PORT
hass: HomeAssistant,
host: str,
modbus_port: int = DEFAULT_MODBUS_PORT,
auto_revert_power_limit: bool = False,
) -> tuple[str, FroniusConfigEntryData]:
"""Validate the user input allows us to connect."""
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
Expand All @@ -48,6 +63,7 @@ async def validate_host(
host=host,
is_logger=True,
modbus_port=modbus_port,
auto_revert_power_limit=auto_revert_power_limit,
)
# Gen24 devices don't provide GetLoggerInfo
try:
Expand All @@ -61,14 +77,15 @@ async def validate_host(
host=host,
is_logger=False,
modbus_port=modbus_port,
auto_revert_power_limit=auto_revert_power_limit,
)


class FroniusConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fronius."""

VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3

def __init__(self) -> None:
"""Initialize flow."""
Expand All @@ -87,6 +104,7 @@ async def async_step_user(
self.hass,
user_input[CONF_HOST],
modbus_port=user_input[CONF_MODBUS_PORT],
auto_revert_power_limit=user_input[CONF_AUTO_REVERT_POWER_LIMIT],
)
except CannotConnect:
errors["base"] = "cannot_connect"
Expand All @@ -101,14 +119,7 @@ async def async_step_user(

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(
CONF_MODBUS_PORT, default=DEFAULT_MODBUS_PORT
): MODBUS_PORT_SELECTOR,
}
),
data_schema=vol.Schema({vol.Required(CONF_HOST): str, **SETTINGS_SCHEMA}),
errors=errors,
)

Expand Down Expand Up @@ -166,6 +177,7 @@ async def async_step_reconfigure(
self.hass,
user_input[CONF_HOST],
modbus_port=user_input[CONF_MODBUS_PORT],
auto_revert_power_limit=user_input[CONF_AUTO_REVERT_POWER_LIMIT],
)
except CannotConnect:
errors["base"] = "cannot_connect"
Expand All @@ -178,17 +190,11 @@ async def async_step_reconfigure(

return self.async_update_reload_and_abort(reconfigure_entry, data=info)

host = reconfigure_entry.data[CONF_HOST]
modbus_port = reconfigure_entry.data.get(CONF_MODBUS_PORT, DEFAULT_MODBUS_PORT)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=host): str,
vol.Required(
CONF_MODBUS_PORT, default=modbus_port
): MODBUS_PORT_SELECTOR,
}
data_schema=self.add_suggested_values_to_schema(
vol.Schema({vol.Required(CONF_HOST): str, **SETTINGS_SCHEMA}),
reconfigure_entry.data,
),
description_placeholders={"device": reconfigure_entry.title},
errors=errors,
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/fronius/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Constants for the Fronius integration."""

from datetime import timedelta
from enum import StrEnum
from typing import Final, NamedTuple, TypedDict

Expand All @@ -11,6 +12,12 @@
CONF_MODBUS_PORT: Final = "modbus_port"
DEFAULT_MODBUS_PORT: Final = 502

CONF_AUTO_REVERT_POWER_LIMIT: Final = "auto_revert_power_limit"
# how long the device holds a setpoint after it last received it
AUTO_REVERT_SECONDS: Final = 3600
# the setpoint is sent again this often, so a restart has room to spare
HEARTBEAT_INTERVAL: Final = timedelta(minutes=15)

type SolarNetId = str
SOLAR_NET_DISCOVERY_NEW: Final = "fronius_discovery_new"
SOLAR_NET_ID_POWER_FLOW: SolarNetId = "power_flow"
Expand All @@ -24,6 +31,7 @@ class FroniusConfigEntryData(TypedDict):
host: str
is_logger: bool
modbus_port: int
auto_revert_power_limit: bool


class FroniusDeviceInfo(NamedTuple):
Expand Down
Loading
Loading