diff --git a/.github/workflows/python-quality.yaml b/.github/workflows/python-quality.yaml index 388338e0..b52cc486 100644 --- a/.github/workflows/python-quality.yaml +++ b/.github/workflows/python-quality.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: WillCodeForCats/python-lint-action@v1.0.7 + - uses: WillCodeForCats/python-lint-action@v1.0.9 with: python-root-list: "custom_components/solaredge_modbus_multi" use-flake8: true diff --git a/README.md b/README.md index e1d80643..73acbe8a 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,7 @@ # SolarEdge Modbus Multi -Home Assistant integration `solaredge-modbus-multi` supports SolarEdge inverters with Modbus/TCP local polling. It works with single inverters, multiple inverters, meters, batteries, and many other improvements over other integrations that didn't work well with a multi-device setup. - -It is designed to communicate locally using Modbus/TCP where you have a single Leader inverter connected with one or more Follower inverters chained using the RS485 bus. Each inverter can connect to three meters and two batteries. - -Simple single inverter setups are fully supported - multiple devices is a feature, not a requirement. +This integration provides Modbus/TCP local polling to one or more SolarEdge inverters for Home Assistant. Each inverter can support three meters and three batteries over Modbus/TCP. It works with single inverters, multiple inverters, meters, and batteries. It has significant improvements over similar integrations, and `solaredge_modbus_multi` is actively maintained. ### Features * Inverter support for 1 to 32 SolarEdge inverters. @@ -20,20 +16,20 @@ Simple single inverter setups are fully supported - multiple devices is a featur * Connects locally using Modbus/TCP - no cloud dependencies. * Informational sensor for device and its attributes * Supports status and error reporting sensors. -* User friendly configuration through Config Flow. +* User friendly: Config Flow, Options, Repair Issues, and Reconfiguration. Read about more features on the wiki: [WillCodeForCats/solaredge-modbus-multi/wiki](https://github.com/WillCodeForCats/solaredge-modbus-multi/wiki) -Note: The modbus interface currently only defines up to 2 batteries per inverter (even if the SolarEdge cloud monitoring platform shows more). - ## Installation Install with [HACS](https://hacs.xyz): Search for "SolarEdge Modbus Multi" in the default repository, +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=WillCodeForCats&repository=solaredge-modbus-multi&category=integration) + OR -Copy the `solaredge_modbus_multi` folder into to your Home Assistant `config/custom_components` folder. +Download the [latest release](https://github.com/WillCodeForCats/solaredge-modbus-multi/releases) and copy the `solaredge_modbus_multi` folder into to your Home Assistant `config/custom_components` folder. -After rebooting Home Assistant, this integration can be configured through the integration setup UI. +After rebooting Home Assistant, this integration can be configured through the integration setup UI. It also supports options, repair issues, and reconfiguration through the user interface. ### Configuration [WillCodeForCats/solaredge-modbus-multi/wiki/Configuration](https://github.com/WillCodeForCats/solaredge-modbus-multi/wiki/Configuration) @@ -42,9 +38,9 @@ After rebooting Home Assistant, this integration can be configured through the i [WillCodeForCats/solaredge-modbus-multi/wiki](https://github.com/WillCodeForCats/solaredge-modbus-multi/wiki) ### Required Versions -* Home Assistant 2024.4.0 or newer +* Home Assistant 2024.9.0 or newer * Python 3.11 or newer -* pymodbus 3.6.6 or newer +* pymodbus 3.6.6 through 3.7.4 ## Specifications [WillCodeForCats/solaredge-modbus-multi/tree/main/doc](https://github.com/WillCodeForCats/solaredge-modbus-multi/tree/main/doc) diff --git a/custom_components/solaredge_modbus_multi/__init__.py b/custom_components/solaredge_modbus_multi/__init__.py index 5c9e9902..d0334637 100644 --- a/custom_components/solaredge_modbus_multi/__init__.py +++ b/custom_components/solaredge_modbus_multi/__init__.py @@ -5,9 +5,7 @@ import asyncio import logging from datetime import timedelta -from typing import Any -import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL, Platform @@ -16,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__) @@ -48,7 +46,6 @@ vol.Optional("timeout"): vol.Coerce(int), vol.Optional("reconnect_delay"): vol.Coerce(float), vol.Optional("reconnect_delay_max"): vol.Coerce(float), - vol.Optional("retry_on_empty"): cv.boolean, } ), } @@ -69,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 ) @@ -168,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 diff --git a/custom_components/solaredge_modbus_multi/binary_sensor.py b/custom_components/solaredge_modbus_multi/binary_sensor.py index 5985680a..09846016 100644 --- a/custom_components/solaredge_modbus_multi/binary_sensor.py +++ b/custom_components/solaredge_modbus_multi/binary_sensor.py @@ -33,8 +33,7 @@ async def async_setup_entry( if hub.option_detect_extras and inverter.advanced_power_control: entities.append(AdvPowerControlEnabled(inverter, config_entry, coordinator)) - if hub.option_detect_extras: - entities.append(GridStatusOnOff(inverter, config_entry, coordinator)) + entities.append(GridStatusOnOff(inverter, config_entry, coordinator)) if entities: async_add_entities(entities) diff --git a/custom_components/solaredge_modbus_multi/config_flow.py b/custom_components/solaredge_modbus_multi/config_flow.py index 97248f7b..468f0a0f 100644 --- a/custom_components/solaredge_modbus_multi/config_flow.py +++ b/custom_components/solaredge_modbus_multi/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from typing import Any import homeassistant.helpers.config_validation as cv @@ -9,27 +10,46 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import 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 -def solaredge_modbus_multi_entries(hass: HomeAssistant): - """Return the hosts already configured.""" - return set( - entry.data[CONF_HOST].lower() - for entry in hass.config_entries.async_entries(DOMAIN) - ) +def generate_config_schema(step_id: str, user_input: dict[str, Any]) -> vol.Schema: + """Generate config flow or repair schema.""" + schema: dict[vol.Marker, Any] = {} + + if step_id == "user": + schema |= {vol.Required(CONF_NAME, default=user_input[CONF_NAME]): cv.string} + + if step_id in ["reconfigure", "confirm", "user"]: + schema |= { + vol.Required(CONF_HOST, default=user_input[CONF_HOST]): cv.string, + vol.Required(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int), + vol.Required( + f"{ConfName.DEVICE_LIST}", + default=user_input[ConfName.DEVICE_LIST], + ): cv.string, + } + + return vol.Schema(schema) 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 @@ -45,61 +65,47 @@ 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 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( step_id="user", - data_schema=vol.Schema( - { - vol.Optional(CONF_NAME, default=user_input[CONF_NAME]): cv.string, - vol.Required(CONF_HOST, default=user_input[CONF_HOST]): cv.string, - vol.Required(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce( - 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), - }, - ), + data_schema=generate_config_schema("user", user_input), errors=errors, ) @@ -111,67 +117,56 @@ 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( step_id="reconfigure", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=user_input[CONF_HOST]): cv.string, - vol.Required(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce( - 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), - }, - ), + data_schema=generate_config_schema("reconfigure", user_input), errors=errors, ) diff --git a/custom_components/solaredge_modbus_multi/const.py b/custom_components/solaredge_modbus_multi/const.py index 91f38c24..1f007271 100644 --- a/custom_components/solaredge_modbus_multi/const.py +++ b/custom_components/solaredge_modbus_multi/const.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from enum import Flag, IntEnum, StrEnum +from enum import IntEnum, StrEnum from typing import Final DOMAIN = "solaredge_modbus_multi" @@ -28,6 +28,26 @@ ) +class ModbusExceptions: + """An enumeration of the valid modbus exceptions.""" + + """ + Copied from pymodbus source: + https://github.com/pymodbus-dev/pymodbus/blob/a1c14c7a8fbea52618ba1cbc9933c1dd24c3339d/pymodbus/pdu/pdu.py#L72 + """ + + IllegalFunction = 0x01 + IllegalAddress = 0x02 + IllegalValue = 0x03 + SlaveFailure = 0x04 + Acknowledge = 0x05 + SlaveBusy = 0x06 + NegativeAcknowledge = 0x07 + MemoryParityError = 0x08 + GatewayPathUnavailable = 0x0A + GatewayNoResponse = 0x0B + + class RetrySettings(IntEnum): """Retry settings when opening a connection to the inverter fails.""" @@ -51,12 +71,6 @@ class ModbusDefaults(IntEnum): ReconnectDelayMax = 3.0 # Maximum in seconds.milliseconds before reconnecting. -class ModbusFlags(Flag): - """Values to pass to pymodbus""" - - RetryOnEmpty = False # Retry on empty response. - - class SolarEdgeTimeouts(IntEnum): """Timeouts in milliseconds.""" @@ -83,8 +97,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 @@ -95,7 +107,7 @@ class ConfDefaultFlag(IntEnum): DETECT_METERS = 1 DETECT_BATTERIES = 0 - DETECT_EXTRAS = 1 + DETECT_EXTRAS = 0 KEEP_MODBUS_OPEN = 0 ADV_PWR_CONTROL = 0 ADV_STORAGE_CONTROL = 0 @@ -103,9 +115,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" @@ -118,6 +135,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 diff --git a/custom_components/solaredge_modbus_multi/diagnostics.py b/custom_components/solaredge_modbus_multi/diagnostics.py index bdd1ee50..c26f30ef 100644 --- a/custom_components/solaredge_modbus_multi/diagnostics.py +++ b/custom_components/solaredge_modbus_multi/diagnostics.py @@ -12,9 +12,9 @@ from .helpers import float_to_hex REDACT_CONFIG = {"unique_id", "host"} -REDACT_INVERTER = {"identifiers", "C_SerialNumber"} -REDACT_METER = {"identifiers", "C_SerialNumber"} -REDACT_BATTERY = {"identifiers", "B_SerialNumber"} +REDACT_INVERTER = {"identifiers", "C_SerialNumber", "serial_number"} +REDACT_METER = {"identifiers", "C_SerialNumber", "serial_number"} +REDACT_BATTERY = {"identifiers", "B_SerialNumber", "serial_number"} def format_values(format_input) -> Any: diff --git a/custom_components/solaredge_modbus_multi/helpers.py b/custom_components/solaredge_modbus_multi/helpers.py index 7d950689..dc64522a 100644 --- a/custom_components/solaredge_modbus_multi/helpers.py +++ b/custom_components/solaredge_modbus_multi/helpers.py @@ -3,6 +3,8 @@ import ipaddress import struct +from homeassistant.exceptions import HomeAssistantError + from .const import DOMAIN_REGEX @@ -37,3 +39,77 @@ def host_valid(host): except ValueError: return DOMAIN_REGEX.match(host) + + +def device_list_from_string(value: str) -> list[int]: + """The function `device_list_from_string` takes a string input and returns a list of + device IDs, where the input can be a single ID or a range of IDs separated by commas + + Parameters + ---------- + value + The `value` parameter is a string that represents a list of device IDs. The + device IDs can be specified as individual IDs or as ranges separated by a hyphen + For example, the string "1,3-5,7" represents the device IDs 1, 3, 4, 5 and 7 + + Returns + ------- + The function `device_list_from_string` returns a list of device IDs. + + Credit: https://github.com/thargy/modbus-scanner/blob/main/scan.py + """ + + parts = [p.strip() for p in value.split(",")] + ids = [] + for p in parts: + r = [i.strip() for i in p.split("-")] + if len(r) < 2: + # We have a single id + ids.append(check_device_id(r[0])) + + elif len(r) > 2: + # Invalid range, multiple '-'s + raise HomeAssistantError("invalid_range_format") + + else: + # Looks like a range + start = check_device_id(r[0]) + end = check_device_id(r[1]) + if end < start: + raise HomeAssistantError("invalid_range_lte") + + ids.extend(range(start, end + 1)) + + return sorted(set(ids)) + + +def check_device_id(value: str | int) -> int: + """The `check_device_id` function takes a value and checks if it is a valid device + ID between 1 and 247, raising an error if it is not. + + Parameters + ---------- + value + The value parameter is the input value that is + being checked for validity as a device ID. + + Returns + ------- + the device ID as an integer. + + Credit: https://github.com/thargy/modbus-scanner/blob/main/scan.py + """ + + if len(value) == 0: + raise HomeAssistantError("empty_device_id") + + try: + id = int(value) + + if (id < 1) or id > 247: + raise HomeAssistantError("invalid_device_id") + + except ValueError: + raise HomeAssistantError("invalid_device_id") + + return id diff --git a/custom_components/solaredge_modbus_multi/hub.py b/custom_components/solaredge_modbus_multi/hub.py index ff26e3f5..af07b4bb 100644 --- a/custom_components/solaredge_modbus_multi/hub.py +++ b/custom_components/solaredge_modbus_multi/hub.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import importlib.metadata import logging from collections import OrderedDict @@ -9,15 +10,11 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.entity import DeviceInfo - -try: - from pymodbus.client import AsyncModbusTcpClient - from pymodbus.constants import Endian - from pymodbus.exceptions import ConnectionException, ModbusIOException - from pymodbus.payload import BinaryPayloadDecoder - from pymodbus.pdu import ExceptionResponse, ModbusExceptions -except ImportError: - raise ImportError("pymodbus is not installed, or pymodbus version is not supported") +from pymodbus.client import AsyncModbusTcpClient +from pymodbus.constants import Endian +from pymodbus.exceptions import ConnectionException, ModbusIOException +from pymodbus.payload import BinaryPayloadDecoder +from pymodbus.pdu import ExceptionResponse from .const import ( BATTERY_REG_BASE, @@ -25,9 +22,10 @@ METER_REG_BASE, ConfDefaultFlag, ConfDefaultInt, + ConfDefaultStr, ConfName, ModbusDefaults, - ModbusFlags, + ModbusExceptions, RetrySettings, SolarEdgeTimeouts, SunSpecNotImpl, @@ -35,6 +33,7 @@ from .helpers import float_to_hex _LOGGER = logging.getLogger(__name__) +pymodbus_version = importlib.metadata.version("pymodbus") class SolarEdgeException(Exception): @@ -127,11 +126,8 @@ def __init__( self._host = entry_data[CONF_HOST] self._port = entry_data[CONF_PORT] self._entry_id = entry_id - self._number_of_inverters = entry_data.get( - ConfName.NUMBER_INVERTERS, ConfDefaultInt.NUMBER_INVERTERS - ) - self._start_device_id = entry_data.get( - ConfName.DEVICE_ID, ConfDefaultInt.DEVICE_ID + self._inverter_list = entry_data.get( + ConfName.DEVICE_LIST, [ConfDefaultStr.DEVICE_LIST] ) self._detect_meters = entry_options.get( ConfName.DETECT_METERS, bool(ConfDefaultFlag.DETECT_METERS) @@ -175,9 +171,6 @@ def __init__( self._mb_reconnect_delay_max = self._yaml_config.get("modbus", {}).get( "reconnect_delay_max", ModbusDefaults.ReconnectDelayMax ) - self._mb_retry_on_empty = self._yaml_config.get("modbus", {}).get( - "retry_on_empty", bool(ModbusFlags.RetryOnEmpty) - ) self._mb_timeout = self._yaml_config.get("modbus", {}).get( "timeout", ModbusDefaults.Timeout ) @@ -199,8 +192,7 @@ def __init__( _LOGGER.debug( ( f"{DOMAIN} configuration: " - f"number_of_inverters={self._number_of_inverters}, " - f"start_device_id={self._start_device_id}, " + f"inverter_list={self._inverter_list}, " f"detect_meters={self._detect_meters}, " f"detect_batteries={self._detect_batteries}, " f"detect_extras={self._detect_extras}, " @@ -213,6 +205,8 @@ def __init__( ), ) + _LOGGER.debug(f"pymodbus version {pymodbus_version}") + async def _async_init_solaredge(self) -> None: """Detect devices and load initial modbus data from inverters.""" @@ -246,8 +240,7 @@ async def _async_init_solaredge(self) -> None: ), ) - for inverter_index in range(self._number_of_inverters): - inverter_unit_id = inverter_index + self._start_device_id + for inverter_unit_id in self._inverter_list: try: _LOGGER.debug( @@ -392,6 +385,9 @@ async def async_refresh_modbus_data(self) -> bool: ir.async_delete_issue(self._hass, DOMAIN, "check_configuration") + if not self.keep_modbus_open: + self.disconnect() + return True if not self.is_connected: @@ -454,26 +450,25 @@ async def async_refresh_modbus_data(self) -> bool: raise DataUpdateFailed(f"Timeout error: {e}") - if not self._keep_modbus_open: - self.disconnect() - if self._timeout_counter > 0: _LOGGER.debug( f"Timeout count {self._timeout_counter} limit {self._retry_limit}" ) self._timeout_counter = 0 + if not self.keep_modbus_open: + self.disconnect() + return True async def connect(self) -> None: """Connect to inverter.""" if self._client is None: - _LOGGER.debug(f"New client object for {self._host}:{self._port}") _LOGGER.debug( + "New AsyncModbusTcpClient: " f"reconnect_delay={self._mb_reconnect_delay} " f"reconnect_delay_max={self._mb_reconnect_delay_max} " - f"retry_on_empty={self._mb_retry_on_empty} " f"timeout={self._mb_timeout}" ) self._client = AsyncModbusTcpClient( @@ -481,16 +476,22 @@ async def connect(self) -> None: port=self._port, reconnect_delay=self._mb_reconnect_delay, reconnect_delay_max=self._mb_reconnect_delay_max, - retry_on_empty=self._mb_retry_on_empty, timeout=self._mb_timeout, ) + _LOGGER.debug((f"Connecting to {self._host}:{self._port} ...")) await self._client.connect() def disconnect(self, clear_client: bool = False) -> None: """Disconnect from inverter.""" if self._client is not None: + _LOGGER.debug( + ( + f"Disconnectng from {self._host}:{self._port} " + f"(clear_client={clear_client})." + ) + ) self._client.close() if clear_client: @@ -510,26 +511,26 @@ async def modbus_read_holding_registers(self, unit, address, rcount): self._rr_address = address self._rr_count = rcount - kwargs = {"slave": self._rr_unit} if self._rr_unit else {} - result = await self._client.read_holding_registers( - self._rr_address, self._rr_count, **kwargs + self._rr_address, count=self._rr_count, slave=self._rr_unit ) if result.isError(): - _LOGGER.debug(f"Unit {unit}: {result}") if type(result) is ModbusIOException: raise ModbusIOError(result) if type(result) is ExceptionResponse: if result.exception_code == ModbusExceptions.IllegalAddress: + _LOGGER.debug(f"Unit {unit} Read IllegalAddress: {result}") raise ModbusIllegalAddress(result) if result.exception_code == ModbusExceptions.IllegalFunction: + _LOGGER.debug(f"Unit {unit} Read IllegalFunction: {result}") raise ModbusIllegalFunction(result) if result.exception_code == ModbusExceptions.IllegalValue: + _LOGGER.debug(f"Unit {unit} Read IllegalValue: {result}") raise ModbusIllegalValue(result) raise ModbusReadError(result) @@ -561,9 +562,8 @@ async def write_registers(self, unit: int, address: int, payload) -> None: if not self.is_connected: await self.connect() - kwargs = {"slave": self._wr_unit} if self._wr_unit else {} result = await self._client.write_registers( - self._wr_address, self._wr_payload, **kwargs + self._wr_address, slave=self._wr_unit, values=self._wr_payload ) self.has_write = address @@ -604,19 +604,25 @@ async def write_registers(self, unit: int, address: int, payload) -> None: if type(result) is ExceptionResponse: if result.exception_code == ModbusExceptions.IllegalAddress: - _LOGGER.debug(f"Write IllegalAddress: {result}") + _LOGGER.debug( + f"Unit {self._wr_unit} Write IllegalAddress: {result}" + ) raise HomeAssistantError( "Address not supported at device at ID {self._wr_unit}." ) if result.exception_code == ModbusExceptions.IllegalFunction: - _LOGGER.debug(f"Write IllegalFunction: {result}") + _LOGGER.debug( + f"Unit {self._wr_unit} Write IllegalFunction: {result}" + ) raise HomeAssistantError( "Function not supported by device at ID {self._wr_unit}." ) if result.exception_code == ModbusExceptions.IllegalValue: - _LOGGER.debug(f"Write IllegalValue: {result}") + _LOGGER.debug( + f"Unit {self._wr_unit} Write IllegalValue: {result}" + ) raise HomeAssistantError( "Value invalid for device at ID {self._wr_unit}." ) @@ -713,7 +719,7 @@ def number_of_batteries(self) -> int: @property def number_of_inverters(self) -> int: - return self._number_of_inverters + return len(self._inverter_list) @property def sleep_after_write(self) -> int: @@ -1330,7 +1336,14 @@ async def read_modbus_data(self) -> None: ) self._grid_status = True - except ModbusIllegalAddress: + except (ModbusIllegalAddress, ModbusIOException) as e: + + if ( + type(e) is ModbusIOException + and "No response recieved after" not in e + ): + raise + try: del self.decoded_model["I_Grid_Status"] except KeyError: @@ -1339,9 +1352,12 @@ async def read_modbus_data(self) -> None: self._grid_status = False _LOGGER.debug( - (f"I{self.inverter_unit_id}: " "Grid On/Off NOT available") + (f"I{self.inverter_unit_id}: Grid On/Off NOT available: {e}") ) + if not self.hub.is_connected: + await self.hub.connect() + except ModbusIOError: raise ModbusReadError( f"No response from inverter ID {self.inverter_unit_id}" @@ -1584,7 +1600,10 @@ async def init_device(self) -> None: self.fw_version = self.decoded_common["C_Version"] self.serial = self.decoded_common["C_SerialNumber"] self.device_address = self.decoded_common["C_Device_address"] - self.name = f"{self.hub.hub_id.capitalize()} M{self.meter_id}" + self.name = ( + f"{self.hub.hub_id.capitalize()} " + f"I{self.inverter_unit_id} M{self.meter_id}" + ) inverter_model = self.inverter_common["C_Model"] inerter_serial = self.inverter_common["C_SerialNumber"] @@ -1837,7 +1856,10 @@ async def init_device(self) -> None: self.fw_version = self.decoded_common["B_Version"] self.serial = self.decoded_common["B_SerialNumber"] self.device_address = self.decoded_common["B_Device_Address"] - self.name = f"{self.hub.hub_id.capitalize()} B{self.battery_id}" + self.name = ( + f"{self.hub.hub_id.capitalize()} " + f"I{self.inverter_unit_id} B{self.battery_id}" + ) inverter_model = self.inverter_common["C_Model"] inerter_serial = self.inverter_common["C_SerialNumber"] diff --git a/custom_components/solaredge_modbus_multi/manifest.json b/custom_components/solaredge_modbus_multi/manifest.json index 2f6fbea3..971a81cd 100644 --- a/custom_components/solaredge_modbus_multi/manifest.json +++ b/custom_components/solaredge_modbus_multi/manifest.json @@ -9,6 +9,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/WillCodeForCats/solaredge-modbus-multi/issues", "loggers": ["custom_components.solaredge_modbus_multi"], - "requirements": ["pymodbus>=3.6.6"], - "version": "2.4.18" + "requirements": ["pymodbus>=3.6.6,<3.8"], + "version": "3.0.4" } diff --git a/custom_components/solaredge_modbus_multi/repairs.py b/custom_components/solaredge_modbus_multi/repairs.py index 0d91350f..0518a016 100644 --- a/custom_components/solaredge_modbus_multi/repairs.py +++ b/custom_components/solaredge_modbus_multi/repairs.py @@ -2,18 +2,19 @@ from __future__ import annotations +import re from typing import cast -import homeassistant.helpers.config_validation as cv -import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.repairs import RepairsFlow from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError -from .const import ConfName -from .helpers import host_valid +from .config_flow import generate_config_schema +from .const import ConfDefaultStr, ConfName +from .helpers import device_list_from_string, host_valid class CheckConfigurationRepairFlow(RepairsFlow): @@ -41,56 +42,52 @@ async def async_step_confirm( 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: - self.hass.config_entries.async_update_entry( - self._entry, data={**self._entry.data, **user_input} + try: + inverter_count = len( + device_list_from_string(user_input[ConfName.DEVICE_LIST]) ) - return self.async_create_entry(title="", data={}) + 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] + ) + + self.hass.config_entries.async_update_entry( + self._entry, data={**self._entry.data, **user_input} + ) + + return self.async_create_entry(title="", data={}) + else: + reconfig_device_list = ",".join( + str(device) + for device in self._entry.data.get( + ConfName.DEVICE_LIST, ConfDefaultStr.DEVICE_LIST + ) + ) + user_input = { CONF_HOST: self._entry.data[CONF_HOST], CONF_PORT: self._entry.data[CONF_PORT], - ConfName.NUMBER_INVERTERS: self._entry.data[ConfName.NUMBER_INVERTERS], - ConfName.DEVICE_ID: self._entry.data[ConfName.DEVICE_ID], + ConfName.DEVICE_LIST: reconfig_device_list, } return self.async_show_form( step_id="confirm", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=user_input[CONF_HOST]): cv.string, - vol.Required(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce( - 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), - } - ), + data_schema=generate_config_schema("confirm", user_input), errors=errors, ) diff --git a/custom_components/solaredge_modbus_multi/sensor.py b/custom_components/solaredge_modbus_multi/sensor.py index 41fc5f64..46b4e878 100644 --- a/custom_components/solaredge_modbus_multi/sensor.py +++ b/custom_components/solaredge_modbus_multi/sensor.py @@ -11,13 +11,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, - POWER_VOLT_AMPERE_REACTIVE, UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency, UnitOfPower, + UnitOfReactivePower, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback @@ -662,9 +662,9 @@ def unique_id(self) -> str: @property def name(self) -> str: if self._phase is None: - return "AC VA" + return "AC Apparent Power" else: - return f"AC VA {self._phase.upper()}" + return f"AC Apparent Power {self._phase.upper()}" @property def entity_registry_enabled_default(self) -> bool: @@ -702,7 +702,7 @@ def suggested_display_precision(self): class ACVoltAmpReactive(SolarEdgeSensorBase): device_class = SensorDeviceClass.REACTIVE_POWER state_class = SensorStateClass.MEASUREMENT - native_unit_of_measurement = POWER_VOLT_AMPERE_REACTIVE + native_unit_of_measurement = UnitOfReactivePower.VOLT_AMPERE_REACTIVE def __init__(self, platform, config_entry, coordinator, phase: str = None): super().__init__(platform, config_entry, coordinator) @@ -719,9 +719,9 @@ def unique_id(self) -> str: @property def name(self) -> str: if self._phase is None: - return "AC var" + return "AC Reactive Power" else: - return f"AC var {self._phase.upper()}" + return f"AC Reactive Power {self._phase.upper()}" @property def entity_registry_enabled_default(self) -> bool: @@ -776,9 +776,9 @@ def unique_id(self) -> str: @property def name(self) -> str: if self._phase is None: - return "AC PF" + return "AC Power Factor" else: - return f"AC PF {self._phase.upper()}" + return f"AC Power Factor {self._phase.upper()}" @property def entity_registry_enabled_default(self) -> bool: @@ -887,7 +887,7 @@ def name(self) -> str: if self._phase is None: return "AC Energy" else: - return f"{re.sub('_', ' ', self._phase)}" + return f"AC Energy {re.sub('_', ' ', self._phase)}" @property def available(self) -> bool: @@ -1652,7 +1652,7 @@ def name(self) -> str: if self._phase is None: raise NotImplementedError else: - return f"{re.sub('_', ' ', self._phase)} VAh" + return f"Apparent Energy {re.sub('_', ' ', self._phase)}" @property def native_value(self): @@ -1730,7 +1730,7 @@ def name(self) -> str: if self._phase is None: raise NotImplementedError else: - return f"{re.sub('_', ' ', self._phase)} varh" + return f"Reactive Energy {re.sub('_', ' ', self._phase)}" @property def native_value(self): @@ -2256,14 +2256,20 @@ def native_value(self): float_to_hex(self._platform.decoded_model["B_Energy_Available"]) == hex(SunSpecNotImpl.FLOAT32) or self._platform.decoded_model["B_Energy_Available"] < 0 - or self._platform.decoded_model["B_Energy_Available"] - > ( - self._platform.decoded_common["B_RatedEnergy"] - * self._platform.battery_rating_adjust - ) ): return None + if self._platform.decoded_model["B_Energy_Available"] > ( + self._platform.decoded_common["B_RatedEnergy"] + * self._platform.battery_rating_adjust + ): + _LOGGER.warning( + f"I{self._platform.inverter_unit_id}B{self._platform.battery_id}: " + "Battery available energy exceeds rated energy. " + "Set configuration for Battery Rating Adjustment when necessary." + ) + return None + else: return self._platform.decoded_model["B_Energy_Available"] diff --git a/custom_components/solaredge_modbus_multi/strings.json b/custom_components/solaredge_modbus_multi/strings.json index af7221d5..1c737acb 100644 --- a/custom_components/solaredge_modbus_multi/strings.json +++ b/custom_components/solaredge_modbus_multi/strings.json @@ -7,8 +7,7 @@ "name": "Sensor Prefix", "host": "Inverter IP Address", "port": "Modbus/TCP Port", - "device_id": "Inverter Modbus Address (Device ID)", - "number_of_inverters": "Number of Inverters" + "device_list": "Inverter Device ID List" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "Inverter IP Address", "port": "Modbus/TCP Port", - "device_id": "Inverter Modbus Address (Device ID)", - "number_of_inverters": "Number of Inverters" + "device_list": "Inverter Device ID List" } } }, "error": { "already_configured": "Device is already configured!", - "max_device_id": "Device ID must be between 1 to 247.", - "min_device_id": "Device ID must be between 1 to 247.", - "max_inverters": "Must be between 1 to 32 inverters.", - "min_inverters": "Must be between 1 to 32 inverters.", - "too_many_inverters": "Number of inverters too high for Device ID.", + "invalid_device_id": "Device ID must be between 1 to 247.", + "invalid_inverter_count": "Must be between 1 to 32 inverters.", "invalid_host": "Invalid IP address.", - "invalid_tcp_port": "Valid port range is 1 to 65535." + "invalid_tcp_port": "Valid port range is 1 to 65535.", + "invalid_range_format": "Entry looks like a range but only one '-' per range is allowed.", + "invalid_range_lte": "Starting ID in a range must be less than or equal to the end ID.", + "empty_device_id": "The ID list contains an empty or undefined value." }, "abort": { "already_configured": "Device is already configured", @@ -84,19 +82,18 @@ "data": { "host": "Inverter IP Address", "port": "Modbus/TCP Port", - "device_id": "Inverter Modbus Address (Device ID)", - "number_of_inverters": "Number of Inverters" + "device_list": "Inverter Device ID List" } } }, "error": { - "max_device_id": "Device ID must be between 1 to 247.", - "min_device_id": "Device ID must be between 1 to 247.", - "max_inverters": "Must be between 1 to 32 inverters.", - "min_inverters": "Must be between 1 to 32 inverters.", - "too_many_inverters": "Number of inverters too high for Device ID.", + "invalid_device_id": "Device ID must be between 1 to 247.", + "invalid_inverter_count": "Must be between 1 to 32 inverters.", "invalid_host": "Invalid IP address.", - "invalid_tcp_port": "Valid port range is 1 to 65535." + "invalid_tcp_port": "Valid port range is 1 to 65535.", + "invalid_range_format": "Entry looks like a range but only one '-' per range is allowed.", + "invalid_range_lte": "Starting ID in a range must be less than or equal to the end ID.", + "empty_device_id": "The ID list contains an empty or undefined value." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/de.json b/custom_components/solaredge_modbus_multi/translations/de.json index b3dd3fc9..7a8adeda 100644 --- a/custom_components/solaredge_modbus_multi/translations/de.json +++ b/custom_components/solaredge_modbus_multi/translations/de.json @@ -7,8 +7,7 @@ "name": "Sensorpräfix", "host": "Wechselrichter-IP-Adresse", "port": "Modbus/TCP-Port", - "device_id": "Wechselrichter-Modbus-Adresse (Geräte-ID)", - "number_of_inverters": "Anzahl Wechselrichter" + "device_list": "Liste der Wechselrichter-Geräte-IDs" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "Wechselrichter-IP-Adresse", "port": "Modbus/TCP-Port", - "device_id": "Wechselrichter-Modbus-Adresse (Geräte-ID)", - "number_of_inverters": "Anzahl Wechselrichter" + "device_list": "Liste der Wechselrichter-Geräte-IDs" } } }, "error": { "already_configured": "Der Wechselrichter ist bereits konfiguriert.", - "max_device_id": "Die Geräte-ID muss zwischen 1 und 247 liegen.", - "min_device_id": "Die Geräte-ID muss zwischen 1 und 247 liegen.", - "max_inverters": "Muss zwischen 1 und 32 Wechselrichtern liegen.", - "min_inverters": "Muss zwischen 1 und 32 Wechselrichtern liegen.", - "too_many_inverters": "Anzahl Wechselrichter zu hoch für Geräte-ID.", + "invalid_device_id": "Die Geräte-ID muss zwischen 1 und 247 liegen.", + "invalid_inverter_count": "Muss zwischen 1 und 32 Wechselrichtern liegen.", "invalid_host": "Ungültige IP-Adresse.", - "invalid_tcp_port": "Der gültige Portbereich ist 1 bis 65535." + "invalid_tcp_port": "Der gültige Portbereich ist 1 bis 65535.", + "invalid_range_format": "Der Eintrag sieht aus wie ein Bereich, es ist jedoch nur ein „-“ pro Bereich zulässig.", + "invalid_range_lte": "Die Start-ID in einem Bereich muss kleiner oder gleich der End-ID sein.", + "empty_device_id": "Die ID-Liste enthält einen leeren oder undefinierten Wert." }, "abort": { "already_configured": "Gerät ist bereits konfiguriert", @@ -90,13 +88,13 @@ } }, "error": { - "max_device_id": "Die Geräte-ID muss zwischen 1 und 247 liegen.", - "min_device_id": "Die Geräte-ID muss zwischen 1 und 247 liegen.", - "max_inverters": "Muss zwischen 1 und 32 Wechselrichtern liegen.", - "min_inverters": "Muss zwischen 1 und 32 Wechselrichtern liegen.", - "too_many_inverters": "Anzahl Wechselrichter zu hoch für Geräte-ID.", + "invalid_device_id": "Die Geräte-ID muss zwischen 1 und 247 liegen.", + "invalid_inverter_count": "Muss zwischen 1 und 32 Wechselrichtern liegen.", "invalid_host": "Ungültige IP-Adresse.", - "invalid_tcp_port": "Der gültige Portbereich ist 1 bis 65535." + "invalid_tcp_port": "Der gültige Portbereich ist 1 bis 65535.", + "invalid_range_format": "Der Eintrag sieht aus wie ein Bereich, es ist jedoch nur ein „-“ pro Bereich zulässig.", + "invalid_range_lte": "Die Start-ID in einem Bereich muss kleiner oder gleich der End-ID sein.", + "empty_device_id": "Die ID-Liste enthält einen leeren oder undefinierten Wert." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/en.json b/custom_components/solaredge_modbus_multi/translations/en.json index af7221d5..1c737acb 100644 --- a/custom_components/solaredge_modbus_multi/translations/en.json +++ b/custom_components/solaredge_modbus_multi/translations/en.json @@ -7,8 +7,7 @@ "name": "Sensor Prefix", "host": "Inverter IP Address", "port": "Modbus/TCP Port", - "device_id": "Inverter Modbus Address (Device ID)", - "number_of_inverters": "Number of Inverters" + "device_list": "Inverter Device ID List" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "Inverter IP Address", "port": "Modbus/TCP Port", - "device_id": "Inverter Modbus Address (Device ID)", - "number_of_inverters": "Number of Inverters" + "device_list": "Inverter Device ID List" } } }, "error": { "already_configured": "Device is already configured!", - "max_device_id": "Device ID must be between 1 to 247.", - "min_device_id": "Device ID must be between 1 to 247.", - "max_inverters": "Must be between 1 to 32 inverters.", - "min_inverters": "Must be between 1 to 32 inverters.", - "too_many_inverters": "Number of inverters too high for Device ID.", + "invalid_device_id": "Device ID must be between 1 to 247.", + "invalid_inverter_count": "Must be between 1 to 32 inverters.", "invalid_host": "Invalid IP address.", - "invalid_tcp_port": "Valid port range is 1 to 65535." + "invalid_tcp_port": "Valid port range is 1 to 65535.", + "invalid_range_format": "Entry looks like a range but only one '-' per range is allowed.", + "invalid_range_lte": "Starting ID in a range must be less than or equal to the end ID.", + "empty_device_id": "The ID list contains an empty or undefined value." }, "abort": { "already_configured": "Device is already configured", @@ -84,19 +82,18 @@ "data": { "host": "Inverter IP Address", "port": "Modbus/TCP Port", - "device_id": "Inverter Modbus Address (Device ID)", - "number_of_inverters": "Number of Inverters" + "device_list": "Inverter Device ID List" } } }, "error": { - "max_device_id": "Device ID must be between 1 to 247.", - "min_device_id": "Device ID must be between 1 to 247.", - "max_inverters": "Must be between 1 to 32 inverters.", - "min_inverters": "Must be between 1 to 32 inverters.", - "too_many_inverters": "Number of inverters too high for Device ID.", + "invalid_device_id": "Device ID must be between 1 to 247.", + "invalid_inverter_count": "Must be between 1 to 32 inverters.", "invalid_host": "Invalid IP address.", - "invalid_tcp_port": "Valid port range is 1 to 65535." + "invalid_tcp_port": "Valid port range is 1 to 65535.", + "invalid_range_format": "Entry looks like a range but only one '-' per range is allowed.", + "invalid_range_lte": "Starting ID in a range must be less than or equal to the end ID.", + "empty_device_id": "The ID list contains an empty or undefined value." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/fr.json b/custom_components/solaredge_modbus_multi/translations/fr.json index af1c0d93..d7cd2d6e 100644 --- a/custom_components/solaredge_modbus_multi/translations/fr.json +++ b/custom_components/solaredge_modbus_multi/translations/fr.json @@ -7,8 +7,7 @@ "name": "Prefix du capteur", "host": "Adresse IP de l'onduleur", "port": "Port Modbus/TCP", - "device_id": "L'adresse Modbus de l'onduleur (Device ID)", - "number_of_inverters": "Nombre d'onduleurs" + "device_list": "Liste des ID des appareils de l'onduleur" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "Adresse IP de l'onduleur", "port": "Port Modbus/TCP", - "device_id": "L'adresse Modbus de l'onduleur (Device ID)", - "number_of_inverters": "Nombre d'onduleurs" + "device_list": "Liste des ID des appareils de l'onduleur" } } }, "error": { "already_configured": "L'appareil est déjà configuré!", - "max_device_id": "L'adresse Modbus doit être entre 1 et 247.", - "min_device_id": "L'adresse Modbus doit être entre 1 et 247.", - "max_inverters": "Doit être entre 1 et 32 onduleurs.", - "min_inverters": "Doit être entre 1 et 32 onduleurs.", - "too_many_inverters": "Nombre d'onduleurs trop important pour l'adresse Modbus.", + "invalid_device_id": "L'adresse Modbus doit être entre 1 et 247.", + "invalid_inverter_count": "Doit être entre 1 et 32 onduleurs.", "invalid_host": "Adresse IP invalide.", - "invalid_tcp_port": "La plage de ports valide est comprise entre 1 et 65535." + "invalid_tcp_port": "La plage de ports valide est comprise entre 1 et 65535.", + "invalid_range_format": "L'entrée ressemble à une plage mais un seul « - » par plage est autorisé.", + "invalid_range_lte": "L’ID de début d’une plage doit être inférieur ou égal à l’ID de fin.", + "empty_device_id": "La liste d'ID contient une valeur vide ou non définie." }, "abort": { "already_configured": "L'appareil est déjà configuré", @@ -90,13 +88,13 @@ } }, "error": { - "max_device_id": "L'adresse Modbus doit être entre 1 et 247.", - "min_device_id": "L'adresse Modbus doit être entre 1 et 247.", - "max_inverters": "Doit être entre 1 et 32 onduleurs.", - "min_inverters": "Doit être entre 1 et 32 onduleurs.", - "too_many_inverters": "Nombre d'onduleurs trop important pour l'adresse Modbus.", + "invalid_device_id": "L'adresse Modbus doit être entre 1 et 247.", + "invalid_inverter_count": "Doit être entre 1 et 32 onduleurs.", "invalid_host": "Adresse IP invalide.", - "invalid_tcp_port": "La plage de ports valide est comprise entre 1 et 65535." + "invalid_tcp_port": "La plage de ports valide est comprise entre 1 et 65535.", + "invalid_range_format": "L'entrée ressemble à une plage mais un seul « - » par plage est autorisé.", + "invalid_range_lte": "L’ID de début d’une plage doit être inférieur ou égal à l’ID de fin.", + "empty_device_id": "La liste d'ID contient une valeur vide ou non définie." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/it.json b/custom_components/solaredge_modbus_multi/translations/it.json index 956b3ade..444cc055 100644 --- a/custom_components/solaredge_modbus_multi/translations/it.json +++ b/custom_components/solaredge_modbus_multi/translations/it.json @@ -7,8 +7,7 @@ "name": "Prefisso sensore", "host": "Indirizzo IP dell'inverter", "port": "Porta Modbus/TCP", - "device_id": "Indirizzo Modbus dell'inverter (ID dispositivo)", - "number_of_inverters": "Numero di inverter" + "device_list": "Elenco ID dispositivi inverter" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "Indirizzo IP dell'inverter", "port": "Porta Modbus/TCP", - "device_id": "Indirizzo Modbus dell'inverter (ID dispositivo)", - "number_of_inverters": "Numero di inverter" + "device_list": "Elenco ID dispositivi inverter" } } }, "error": { "already_configured": "Il dispositivo è già configurato!", - "max_device_id": "L'ID del dispositivo deve essere compreso tra 1 e 247.", - "min_device_id": "L'ID del dispositivo deve essere compreso tra 1 e 247.", - "max_inverters": "Deve essere compreso tra 1 e 32 inverter.", - "min_inverters": "Deve essere compreso tra 1 e 32 inverter.", - "too_many_inverters": "Numero di inverter troppo elevato per l'ID dispositivo.", + "invalid_device_id": "L'ID del dispositivo deve essere compreso tra 1 e 247.", + "invalid_inverter_count": "Deve essere compreso tra 1 e 32 inverter.", "invalid_host": "Indirizzo IP non valido.", - "invalid_tcp_port": "L'intervallo di porte valido è compreso tra 1 e 65535." + "invalid_tcp_port": "L'intervallo di porte valido è compreso tra 1 e 65535.", + "invalid_range_format": "L'immissione sembra un intervallo ma è consentito solo un '-' per intervallo.", + "invalid_range_lte": "L'ID iniziale in un intervallo deve essere inferiore o uguale all'ID finale.", + "empty_device_id": "L'elenco ID contiene un valore vuoto o non definito." }, "abort": { "already_configured": "Il dispositivo è già configurato", @@ -90,13 +88,13 @@ } }, "error": { - "max_device_id": "L'ID del dispositivo deve essere compreso tra 1 e 247.", - "min_device_id": "L'ID del dispositivo deve essere compreso tra 1 e 247.", - "max_inverters": "Deve essere compreso tra 1 e 32 inverter.", - "min_inverters": "Deve essere compreso tra 1 e 32 inverter.", - "too_many_inverters": "Numero di inverter troppo elevato per l'ID dispositivo.", + "invalid_device_id": "L'ID del dispositivo deve essere compreso tra 1 e 247.", + "invalid_inverter_count": "Deve essere compreso tra 1 e 32 inverter.", "invalid_host": "Indirizzo IP non valido.", - "invalid_tcp_port": "L'intervallo di porte valido è compreso tra 1 e 65535." + "invalid_tcp_port": "L'intervallo di porte valido è compreso tra 1 e 65535.", + "invalid_range_format": "L'immissione sembra un intervallo ma è consentito solo un '-' per intervallo.", + "invalid_range_lte": "L'ID iniziale in un intervallo deve essere inferiore o uguale all'ID finale.", + "empty_device_id": "L'elenco ID contiene un valore vuoto o non definito." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/nb.json b/custom_components/solaredge_modbus_multi/translations/nb.json index a81a3be6..a39f81c9 100644 --- a/custom_components/solaredge_modbus_multi/translations/nb.json +++ b/custom_components/solaredge_modbus_multi/translations/nb.json @@ -7,8 +7,7 @@ "name": "Sensorvoorvoegsel", "host": "IP-adres van omvormer", "port": "Modbus/TCP-poort", - "device_id": "Inverter Modbus-adresse (enhets-ID)", - "number_of_inverters": "Antall omformere koblet sammen" + "device_list": "Inverter-enhetsliste" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "IP-adres van omvormer", "port": "Modbus/TCP-poort", - "device_id": "Inverter Modbus-adresse (enhets-ID)", - "number_of_inverters": "Antall omformere koblet sammen" + "device_list": "Inverter-enhetsliste" } } }, "error": { "already_configured": "Enheten er allerede konfigurert", - "max_device_id": "Enhets-ID må være mellom 1 og 247.", - "min_device_id": "Enhets-ID må være mellom 1 og 247.", - "max_inverters": "Må være mellom 1 og 32 omformere.", - "min_inverters": "Må være mellom 1 og 32 omformere.", - "too_many_inverters": "Antall invertere for høyt for enhets-ID.", + "invalid_device_id": "Enhets-ID må være mellom 1 og 247.", + "invalid_inverter_count": "Må være mellom 1 og 32 omformere.", "invalid_host": "Ugyldig IP-adresse.", - "invalid_tcp_port": "Gyldig portområde er 1 til 65535." + "invalid_tcp_port": "Gyldig portområde er 1 til 65535.", + "invalid_range_format": "Oppføring ser ut som et område, men bare én '-' per område er tillatt.", + "invalid_range_lte": "Start-ID i et område må være mindre enn eller lik slutt-ID.", + "empty_device_id": "ID-listen inneholder en tom eller udefinert verdi." }, "abort": { "already_configured": "Enheten er allerede konfigurert", @@ -90,13 +88,13 @@ } }, "error": { - "max_device_id": "Enhets-ID må være mellom 1 og 247.", - "min_device_id": "Enhets-ID må være mellom 1 og 247.", - "max_inverters": "Må være mellom 1 og 32 omformere.", - "min_inverters": "Må være mellom 1 og 32 omformere.", - "too_many_inverters": "Antall invertere for høyt for enhets-ID.", + "invalid_device_id": "Enhets-ID må være mellom 1 og 247.", + "invalid_inverter_count": "Må være mellom 1 og 32 omformere.", "invalid_host": "Ugyldig IP-adresse.", - "invalid_tcp_port": "Gyldig portområde er 1 til 65535." + "invalid_tcp_port": "Gyldig portområde er 1 til 65535.", + "invalid_range_format": "Oppføring ser ut som et område, men bare én '-' per område er tillatt.", + "invalid_range_lte": "Start-ID i et område må være mindre enn eller lik slutt-ID.", + "empty_device_id": "ID-listen inneholder en tom eller udefinert verdi." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/nl.json b/custom_components/solaredge_modbus_multi/translations/nl.json index 07f30d13..21bbf9d2 100644 --- a/custom_components/solaredge_modbus_multi/translations/nl.json +++ b/custom_components/solaredge_modbus_multi/translations/nl.json @@ -7,8 +7,7 @@ "name": "Sensor prefix", "host": "omvormer IP-adres", "port": "Modbus/TCP Port", - "device_id": "Omvormer Modbus-adres (apparaat-ID)", - "number_of_inverters": "Aantal aangesloten omvormers" + "device_list": "Omvormerapparaat-ID-lijst" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "omvormer IP-adres", "port": "Modbus/TCP Port", - "device_id": "Omvormer Modbus-adres (apparaat-ID)", - "number_of_inverters": "Aantal aangesloten omvormers" + "device_list": "Omvormerapparaat-ID-lijst" } } }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "max_device_id": "Apparaat-ID moet tussen 1 en 247 liggen.", - "min_device_id": "Apparaat-ID moet tussen 1 en 247 liggen.", - "max_inverters": "Moet tussen 1 en 32 omvormers zijn.", - "min_inverters": "Moet tussen 1 en 32 omvormers zijn.", - "too_many_inverters": "Aantal omvormers te hoog voor Device ID.", + "invalid_device_id": "Apparaat-ID moet tussen 1 en 247 liggen.", + "invalid_inverter_count": "Moet tussen 1 en 32 omvormers zijn.", "invalid_host": "Ongeldig IP-adres.", - "invalid_tcp_port": "Geldig poortbereik is 1 tot 65535." + "invalid_tcp_port": "Geldig poortbereik is 1 tot 65535.", + "invalid_range_format": "Invoer ziet eruit als een bereik, maar er is slechts één '-' per bereik toegestaan.", + "invalid_range_lte": "De start-ID in een bereik moet kleiner zijn dan of gelijk zijn aan de eind-ID.", + "empty_device_id": "De ID-lijst bevat een lege of ongedefinieerde waarde." }, "abort": { "already_configured": "Apparaat is al geconfigureerd", @@ -90,13 +88,13 @@ } }, "error": { - "max_device_id": "Apparaat-ID moet tussen 1 en 247 liggen.", - "min_device_id": "Apparaat-ID moet tussen 1 en 247 liggen.", - "max_inverters": "Moet tussen 1 en 32 omvormers zijn.", - "min_inverters": "Moet tussen 1 en 32 omvormers zijn.", - "too_many_inverters": "Aantal omvormers te hoog voor Device ID.", + "invalid_device_id": "Apparaat-ID moet tussen 1 en 247 liggen.", + "invalid_inverter_count": "Moet tussen 1 en 32 omvormers zijn.", "invalid_host": "Ongeldig IP-adres.", - "invalid_tcp_port": "Geldig poortbereik is 1 tot 65535." + "invalid_tcp_port": "Geldig poortbereik is 1 tot 65535.", + "invalid_range_format": "Invoer ziet eruit als een bereik, maar er is slechts één '-' per bereik toegestaan.", + "invalid_range_lte": "De start-ID in een bereik moet kleiner zijn dan of gelijk zijn aan de eind-ID.", + "empty_device_id": "De ID-lijst bevat een lege of ongedefinieerde waarde." } } } diff --git a/custom_components/solaredge_modbus_multi/translations/pl.json b/custom_components/solaredge_modbus_multi/translations/pl.json index 2647a938..8a36c8bd 100644 --- a/custom_components/solaredge_modbus_multi/translations/pl.json +++ b/custom_components/solaredge_modbus_multi/translations/pl.json @@ -7,8 +7,7 @@ "name": "Prefix sensora", "host": "Adres IP inwertera", "port": "Modbus/TCP Port", - "device_id": "Adres Modbus Inwertera (Device ID)", - "number_of_inverters": "Ilość inwerterów" + "device_list": "Lista identyfikatorów urządzeń falownika" } }, "reconfigure": { @@ -16,20 +15,19 @@ "data": { "host": "Adres IP inwertera", "port": "Modbus/TCP Port", - "device_id": "Adres Modbus Inwertera (Device ID)", - "number_of_inverters": "Ilość inwerterów" + "device_list": "Lista identyfikatorów urządzeń falownika" } } }, "error": { "already_configured": "Urządzenie jest już skonfigurowane!", - "max_device_id": "Device ID musi być pomiędzy 1 i 247.", - "min_device_id": "Device ID musi być pomiędzy 1 i 247.", - "max_inverters": "Dopuszczalna liczba inwerterów to od 1 do 32.", - "min_inverters": "Dopuszczalna liczba inwerterów to od 1 do 32.", - "too_many_inverters": "Liczba inwerterów za duża dla Device ID.", + "invalid_device_id": "Device ID musi być pomiędzy 1 i 247.", + "invalid_inverter_count": "Dopuszczalna liczba inwerterów to od 1 do 32.", "invalid_host": "Błędny adres IP.", - "invalid_tcp_port": "Dozwolony zakres portów to od 1 do 65535." + "invalid_tcp_port": "Dozwolony zakres portów to od 1 do 65535.", + "invalid_range_format": "Wpis wygląda jak zakres, ale dozwolony jest tylko jeden znak „-” na zakres.", + "invalid_range_lte": "Początkowy identyfikator w zakresie musi być mniejszy lub równy identyfikatorowi końcowemu.", + "empty_device_id": "Lista identyfikatorów zawiera pustą lub niezdefiniowaną wartość." }, "abort": { "already_configured": "Urządzenie jest już skonfigurowane", @@ -90,13 +88,13 @@ } }, "error": { - "max_device_id": "Device ID musi być pomiędzy 1 i 247.", - "min_device_id": "Device ID musi być pomiędzy 1 i 247.", - "max_inverters": "Dopuszczalna liczba inwerterów to od 1 do 32.", - "min_inverters": "Dopuszczalna liczba inwerterów to od 1 do 32.", - "too_many_inverters": "Liczba inwerterów za duża dla Device ID.", + "invalid_device_id": "Device ID musi być pomiędzy 1 i 247.", + "invalid_inverter_count": "Dopuszczalna liczba inwerterów to od 1 do 32.", "invalid_host": "Błędny adres IP.", - "invalid_tcp_port": "Dozwolony zakres portów to od 1 do 65535." + "invalid_tcp_port": "Dozwolony zakres portów to od 1 do 65535.", + "invalid_range_format": "Wpis wygląda jak zakres, ale dozwolony jest tylko jeden znak „-” na zakres.", + "invalid_range_lte": "Początkowy identyfikator w zakresie musi być mniejszy lub równy identyfikatorowi końcowemu.", + "empty_device_id": "Lista identyfikatorów zawiera pustą lub niezdefiniowaną wartość." } } } diff --git a/hacs.json b/hacs.json index 9c206f7b..ba94699f 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,5 @@ { "name": "SolarEdge Modbus Multi", "content_in_root": false, - "homeassistant": "2024.4.0", - "render_readme": false + "homeassistant": "2024.9.0" } diff --git a/info.md b/info.md deleted file mode 100644 index edd7e6be..00000000 --- a/info.md +++ /dev/null @@ -1,25 +0,0 @@ -## SolarEdge Modbus Multi - -Integrates SolarEdge inverters with Modbus/TCP local polling. Single inverters, multiple inverters, meters, and batteries are supported. - -Many improvements over other integrations that didn't work well with a multi-device setup. - -Simple single inverter setups are fully supported - multiple devices is a feature, not a requirement. - -Read more on the wiki: [WillCodeForCats/solaredge-modbus-multi/wiki](https://github.com/WillCodeForCats/solaredge-modbus-multi/wiki) - -## Features -* Inverter support for 1 to 32 SolarEdge inverters. -* Meter support for 1 to 3 meters per inverter. -* Battery support for 1 to 3 batteries per inverter. -* Supports site limit and storage controls. -* Automatically detects meters and batteries. -* Supports Three Phase Inverters with Synergy Technology. -* Polling frequency configuration option (1 to 86400 seconds). -* Configurable starting inverter device ID. -* Connects locally using Modbus/TCP - no cloud dependencies. -* Informational sensor for device and its attributes -* Supports status and error reporting sensors. -* User friendly configuration through Config Flow. - -Requires Home Assistant 2024.4.0 and newer with pymodbus 3.6.6 and newer.