Add select entities to SolarEdge Modbus (#180698)

This commit is contained in:
Franck Nijhof
2026-08-30 17:10:14 +02:00
committed by GitHub
parent bbe9477fe6
commit ea0417753c
6 changed files with 833 additions and 1 deletions
@@ -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(
@@ -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"
@@ -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)
@@ -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",
@@ -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({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'total',
'per_phase',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.solaredge_se10000h_export_limit_type',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SolarEdge SE10000H Export limit type',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'total',
'per_phase',
]),
}),
'context': <ANY>,
'entity_id': 'select.solaredge_se10000h_export_limit_type',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'total',
})
# ---
# name: test_selects[select.solaredge_se10000h_export_limitation-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'export_control_export_import_meter',
'export_control_consumption_meter',
'production_control',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.solaredge_se10000h_export_limitation',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SolarEdge SE10000H Export limitation',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'export_control_export_import_meter',
'export_control_consumption_meter',
'production_control',
]),
}),
'context': <ANY>,
'entity_id': 'select.solaredge_se10000h_export_limitation',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'disabled',
})
# ---
# name: test_selects[select.solaredge_se10000h_storage_ac_charge_policy-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'always',
'fixed_energy_limit',
'percent_of_production',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.solaredge_se10000h_storage_ac_charge_policy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SolarEdge SE10000H Storage AC charge policy',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'always',
'fixed_energy_limit',
'percent_of_production',
]),
}),
'context': <ANY>,
'entity_id': 'select.solaredge_se10000h_storage_ac_charge_policy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'disabled',
})
# ---
# name: test_selects[select.solaredge_se10000h_storage_command_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.solaredge_se10000h_storage_command_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SolarEdge SE10000H Storage command mode',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'select.solaredge_se10000h_storage_command_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_selects[select.solaredge_se10000h_storage_control_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'maximize_self_consumption',
'time_of_use',
'backup_only',
'remote_control',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.solaredge_se10000h_storage_control_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SolarEdge SE10000H Storage control mode',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'maximize_self_consumption',
'time_of_use',
'backup_only',
'remote_control',
]),
}),
'context': <ANY>,
'entity_id': 'select.solaredge_se10000h_storage_control_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'maximize_self_consumption',
})
# ---
# name: test_selects[select.solaredge_se10000h_storage_default_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.solaredge_se10000h_storage_default_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SolarEdge SE10000H Storage default mode',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'select.solaredge_se10000h_storage_default_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'solar_only',
})
# ---
@@ -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",
]