mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Add select entities to SolarEdge Modbus (#180698)
This commit is contained in:
@@ -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",
|
||||
]
|
||||
Reference in New Issue
Block a user