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
@@ -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",
]