From ea0417753ce9e42c520aa5ae5200aef2ae46bfea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Aug 2026 17:10:14 +0200 Subject: [PATCH] Add select entities to SolarEdge Modbus (#180698) --- .../components/solaredge_modbus/__init__.py | 7 +- .../components/solaredge_modbus/icons.json | 20 + .../components/solaredge_modbus/select.py | 215 ++++++++++ .../components/solaredge_modbus/strings.json | 61 +++ .../snapshots/test_select.ambr | 389 ++++++++++++++++++ .../solaredge_modbus/test_select.py | 142 +++++++ 6 files changed, 833 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/solaredge_modbus/select.py create mode 100644 tests/components/solaredge_modbus/snapshots/test_select.ambr create mode 100644 tests/components/solaredge_modbus/test_select.py diff --git a/homeassistant/components/solaredge_modbus/__init__.py b/homeassistant/components/solaredge_modbus/__init__.py index 48155d6a5595..1b5d5e3bfe73 100644 --- a/homeassistant/components/solaredge_modbus/__init__.py +++ b/homeassistant/components/solaredge_modbus/__init__.py @@ -40,7 +40,12 @@ from .coordinator import ( from .entity import attachment_identity, inverter_device_info from .helpers import create_modbus_params -PLATFORMS = [Platform.BINARY_SENSOR, Platform.NUMBER, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, +] async def async_setup_entry( diff --git a/homeassistant/components/solaredge_modbus/icons.json b/homeassistant/components/solaredge_modbus/icons.json index 33959b00a866..f9a6ceec5827 100644 --- a/homeassistant/components/solaredge_modbus/icons.json +++ b/homeassistant/components/solaredge_modbus/icons.json @@ -31,6 +31,26 @@ "default": "mdi:transmission-tower-export" } }, + "select": { + "export_control_limit_type": { + "default": "mdi:scale-balance" + }, + "export_control_mode": { + "default": "mdi:transmission-tower-export" + }, + "storage_ac_charge_policy": { + "default": "mdi:battery-charging" + }, + "storage_command_mode": { + "default": "mdi:battery-sync-outline" + }, + "storage_control_mode": { + "default": "mdi:battery-sync" + }, + "storage_default_mode": { + "default": "mdi:battery-sync-outline" + } + }, "sensor": { "battery_status": { "default": "mdi:home-battery" diff --git a/homeassistant/components/solaredge_modbus/select.py b/homeassistant/components/solaredge_modbus/select.py new file mode 100644 index 000000000000..bb8971d21bee --- /dev/null +++ b/homeassistant/components/solaredge_modbus/select.py @@ -0,0 +1,215 @@ +"""Support for SolarEdge Modbus select entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, override + +from solaredged import ( + ExportControl, + ExportControlLimit, + ExportControlMode, + SolarEdge, + StorageChargePolicy, + StorageControl, + StorageControlMode, + StorageMode, +) + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import SolarEdgeModbusConfigEntry +from .entity import ControlComponent, SolarEdgeModbusControlEntity +from .helpers import solaredge_exception_handler + +PARALLEL_UPDATES = 1 + +# Export limiting has no dedicated "off" enum member; the library models a +# disabled limiter as mode None, exposed here as an explicit option. +EXPORT_MODE_DISABLED = "disabled" + + +@dataclass(frozen=True, kw_only=True) +class SolarEdgeModbusSelectEntityDescription[ComponentT](SelectEntityDescription): + """Describes a SolarEdge Modbus select entity.""" + + current_fn: Callable[[ComponentT], str | None] + # Options that depend on the detected layout, like meter presence. + options_fn: Callable[[SolarEdge], list[str]] | None = None + select_fn: Callable[[ComponentT, str], Awaitable[Any]] + + +STORAGE_SELECTS: tuple[SolarEdgeModbusSelectEntityDescription[StorageControl], ...] = ( + SolarEdgeModbusSelectEntityDescription( + key="storage_control_mode", + translation_key="storage_control_mode", + entity_category=EntityCategory.CONFIG, + options=[mode.name.lower() for mode in StorageControlMode], + current_fn=lambda storage: ( + storage.control_mode.name.lower() + if storage.control_mode is not None + else None + ), + select_fn=lambda storage, option: storage.set_control_mode( + StorageControlMode[option.upper()] + ), + ), + SolarEdgeModbusSelectEntityDescription( + key="storage_ac_charge_policy", + translation_key="storage_ac_charge_policy", + entity_category=EntityCategory.CONFIG, + options=[policy.name.lower() for policy in StorageChargePolicy], + current_fn=lambda storage: ( + storage.ac_charge_policy.name.lower() + if storage.ac_charge_policy is not None + else None + ), + select_fn=lambda storage, option: storage.set_ac_charge_policy( + StorageChargePolicy[option.upper()] + ), + ), + SolarEdgeModbusSelectEntityDescription( + key="storage_default_mode", + translation_key="storage_default_mode", + entity_category=EntityCategory.CONFIG, + options=[mode.name.lower() for mode in StorageMode], + current_fn=lambda storage: ( + storage.default_mode.name.lower() + if storage.default_mode is not None + else None + ), + select_fn=lambda storage, option: storage.set_default_mode( + StorageMode[option.upper()] + ), + ), + SolarEdgeModbusSelectEntityDescription( + key="storage_command_mode", + translation_key="storage_command_mode", + entity_category=EntityCategory.CONFIG, + options=[mode.name.lower() for mode in StorageMode], + current_fn=lambda storage: ( + storage.command_mode.name.lower() + if storage.command_mode is not None + else None + ), + select_fn=lambda storage, option: storage.set_command_mode( + StorageMode[option.upper()] + ), + ), +) + +EXPORT_SELECTS: tuple[SolarEdgeModbusSelectEntityDescription[ExportControl], ...] = ( + SolarEdgeModbusSelectEntityDescription( + key="export_control_mode", + translation_key="export_control_mode", + entity_category=EntityCategory.CONFIG, + # Limiting export by a meter reading needs a meter to read. + options_fn=lambda solaredge: [ + EXPORT_MODE_DISABLED, + *( + mode.name.lower() + for mode in ExportControlMode + if solaredge.meters or mode is ExportControlMode.PRODUCTION_CONTROL + ), + ], + current_fn=lambda export: ( + export.mode.name.lower() + if export.mode is not None + else EXPORT_MODE_DISABLED + ), + select_fn=lambda export, option: export.set_mode( + None + if option == EXPORT_MODE_DISABLED + else ExportControlMode[option.upper()] + ), + ), + SolarEdgeModbusSelectEntityDescription( + key="export_control_limit_type", + translation_key="export_control_limit_type", + entity_category=EntityCategory.CONFIG, + # Whether the site limit counts per phase or in total is part of the + # export-control setup the installer does, not day-to-day operation. + entity_registry_enabled_default=False, + options=[limit.name.lower() for limit in ExportControlLimit], + current_fn=lambda export: ( + export.limit_type.name.lower() if export.limit_type is not None else None + ), + select_fn=lambda export, option: export.write( + "limit_type", ExportControlLimit[option.upper()] + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SolarEdgeModbusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up SolarEdge Modbus select entities based on a config entry.""" + solaredge = entry.runtime_data.solaredge + + entities: list[SelectEntity] = [] + # The storage control block answers on inverters without storage too; the + # settings only mean something when a battery is actually attached. + if (storage := solaredge.storage_control) is not None and solaredge.batteries: + entities.extend( + SolarEdgeModbusSelectEntity( + entry=entry, description=description, component=storage + ) + for description in STORAGE_SELECTS + ) + if (export := solaredge.export_control) is not None: + entities.extend( + SolarEdgeModbusSelectEntity( + entry=entry, description=description, component=export + ) + for description in EXPORT_SELECTS + ) + + async_add_entities(entities) + + +class SolarEdgeModbusSelectEntity[ComponentT: ControlComponent]( + SolarEdgeModbusControlEntity[ComponentT], SelectEntity +): + """Defines a SolarEdge Modbus select entity.""" + + entity_description: SolarEdgeModbusSelectEntityDescription[ComponentT] + + @property + @override + def options(self) -> list[str]: + """Return the options this site can use, plus the one it is set to. + + A mode that needs hardware the site does not have is left out. The + exception is whatever the inverter is set to right now, which can be + anything an installer or the SolarEdge app left behind: the register is + the truth, and an entity may not report a state outside its options. + """ + description = self.entity_description + options = ( + description.options_fn(self.coordinator.solaredge) + if description.options_fn is not None + else super().options + ) + + current = self.current_option + if current is not None and current not in options: + return [*options, current] + + return options + + @property + @override + def current_option(self) -> str | None: + """Return the selected option.""" + return self.entity_description.current_fn(self._component) + + @solaredge_exception_handler + @override + async def async_select_option(self, option: str) -> None: + """Select an option.""" + await self.entity_description.select_fn(self._component, option) diff --git a/homeassistant/components/solaredge_modbus/strings.json b/homeassistant/components/solaredge_modbus/strings.json index 2c1ee90fcec1..c1d1e14712c5 100644 --- a/homeassistant/components/solaredge_modbus/strings.json +++ b/homeassistant/components/solaredge_modbus/strings.json @@ -132,6 +132,67 @@ "name": "Site export limit" } }, + "select": { + "export_control_limit_type": { + "name": "Export limit type", + "state": { + "per_phase": "Per phase", + "total": "Total" + } + }, + "export_control_mode": { + "name": "Export limitation", + "state": { + "disabled": "[%key:common::state::disabled%]", + "export_control_consumption_meter": "Consumption meter", + "export_control_export_import_meter": "Export and import meter", + "production_control": "Production control" + } + }, + "storage_ac_charge_policy": { + "name": "Storage AC charge policy", + "state": { + "always": "Always", + "disabled": "[%key:common::state::disabled%]", + "fixed_energy_limit": "Fixed energy limit", + "percent_of_production": "Percentage of production" + } + }, + "storage_command_mode": { + "name": "Storage command mode", + "state": { + "charge_from_clipped_solar": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::charge_from_clipped_solar%]", + "charge_from_solar": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::charge_from_solar%]", + "charge_from_solar_and_grid": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::charge_from_solar_and_grid%]", + "discharge_to_maximize_export": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::discharge_to_maximize_export%]", + "discharge_to_minimize_import": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::discharge_to_minimize_import%]", + "maximize_self_consumption": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::maximize_self_consumption%]", + "solar_only": "[%key:component::solaredge_modbus::entity::select::storage_default_mode::state::solar_only%]" + } + }, + "storage_control_mode": { + "name": "Storage control mode", + "state": { + "backup_only": "Backup only", + "disabled": "[%key:common::state::disabled%]", + "maximize_self_consumption": "Maximize self-consumption", + "remote_control": "Remote control", + "time_of_use": "Time of use" + } + }, + "storage_default_mode": { + "name": "Storage default mode", + "state": { + "charge_from_clipped_solar": "Charge from clipped solar", + "charge_from_solar": "Charge from solar", + "charge_from_solar_and_grid": "Charge from solar and grid", + "discharge_to_maximize_export": "Discharge to maximize export", + "discharge_to_minimize_import": "Discharge to minimize import", + "maximize_self_consumption": "Maximize self-consumption", + "solar_only": "Solar only" + } + } + }, "sensor": { "battery_status": { "name": "Status", diff --git a/tests/components/solaredge_modbus/snapshots/test_select.ambr b/tests/components/solaredge_modbus/snapshots/test_select.ambr new file mode 100644 index 000000000000..551d380175dd --- /dev/null +++ b/tests/components/solaredge_modbus/snapshots/test_select.ambr @@ -0,0 +1,389 @@ +# serializer version: 1 +# name: test_selects[select.solaredge_se10000h_export_limit_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'total', + 'per_phase', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.solaredge_se10000h_export_limit_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Export limit type', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Export limit type', + 'platform': 'solaredge_modbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'export_control_limit_type', + 'unique_id': '7E123ABC_export_control_limit_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.solaredge_se10000h_export_limit_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'SolarEdge SE10000H Export limit type', + : list([ + 'total', + 'per_phase', + ]), + }), + 'context': , + 'entity_id': 'select.solaredge_se10000h_export_limit_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'total', + }) +# --- +# name: test_selects[select.solaredge_se10000h_export_limitation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'disabled', + 'export_control_export_import_meter', + 'export_control_consumption_meter', + 'production_control', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.solaredge_se10000h_export_limitation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Export limitation', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Export limitation', + 'platform': 'solaredge_modbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'export_control_mode', + 'unique_id': '7E123ABC_export_control_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.solaredge_se10000h_export_limitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'SolarEdge SE10000H Export limitation', + : list([ + 'disabled', + 'export_control_export_import_meter', + 'export_control_consumption_meter', + 'production_control', + ]), + }), + 'context': , + 'entity_id': 'select.solaredge_se10000h_export_limitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disabled', + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_ac_charge_policy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'disabled', + 'always', + 'fixed_energy_limit', + 'percent_of_production', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.solaredge_se10000h_storage_ac_charge_policy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage AC charge policy', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Storage AC charge policy', + 'platform': 'solaredge_modbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ac_charge_policy', + 'unique_id': '7E123ABC_storage_ac_charge_policy', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_ac_charge_policy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'SolarEdge SE10000H Storage AC charge policy', + : list([ + 'disabled', + 'always', + 'fixed_energy_limit', + 'percent_of_production', + ]), + }), + 'context': , + 'entity_id': 'select.solaredge_se10000h_storage_ac_charge_policy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disabled', + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_command_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'solar_only', + 'charge_from_clipped_solar', + 'charge_from_solar', + 'charge_from_solar_and_grid', + 'discharge_to_maximize_export', + 'discharge_to_minimize_import', + 'maximize_self_consumption', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.solaredge_se10000h_storage_command_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage command mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Storage command mode', + 'platform': 'solaredge_modbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_command_mode', + 'unique_id': '7E123ABC_storage_command_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_command_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'SolarEdge SE10000H Storage command mode', + : list([ + 'solar_only', + 'charge_from_clipped_solar', + 'charge_from_solar', + 'charge_from_solar_and_grid', + 'discharge_to_maximize_export', + 'discharge_to_minimize_import', + 'maximize_self_consumption', + ]), + }), + 'context': , + 'entity_id': 'select.solaredge_se10000h_storage_command_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_control_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'disabled', + 'maximize_self_consumption', + 'time_of_use', + 'backup_only', + 'remote_control', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.solaredge_se10000h_storage_control_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage control mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Storage control mode', + 'platform': 'solaredge_modbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_control_mode', + 'unique_id': '7E123ABC_storage_control_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_control_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'SolarEdge SE10000H Storage control mode', + : list([ + 'disabled', + 'maximize_self_consumption', + 'time_of_use', + 'backup_only', + 'remote_control', + ]), + }), + 'context': , + 'entity_id': 'select.solaredge_se10000h_storage_control_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'maximize_self_consumption', + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_default_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'solar_only', + 'charge_from_clipped_solar', + 'charge_from_solar', + 'charge_from_solar_and_grid', + 'discharge_to_maximize_export', + 'discharge_to_minimize_import', + 'maximize_self_consumption', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.solaredge_se10000h_storage_default_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage default mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Storage default mode', + 'platform': 'solaredge_modbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_default_mode', + 'unique_id': '7E123ABC_storage_default_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.solaredge_se10000h_storage_default_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'SolarEdge SE10000H Storage default mode', + : list([ + 'solar_only', + 'charge_from_clipped_solar', + 'charge_from_solar', + 'charge_from_solar_and_grid', + 'discharge_to_maximize_export', + 'discharge_to_minimize_import', + 'maximize_self_consumption', + ]), + }), + 'context': , + 'entity_id': 'select.solaredge_se10000h_storage_default_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'solar_only', + }) +# --- diff --git a/tests/components/solaredge_modbus/test_select.py b/tests/components/solaredge_modbus/test_select.py new file mode 100644 index 000000000000..d6380be420e0 --- /dev/null +++ b/tests/components/solaredge_modbus/test_select.py @@ -0,0 +1,142 @@ +"""Tests for the SolarEdge Modbus select entities.""" + +from unittest.mock import patch + +from modbus_connection.mock import MockModbusUnit +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +CONTROL_MODE_ENTITY = "select.solaredge_se10000h_storage_control_mode" +CONTROL_MODE_REGISTER = 57348 +EXPORT_MODE_ENTITY = "select.solaredge_se10000h_export_limitation" + + +async def _setup_select_platform(hass: HomeAssistant, entry: MockConfigEntry) -> None: + with patch( + "homeassistant.components.solaredge_modbus.PLATFORMS", [Platform.SELECT] + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_selects( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """All select entities and their states match the snapshot.""" + await _setup_select_platform(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_limit_type_disabled_by_default( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """How the site limit is counted is part of the installer's setup.""" + await _setup_select_platform(hass, mock_config_entry) + + entity_id = "select.solaredge_se10000h_export_limit_type" + + assert hass.states.get(entity_id) is None + entry = entity_registry.async_get(entity_id) + assert entry is not None + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +async def test_select_option( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_modbus_unit: MockModbusUnit, +) -> None: + """Selecting an option writes the mode to the device and updates the state.""" + await _setup_select_platform(hass, mock_config_entry) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: CONTROL_MODE_ENTITY, ATTR_OPTION: "time_of_use"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(CONTROL_MODE_ENTITY) + assert state is not None + assert state.state == "time_of_use" + assert mock_modbus_unit.holding[CONTROL_MODE_REGISTER] == 2 + + +async def test_export_mode_keeps_the_mode_the_inverter_is_set_to( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_modbus_unit: MockModbusUnit, +) -> None: + """A mode the inverter is set to is offered even where it does not fit. + + The register is the authority on what the inverter is set to, whatever an + installer or the SolarEdge app left behind, and an entity may not report a + state outside its own options. + """ + # Remove the meter from the register image (no meter model = absent). + mock_modbus_unit.holding[40188] = 0 + # ...while the inverter is set to a meter-based mode: bit 1 of the mode. + mock_modbus_unit.holding[57344] = 0b10 + + await _setup_select_platform(hass, mock_config_entry) + + state = hass.states.get(EXPORT_MODE_ENTITY) + assert state is not None + assert state.state == "export_control_consumption_meter" + assert state.state in state.attributes[ATTR_OPTIONS] + + +async def test_export_mode_without_a_meter_hides_the_meter_modes( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_modbus_unit: MockModbusUnit, +) -> None: + """Limiting export by a meter reading needs a meter to read.""" + # Remove the meter from the register image (no meter model = absent). + mock_modbus_unit.holding[40188] = 0 + # ...with export limiting switched off, so no mode has to be kept. + mock_modbus_unit.holding[57344] = 0 + + await _setup_select_platform(hass, mock_config_entry) + + state = hass.states.get(EXPORT_MODE_ENTITY) + assert state is not None + assert state.state == "disabled" + assert state.attributes[ATTR_OPTIONS] == ["disabled", "production_control"] + + +async def test_export_mode_options_with_meter( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """A site with a meter offers the meter-based export limitation modes too.""" + await _setup_select_platform(hass, mock_config_entry) + + state = hass.states.get(EXPORT_MODE_ENTITY) + assert state is not None + assert state.attributes[ATTR_OPTIONS] == [ + "disabled", + "export_control_export_import_meter", + "export_control_consumption_meter", + "production_control", + ]