Skip to content

Commit

Permalink
Merge pull request #636 from WillCodeForCats/config-inverter-id-list
Browse files Browse the repository at this point in the history
Support for non-sequential modbus device ID numbers
  • Loading branch information
WillCodeForCats authored Aug 28, 2024
2 parents e3f1249 + ff32a77 commit b7f731b
Show file tree
Hide file tree
Showing 15 changed files with 387 additions and 270 deletions.
64 changes: 51 additions & 13 deletions custom_components/solaredge_modbus_multi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import asyncio
import logging
from datetime import timedelta
from typing import Any

import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
Expand All @@ -15,7 +14,7 @@
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, ConfDefaultInt, RetrySettings
from .const import DOMAIN, ConfDefaultInt, ConfName, RetrySettings
from .hub import DataUpdateFailed, HubInitFailed, SolarEdgeModbusMultiHub

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -67,17 +66,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SolarEdge Modbus Muti from a config entry."""

entry_updates: dict[str, Any] = {}
if CONF_SCAN_INTERVAL in entry.data:
data = {**entry.data}
entry_updates["data"] = data
entry_updates["options"] = {
**entry.options,
CONF_SCAN_INTERVAL: data.pop(CONF_SCAN_INTERVAL),
}
if entry_updates:
hass.config_entries.async_update_entry(entry, **entry_updates)

solaredge_hub = SolarEdgeModbusMultiHub(
hass, entry.entry_id, entry.data, entry.options
)
Expand Down Expand Up @@ -166,6 +154,56 @@ async def async_remove_config_entry_device(
return True


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating from config version "
f"{config_entry.version}.{config_entry.minor_version}"
)

if config_entry.version > 1:
return False

if config_entry.version == 1:

update_data = {**config_entry.data}
update_options = {**config_entry.options}

if CONF_SCAN_INTERVAL in update_data:
update_options = {
**update_options,
CONF_SCAN_INTERVAL: update_data.pop(CONF_SCAN_INTERVAL),
}

start_device_id = update_data.pop(ConfName.DEVICE_ID)
number_of_inverters = update_data.pop(ConfName.NUMBER_INVERTERS)

inverter_list = []
for inverter_index in range(number_of_inverters):
inverter_unit_id = inverter_index + start_device_id
inverter_list.append(inverter_unit_id)

update_data = {
**update_data,
ConfName.DEVICE_LIST: inverter_list,
}

hass.config_entries.async_update_entry(
config_entry,
data=update_data,
options=update_options,
version=2,
minor_version=0,
)

_LOGGER.warning(
"Migrated to config version "
f"{config_entry.version}.{config_entry.minor_version}"
)

return True


class SolarEdgeCoordinator(DataUpdateCoordinator):
def __init__(
self, hass: HomeAssistant, hub: SolarEdgeModbusMultiHub, scan_interval: int
Expand Down
163 changes: 86 additions & 77 deletions custom_components/solaredge_modbus_multi/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import re
from typing import Any

import homeassistant.helpers.config_validation as cv
Expand All @@ -11,9 +12,17 @@
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError

from .const import DEFAULT_NAME, DOMAIN, ConfDefaultFlag, ConfDefaultInt, ConfName
from .helpers import host_valid
from .const import (
DEFAULT_NAME,
DOMAIN,
ConfDefaultFlag,
ConfDefaultInt,
ConfDefaultStr,
ConfName,
)
from .helpers import device_list_from_string, host_valid


@callback
Expand All @@ -28,8 +37,8 @@ def solaredge_modbus_multi_entries(hass: HomeAssistant):
class SolaredgeModbusMultiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SolarEdge Modbus Multi."""

VERSION = 1
MINOR_VERSION = 1
VERSION = 2
MINOR_VERSION = 0

@staticmethod
@callback
Expand All @@ -45,41 +54,44 @@ async def async_step_user(

if user_input is not None:
user_input[CONF_HOST] = user_input[CONF_HOST].lower()
user_input[ConfName.DEVICE_LIST] = re.sub(
r"\s+", "", user_input[ConfName.DEVICE_LIST], flags=re.UNICODE
)

if not host_valid(user_input[CONF_HOST]):
errors[CONF_HOST] = "invalid_host"
elif user_input[CONF_HOST] in solaredge_modbus_multi_entries(self.hass):
errors[CONF_HOST] = "already_configured"
elif user_input[CONF_PORT] < 1:
errors[CONF_PORT] = "invalid_tcp_port"
elif user_input[CONF_PORT] > 65535:
errors[CONF_PORT] = "invalid_tcp_port"
elif user_input[ConfName.DEVICE_ID] > 247:
errors[ConfName.DEVICE_ID] = "max_device_id"
elif user_input[ConfName.DEVICE_ID] < 1:
errors[ConfName.DEVICE_ID] = "min_device_id"
elif user_input[ConfName.NUMBER_INVERTERS] > 32:
errors[ConfName.NUMBER_INVERTERS] = "max_inverters"
elif user_input[ConfName.NUMBER_INVERTERS] < 1:
errors[ConfName.NUMBER_INVERTERS] = "min_inverters"
elif (
user_input[ConfName.NUMBER_INVERTERS] + user_input[ConfName.DEVICE_ID]
> 247
):
errors[ConfName.NUMBER_INVERTERS] = "too_many_inverters"
else:
await self.async_set_unique_id(user_input[CONF_HOST])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
try:
inverter_count = len(
device_list_from_string(user_input[ConfName.DEVICE_LIST])
)
except HomeAssistantError as e:
errors[ConfName.DEVICE_LIST] = f"{e}"

else:
if not host_valid(user_input[CONF_HOST]):
errors[CONF_HOST] = "invalid_host"
elif user_input[CONF_HOST] in solaredge_modbus_multi_entries(self.hass):
errors[CONF_HOST] = "already_configured"
elif not 1 <= user_input[CONF_PORT] <= 65535:
errors[CONF_PORT] = "invalid_tcp_port"
elif not 1 <= inverter_count <= 32:
errors[ConfName.DEVICE_LIST] = "invalid_inverter_count"
else:
await self.async_set_unique_id(user_input[CONF_HOST])

self._abort_if_unique_id_configured()

user_input[ConfName.DEVICE_LIST] = device_list_from_string(
user_input[ConfName.DEVICE_LIST]
)

return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
else:
user_input = {
CONF_NAME: DEFAULT_NAME,
CONF_HOST: "",
CONF_PORT: ConfDefaultInt.PORT,
ConfName.NUMBER_INVERTERS: ConfDefaultInt.NUMBER_INVERTERS,
ConfName.DEVICE_ID: ConfDefaultInt.DEVICE_ID,
ConfName.DEVICE_LIST: ConfDefaultStr.DEVICE_LIST,
}

return self.async_show_form(
Expand All @@ -92,12 +104,9 @@ async def async_step_user(
int
),
vol.Required(
f"{ConfName.NUMBER_INVERTERS}",
default=user_input[ConfName.NUMBER_INVERTERS],
): vol.Coerce(int),
vol.Required(
f"{ConfName.DEVICE_ID}", default=user_input[ConfName.DEVICE_ID]
): vol.Coerce(int),
f"{ConfName.DEVICE_LIST}",
default=user_input[ConfName.DEVICE_LIST],
): cv.string,
},
),
errors=errors,
Expand All @@ -111,48 +120,51 @@ async def async_step_reconfigure(
config_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
assert config_entry
unique_id = config_entry.unique_id

if user_input is not None:
user_input[CONF_HOST] = user_input[CONF_HOST].lower()
user_input[ConfName.DEVICE_LIST] = re.sub(
r"\s+", "", user_input[ConfName.DEVICE_LIST], flags=re.UNICODE
)

if not host_valid(user_input[CONF_HOST]):
errors[CONF_HOST] = "invalid_host"
elif user_input[CONF_PORT] < 1:
errors[CONF_PORT] = "invalid_tcp_port"
elif user_input[CONF_PORT] > 65535:
errors[CONF_PORT] = "invalid_tcp_port"
elif user_input[ConfName.DEVICE_ID] > 247:
errors[ConfName.DEVICE_ID] = "max_device_id"
elif user_input[ConfName.DEVICE_ID] < 1:
errors[ConfName.DEVICE_ID] = "min_device_id"
elif user_input[ConfName.NUMBER_INVERTERS] > 32:
errors[ConfName.NUMBER_INVERTERS] = "max_inverters"
elif user_input[ConfName.NUMBER_INVERTERS] < 1:
errors[ConfName.NUMBER_INVERTERS] = "min_inverters"
elif (
user_input[ConfName.NUMBER_INVERTERS] + user_input[ConfName.DEVICE_ID]
> 247
):
errors[ConfName.NUMBER_INVERTERS] = "too_many_inverters"
else:
return self.async_update_reload_and_abort(
config_entry,
unique_id=unique_id,
data={**config_entry.data, **user_input},
reason="reconfigure_successful",
try:
inverter_count = len(
device_list_from_string(user_input[ConfName.DEVICE_LIST])
)
except HomeAssistantError as e:
errors[ConfName.DEVICE_LIST] = f"{e}"

else:
if not host_valid(user_input[CONF_HOST]):
errors[CONF_HOST] = "invalid_host"
elif not 1 <= user_input[CONF_PORT] <= 65535:
errors[CONF_PORT] = "invalid_tcp_port"
elif not 1 <= inverter_count <= 32:
errors[ConfName.DEVICE_LIST] = "invalid_inverter_count"
else:

user_input[ConfName.DEVICE_LIST] = device_list_from_string(
user_input[ConfName.DEVICE_LIST]
)

return self.async_update_reload_and_abort(
config_entry,
unique_id=config_entry.unique_id,
data={**config_entry.data, **user_input},
reason="reconfigure_successful",
)
else:
reconfig_device_list = ",".join(
str(device)
for device in config_entry.data.get(
ConfName.DEVICE_LIST, ConfDefaultStr.DEVICE_LIST
)
)

user_input = {
CONF_HOST: config_entry.data.get(CONF_HOST),
CONF_PORT: config_entry.data.get(CONF_PORT, ConfDefaultInt.PORT),
ConfName.NUMBER_INVERTERS: config_entry.data.get(
ConfName.NUMBER_INVERTERS, ConfDefaultInt.NUMBER_INVERTERS
),
ConfName.DEVICE_ID: config_entry.data.get(
ConfName.DEVICE_ID, ConfDefaultInt.DEVICE_ID
),
ConfName.DEVICE_LIST: reconfig_device_list,
}

return self.async_show_form(
Expand All @@ -164,12 +176,9 @@ async def async_step_reconfigure(
int
),
vol.Required(
f"{ConfName.NUMBER_INVERTERS}",
default=user_input[ConfName.NUMBER_INVERTERS],
): vol.Coerce(int),
vol.Required(
f"{ConfName.DEVICE_ID}", default=user_input[ConfName.DEVICE_ID]
): vol.Coerce(int),
f"{ConfName.DEVICE_LIST}",
default=user_input[ConfName.DEVICE_LIST],
): cv.string,
},
),
errors=errors,
Expand Down
15 changes: 11 additions & 4 deletions custom_components/solaredge_modbus_multi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ class ConfDefaultInt(IntEnum):

SCAN_INTERVAL = 300
PORT = 1502
NUMBER_INVERTERS = 1
DEVICE_ID = 1
SLEEP_AFTER_WRITE = 0
BATTERY_RATING_ADJUST = 0
BATTERY_ENERGY_RESET_CYCLES = 0
Expand All @@ -97,9 +95,14 @@ class ConfDefaultFlag(IntEnum):
ALLOW_BATTERY_ENERGY_RESET = 0


class ConfDefaultStr(StrEnum):
"""Defaults for options that are strings."""

DEVICE_LIST = "1"


class ConfName(StrEnum):
NUMBER_INVERTERS = "number_of_inverters"
DEVICE_ID = "device_id"
DEVICE_LIST = "device_list"
DETECT_METERS = "detect_meters"
DETECT_BATTERIES = "detect_batteries"
DETECT_EXTRAS = "detect_extras"
Expand All @@ -112,6 +115,10 @@ class ConfName(StrEnum):
BATTERY_RATING_ADJUST = "battery_rating_adjust"
BATTERY_ENERGY_RESET_CYCLES = "battery_energy_reset_cycles"

# Old config entry names for migration
NUMBER_INVERTERS = "number_of_inverters"
DEVICE_ID = "device_id"


class SunSpecAccum(IntEnum):
NA16 = 0x0000
Expand Down
Loading

0 comments on commit b7f731b

Please sign in to comment.