From 172778053c84f3af4e47091585923f0de1b3f7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 19 Jul 2024 11:29:58 +0200 Subject: [PATCH] Add select platform to myuplink (#118661) --- homeassistant/components/myuplink/__init__.py | 1 + homeassistant/components/myuplink/helpers.py | 3 + homeassistant/components/myuplink/select.py | 95 +++++++++++++++++++ .../fixtures/device_points_nibe_f730.json | 38 ++++++++ .../fixtures/device_points_nibe_smo20.json | 4 +- .../myuplink/snapshots/test_diagnostics.ambr | 76 +++++++++++++++ tests/components/myuplink/test_select.py | 89 +++++++++++++++++ 7 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/myuplink/select.py create mode 100644 tests/components/myuplink/test_select.py diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index a8307cf8c6c..d801f27817d 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -25,6 +25,7 @@ from .coordinator import MyUplinkDataCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, diff --git a/homeassistant/components/myuplink/helpers.py b/homeassistant/components/myuplink/helpers.py index abe039605d3..ac3d2a2d7fa 100644 --- a/homeassistant/components/myuplink/helpers.py +++ b/homeassistant/components/myuplink/helpers.py @@ -21,6 +21,9 @@ def find_matching_platform( return Platform.SWITCH return Platform.BINARY_SENSOR + if len(device_point.enum_values) > 0 and device_point.writable: + return Platform.SELECT + if ( description and description.native_unit_of_measurement == "DM" diff --git a/homeassistant/components/myuplink/select.py b/homeassistant/components/myuplink/select.py new file mode 100644 index 00000000000..c0fb66602de --- /dev/null +++ b/homeassistant/components/myuplink/select.py @@ -0,0 +1,95 @@ +"""Select entity for myUplink.""" + +from typing import cast + +from aiohttp import ClientError +from myuplink import DevicePoint + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import MyUplinkConfigEntry, MyUplinkDataCoordinator +from .entity import MyUplinkEntity +from .helpers import find_matching_platform, skip_entity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: MyUplinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up myUplink select.""" + entities: list[SelectEntity] = [] + coordinator: MyUplinkDataCoordinator = config_entry.runtime_data + + # Setup device point select entities + for device_id, point_data in coordinator.data.points.items(): + for point_id, device_point in point_data.items(): + if skip_entity(device_point.category, device_point): + continue + description = None + if find_matching_platform(device_point, description) == Platform.SELECT: + entities.append( + MyUplinkSelect( + coordinator=coordinator, + device_id=device_id, + device_point=device_point, + entity_description=description, + unique_id_suffix=point_id, + ) + ) + + async_add_entities(entities) + + +class MyUplinkSelect(MyUplinkEntity, SelectEntity): + """Representation of a myUplink select entity.""" + + def __init__( + self, + coordinator: MyUplinkDataCoordinator, + device_id: str, + device_point: DevicePoint, + entity_description: SelectEntityDescription | None, + unique_id_suffix: str, + ) -> None: + """Initialize the select.""" + super().__init__( + coordinator=coordinator, + device_id=device_id, + unique_id_suffix=unique_id_suffix, + ) + + # Internal properties + self.point_id = device_point.parameter_id + self._attr_name = device_point.parameter_name + + self._attr_options = [x["text"].capitalize() for x in device_point.enum_values] + self.options_map = { + str(int(x["value"])): x["text"].capitalize() + for x in device_point.enum_values + } + self.options_rev = {value: key for key, value in self.options_map.items()} + + @property + def current_option(self) -> str | None: + """Retrieve currently selected option.""" + device_point = self.coordinator.data.points[self.device_id][self.point_id] + value = int(cast(int, device_point.value_t)) + return self.options_map.get(str(value)) + + async def async_select_option(self, option: str) -> None: + """Set the current option.""" + try: + await self.coordinator.api.async_set_device_points( + self.device_id, data={self.point_id: str(self.options_rev[option])} + ) + except ClientError as err: + raise HomeAssistantError( + f"Failed to set new option {self.options_rev[option]} for {self.point_id}/{self.entity_id}" + ) from err + + await self.coordinator.async_request_refresh() diff --git a/tests/components/myuplink/fixtures/device_points_nibe_f730.json b/tests/components/myuplink/fixtures/device_points_nibe_f730.json index 49340bd9e26..9ec5db0ea3b 100644 --- a/tests/components/myuplink/fixtures/device_points_nibe_f730.json +++ b/tests/components/myuplink/fixtures/device_points_nibe_f730.json @@ -951,5 +951,43 @@ ], "scaleValue": "1", "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "47041", + "parameterName": "comfort mode", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-05-22T15:02:03+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "4", + "text": "smart control", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null } ] diff --git a/tests/components/myuplink/fixtures/device_points_nibe_smo20.json b/tests/components/myuplink/fixtures/device_points_nibe_smo20.json index b64869c236c..9135862d991 100644 --- a/tests/components/myuplink/fixtures/device_points_nibe_smo20.json +++ b/tests/components/myuplink/fixtures/device_points_nibe_smo20.json @@ -3996,9 +3996,9 @@ "parameterUnit": "", "writable": true, "timestamp": "2024-02-14T08:36:05+00:00", - "value": 0, + "value": 0.0, "strVal": "economy", - "smartHomeCategories": [], + "smartHomeCategories": ["test"], "minValue": null, "maxValue": null, "stepValue": 1, diff --git a/tests/components/myuplink/snapshots/test_diagnostics.ambr b/tests/components/myuplink/snapshots/test_diagnostics.ambr index 53664820364..9160fd3b365 100644 --- a/tests/components/myuplink/snapshots/test_diagnostics.ambr +++ b/tests/components/myuplink/snapshots/test_diagnostics.ambr @@ -1012,6 +1012,44 @@ ], "scaleValue": "1", "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "47041", + "parameterName": "comfort mode", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-05-22T15:02:03+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "4", + "text": "smart control", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null } ] @@ -2017,6 +2055,44 @@ ], "scaleValue": "1", "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "47041", + "parameterName": "comfort mode", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-05-22T15:02:03+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "4", + "text": "smart control", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null } ] diff --git a/tests/components/myuplink/test_select.py b/tests/components/myuplink/test_select.py new file mode 100644 index 00000000000..7ad2d17cb5d --- /dev/null +++ b/tests/components/myuplink/test_select.py @@ -0,0 +1,89 @@ +"""Tests for myuplink select module.""" + +from unittest.mock import MagicMock + +from aiohttp import ClientError +import pytest + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_OPTION, + SERVICE_SELECT_OPTION, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +TEST_PLATFORM = Platform.SELECT +pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) + +ENTITY_ID = "select.gotham_city_comfort_mode" +ENTITY_FRIENDLY_NAME = "Gotham City comfort mode" +ENTITY_UID = "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47041" + + +async def test_select_entity( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_myuplink_client: MagicMock, + setup_platform: None, +) -> None: + """Test that the entities are registered in the entity registry.""" + + entry = entity_registry.async_get(ENTITY_ID) + assert entry.unique_id == ENTITY_UID + + # Test the select attributes are correct. + + state = hass.states.get(ENTITY_ID) + assert state.state == "Economy" + assert state.attributes == { + "options": ["Smart control", "Economy", "Normal", "Luxury"], + "friendly_name": ENTITY_FRIENDLY_NAME, + } + + +async def test_selecting( + hass: HomeAssistant, + mock_myuplink_client: MagicMock, + setup_platform: None, +) -> None: + """Test select option service.""" + + await hass.services.async_call( + TEST_PLATFORM, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: "Economy"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_myuplink_client.async_set_device_points.assert_called_once() + + # Test handling of exception from API. + + mock_myuplink_client.async_set_device_points.side_effect = ClientError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + TEST_PLATFORM, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: "Economy"}, + blocking=True, + ) + assert mock_myuplink_client.async_set_device_points.call_count == 2 + + +@pytest.mark.parametrize( + "load_device_points_file", + ["device_points_nibe_smo20.json"], +) +async def test_entity_registry_smo20( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_myuplink_client: MagicMock, + setup_platform: None, +) -> None: + """Test that the entities are registered in the entity registry.""" + + entry = entity_registry.async_get("select.gotham_city_all") + assert entry.unique_id == "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47660"