From 7044771876a12d3d0f4a23b67664a3805f59ba2a Mon Sep 17 00:00:00 2001 From: Dan Raper Date: Sun, 26 Jan 2025 12:52:01 +0000 Subject: [PATCH] Add select platform to Ohme (#136536) * Add select platform * Formatting * Add parallel updates to select * Remove comments --- homeassistant/components/ohme/const.py | 1 + homeassistant/components/ohme/icons.json | 5 ++ homeassistant/components/ohme/select.py | 70 ++++++++++++++++++ homeassistant/components/ohme/strings.json | 10 +++ .../ohme/snapshots/test_select.ambr | 58 +++++++++++++++ tests/components/ohme/test_select.py | 72 +++++++++++++++++++ 6 files changed, 216 insertions(+) create mode 100644 homeassistant/components/ohme/select.py create mode 100644 tests/components/ohme/snapshots/test_select.ambr create mode 100644 tests/components/ohme/test_select.py diff --git a/homeassistant/components/ohme/const.py b/homeassistant/components/ohme/const.py index 308664ba0ad..d97f6e3cfd7 100644 --- a/homeassistant/components/ohme/const.py +++ b/homeassistant/components/ohme/const.py @@ -6,6 +6,7 @@ DOMAIN = "ohme" PLATFORMS = [ Platform.BUTTON, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.TIME, diff --git a/homeassistant/components/ohme/icons.json b/homeassistant/components/ohme/icons.json index a6b04004833..7a27156b2fe 100644 --- a/homeassistant/components/ohme/icons.json +++ b/homeassistant/components/ohme/icons.json @@ -10,6 +10,11 @@ "default": "mdi:battery-heart" } }, + "select": { + "charge_mode": { + "default": "mdi:play-box" + } + }, "sensor": { "status": { "default": "mdi:car", diff --git a/homeassistant/components/ohme/select.py b/homeassistant/components/ohme/select.py new file mode 100644 index 00000000000..a357e98f0a6 --- /dev/null +++ b/homeassistant/components/ohme/select.py @@ -0,0 +1,70 @@ +"""Platform for Ohme selects.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Final + +from ohme import ApiException, ChargerMode, OhmeApiClient + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import OhmeConfigEntry +from .const import DOMAIN +from .entity import OhmeEntity, OhmeEntityDescription + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class OhmeSelectDescription(OhmeEntityDescription, SelectEntityDescription): + """Class to describe an Ohme select entity.""" + + select_fn: Callable[[OhmeApiClient, Any], Awaitable[None]] + current_option_fn: Callable[[OhmeApiClient], str | None] + + +SELECT_DESCRIPTION: Final[OhmeSelectDescription] = OhmeSelectDescription( + key="charge_mode", + translation_key="charge_mode", + select_fn=lambda client, mode: client.async_set_mode(mode), + options=[e.value for e in ChargerMode], + current_option_fn=lambda client: client.mode.value if client.mode else None, + available_fn=lambda client: client.mode is not None, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: OhmeConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Ohme selects.""" + coordinator = config_entry.runtime_data.charge_session_coordinator + + async_add_entities([OhmeSelect(coordinator, SELECT_DESCRIPTION)]) + + +class OhmeSelect(OhmeEntity, SelectEntity): + """Ohme select entity.""" + + entity_description: OhmeSelectDescription + + async def async_select_option(self, option: str) -> None: + """Handle the selection of an option.""" + try: + await self.entity_description.select_fn(self.coordinator.client, option) + except ApiException as e: + raise HomeAssistantError( + translation_key="api_failed", translation_domain=DOMAIN + ) from e + await self.coordinator.async_request_refresh() + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + return self.entity_description.current_option_fn(self.coordinator.client) diff --git a/homeassistant/components/ohme/strings.json b/homeassistant/components/ohme/strings.json index 84f62ba65ab..eb5bbffda52 100644 --- a/homeassistant/components/ohme/strings.json +++ b/homeassistant/components/ohme/strings.json @@ -55,6 +55,16 @@ "name": "Target percentage" } }, + "select": { + "charge_mode": { + "name": "Charge mode", + "state": { + "smart_charge": "Smart charge", + "max_charge": "Max charge", + "paused": "Paused" + } + } + }, "sensor": { "status": { "name": "Status", diff --git a/tests/components/ohme/snapshots/test_select.ambr b/tests/components/ohme/snapshots/test_select.ambr new file mode 100644 index 00000000000..04770397098 --- /dev/null +++ b/tests/components/ohme/snapshots/test_select.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_selects[select.ohme_home_pro_charge_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'smart_charge', + 'max_charge', + 'paused', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.ohme_home_pro_charge_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge mode', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_mode', + 'unique_id': 'chargerid_charge_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.ohme_home_pro_charge_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Charge mode', + 'options': list([ + 'smart_charge', + 'max_charge', + 'paused', + ]), + }), + 'context': , + 'entity_id': 'select.ohme_home_pro_charge_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/ohme/test_select.py b/tests/components/ohme/test_select.py new file mode 100644 index 00000000000..5aeebc1f477 --- /dev/null +++ b/tests/components/ohme/test_select.py @@ -0,0 +1,72 @@ +"""Tests for selects.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from ohme import ChargerMode +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_selects( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the Ohme selects.""" + with patch("homeassistant.components.ohme.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_select_option( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test selecting an option in the Ohme select entity.""" + mock_client.mode = ChargerMode.SMART_CHARGE + mock_client.async_set_mode = AsyncMock() + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.ohme_home_pro_charge_mode") + assert state is not None + assert state.state == "smart_charge" + + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": "select.ohme_home_pro_charge_mode", + "option": "max_charge", + }, + blocking=True, + ) + + mock_client.async_set_mode.assert_called_once_with("max_charge") + assert state.state == "smart_charge" + + +async def test_select_unavailable( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test that the select entity shows as unavailable when no mode is set.""" + mock_client.mode = None + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.ohme_home_pro_charge_mode") + assert state is not None + assert state.state == STATE_UNAVAILABLE