From a1e35b34f0acbda5c2d283321bc52d69ad599698 Mon Sep 17 00:00:00 2001 From: rrooggiieerr Date: Wed, 30 Sep 2026 17:14:07 +0200 Subject: [PATCH] Add select platform to my-PV integration (#182330) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/my_pv/__init__.py | 1 + homeassistant/components/my_pv/select.py | 63 ++++++ homeassistant/components/my_pv/strings.json | 12 ++ tests/components/my_pv/conftest.py | 6 + .../my_pv/snapshots/test_select.ambr | 62 ++++++ tests/components/my_pv/test_select.py | 183 ++++++++++++++++++ 6 files changed, 327 insertions(+) create mode 100644 homeassistant/components/my_pv/select.py create mode 100644 tests/components/my_pv/snapshots/test_select.ambr create mode 100644 tests/components/my_pv/test_select.py diff --git a/homeassistant/components/my_pv/__init__.py b/homeassistant/components/my_pv/__init__.py index 068149abe5b0..f4e894125ac5 100644 --- a/homeassistant/components/my_pv/__init__.py +++ b/homeassistant/components/my_pv/__init__.py @@ -13,6 +13,7 @@ from .coordinator import MyPVConfigEntry, MyPVCoordinator PLATFORMS: list[Platform] = [ Platform.BUTTON, Platform.NUMBER, + Platform.SELECT, Platform.WATER_HEATER, ] diff --git a/homeassistant/components/my_pv/select.py b/homeassistant/components/my_pv/select.py new file mode 100644 index 000000000000..3240b624379a --- /dev/null +++ b/homeassistant/components/my_pv/select.py @@ -0,0 +1,63 @@ +"""Creates Select entities for the my-PV Home Assistant integration.""" + +from typing import override + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import MyPVConfigEntry +from .const import DOMAIN +from .entity import MyPVSetupEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: MyPVConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the my-PV select.""" + coordinator = config_entry.runtime_data + entities = [] + + config = coordinator.device.get_setup_configuration("bstmode") + if config and config.get("type") == "enumeration": + entity_description = SelectEntityDescription( + key="bstmode", + entity_category=EntityCategory.CONFIG, + translation_key="bstmode", + entity_registry_enabled_default=False, + options=list(config.get("options", {}).keys()), + ) + entities.append( + MyPVSelect( + coordinator, + entity_description, + coordinator.device.serial_number, + ) + ) + + async_add_entities(entities) + + +class MyPVSelect(MyPVSetupEntity, SelectEntity): + """my-PV select.""" + + @property + @override + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + value = self.coordinator.device.get_setup_value(self.entity_description.key) + return str(value) if value is not None else None + + @override + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + if not await self.coordinator.set_setup_value( + self.entity_description.key, option + ): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="unknown_error" + ) diff --git a/homeassistant/components/my_pv/strings.json b/homeassistant/components/my_pv/strings.json index 34d35b9afca6..fea6145262d2 100644 --- a/homeassistant/components/my_pv/strings.json +++ b/homeassistant/components/my_pv/strings.json @@ -68,6 +68,18 @@ "ww_targ_h": { "name": "Maximum Hysteresis" } + }, + "select": { + "bstmode": { + "name": "Boost mode", + "state": { + "0": "[%key:common::state::off%]", + "1": "[%key:common::state::on%]", + "3": "Relay", + "4": "SELV relay", + "5": "AUX relay" + } + } } }, "exceptions": { diff --git a/tests/components/my_pv/conftest.py b/tests/components/my_pv/conftest.py index bd6ae51ed1e9..064b2e4d7ada 100644 --- a/tests/components/my_pv/conftest.py +++ b/tests/components/my_pv/conftest.py @@ -13,6 +13,10 @@ from . import ELWA2_SERIAL_NUMBER from tests.common import MockConfigEntry SETUP_CONFIGURATION = { + "bstmode": { + "type": "enumeration", + "options": {"0": "Off", "1": "On", "3": "Relais"}, + }, "bsttemp": {"type": "number", "step": 0.1, "unit": "°C", "min": 5.0, "max": 95.0}, "ww1boost": {"type": "number", "step": 0.1, "unit": "°C", "min": 5.0, "max": 95.0}, "ww_boost_h": {"type": "number", "step": 0.1, "unit": "°C", "min": 0.1, "max": 9.9}, @@ -23,6 +27,7 @@ SETUP_CONFIGURATION = { COMMAND_CONFIGURATION = {"reboot_device": {"type": "any"}} SETUP_VALUE = { + "bstmode": "0", "bsttemp": 55.0, "ww1boost": 65.0, "ww_boost_h": 3.5, @@ -97,6 +102,7 @@ def mock_my_pv_client() -> Generator[AsyncMock]: client.get_setup_configurations = Mock(return_value=SETUP_CONFIGURATION) client.get_setup_configuration = Mock(side_effect=_setup_configuration_lookup) client.get_setup_value = Mock(side_effect=_setup_value_lookup) + client.set_setup_value = AsyncMock() client.get_command_configuration = Mock( side_effect=_command_configuration_lookup ) diff --git a/tests/components/my_pv/snapshots/test_select.ambr b/tests/components/my_pv/snapshots/test_select.ambr new file mode 100644 index 000000000000..8f0a2269ea3a --- /dev/null +++ b/tests/components/my_pv/snapshots/test_select.ambr @@ -0,0 +1,62 @@ +# serializer version: 1 +# name: test_select[select.my_pv_ac_elwa_2_boost_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + '0', + '1', + '3', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_pv_ac_elwa_2_boost_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Boost mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boost mode', + 'platform': 'my_pv', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bstmode', + 'unique_id': '1601500000000000-bstmode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[select.my_pv_ac_elwa_2_boost_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'my-PV AC ELWA 2 Boost mode', + : list([ + '0', + '1', + '3', + ]), + }), + 'context': , + 'entity_id': 'select.my_pv_ac_elwa_2_boost_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/my_pv/test_select.py b/tests/components/my_pv/test_select.py new file mode 100644 index 000000000000..d4a9bd162280 --- /dev/null +++ b/tests/components/my_pv/test_select.py @@ -0,0 +1,183 @@ +"""Test the my-PV select platform.""" + +from unittest.mock import AsyncMock, Mock, patch + +from my_pv.exceptions import MyPVAuthenticationError, MyPVConnectionError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("mock_my_pv_client") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test successful setup of the select platform.""" + + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SELECT]): + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_unavailable_not_connected( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, +) -> None: + """Test if a select is unavailable when not connected.""" + + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SELECT]): + mock_config_entry.add_to_hass(hass) + + mock_my_pv_client.connected = False + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("select.my_pv_ac_elwa_2_boost_mode") + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_unavailable_setup_value_none( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, +) -> None: + """Test if a select is unavailable when setup value is None.""" + + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SELECT]): + mock_config_entry.add_to_hass(hass) + + mock_my_pv_client.get_setup_value = Mock(return_value=None) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("select.my_pv_ac_elwa_2_boost_mode") + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_select_option( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, +) -> None: + """Test setting value.""" + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SELECT]): + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("select.my_pv_ac_elwa_2_boost_mode") + assert state.state == "0" + + mock_my_pv_client.get_setup_value = Mock(return_value="1") + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_pv_ac_elwa_2_boost_mode", + ATTR_OPTION: "1", + }, + blocking=True, + ) + mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", "1") + + state = hass.states.get("select.my_pv_ac_elwa_2_boost_mode") + assert state.state == "1" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_select_option_returns_false( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, +) -> None: + """Test for HomeAssistantError when set_setup_value returns false.""" + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SELECT]): + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_my_pv_client.set_setup_value = AsyncMock(return_value=False) + + with ( + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_pv_ac_elwa_2_boost_mode", + ATTR_OPTION: "1", + }, + blocking=True, + ) + mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", "1") + + state = hass.states.get("select.my_pv_ac_elwa_2_boost_mode") + assert state.state == "0" + + +@pytest.mark.parametrize( + ("error", "expected_ha_error"), + [ + (MyPVConnectionError(), HomeAssistantError), + (MyPVAuthenticationError(), ConfigEntryAuthFailed), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_select_option_raises_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, + error: MyPVConnectionError | MyPVAuthenticationError, + expected_ha_error: type[HomeAssistantError], +) -> None: + """Test for HomeAssistantError when set_setup_value raises error.""" + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SELECT]): + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_my_pv_client.set_setup_value = AsyncMock(side_effect=error) + + with ( + pytest.raises(expected_ha_error), + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.my_pv_ac_elwa_2_boost_mode", ATTR_OPTION: "1"}, + blocking=True, + ) + mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", "1") + + state = hass.states.get("select.my_pv_ac_elwa_2_boost_mode") + assert state.state == "0"