From a29695e622ee26d66693b06880fd74437536228e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 6 Dec 2023 14:23:26 +0200 Subject: [PATCH] Add Huawei LTE network mode select (#104614) * Convert network mode from sensor to select for huawei_lte This also introduces the select platform to huawei_lte integration. * Move (networkmode, str) mapping to const Also, rebase on top of the current dev * Fix variable naming, initialize name * Fix wrong key for router access * Typing fixes * Adapt to current way of registering subscriptions * Simplify option management, make translatable * Make use of custom entity description * Add icon * Revert sensor formatting changes, move to another PR * Improve entity class naming * Add test * Make sure entity descriptions define a setter function --------- Co-authored-by: Teemu Rytilahti --- .../components/huawei_lte/__init__.py | 1 + homeassistant/components/huawei_lte/select.py | 132 ++++++++++++++++++ .../components/huawei_lte/strings.json | 14 ++ tests/components/huawei_lte/test_select.py | 43 ++++++ 4 files changed, 190 insertions(+) create mode 100644 homeassistant/components/huawei_lte/select.py create mode 100644 tests/components/huawei_lte/test_select.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index d8c939e5c3a..dcd40b8346c 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -135,6 +135,7 @@ PLATFORMS = [ Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, + Platform.SELECT, ] diff --git a/homeassistant/components/huawei_lte/select.py b/homeassistant/components/huawei_lte/select.py new file mode 100644 index 00000000000..2f4b7274fc0 --- /dev/null +++ b/homeassistant/components/huawei_lte/select.py @@ -0,0 +1,132 @@ +"""Support for Huawei LTE selects.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from functools import partial +import logging + +from huawei_lte_api.enums.net import LTEBandEnum, NetworkBandEnum, NetworkModeEnum + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SelectEntity, + SelectEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import UNDEFINED + +from . import HuaweiLteBaseEntityWithDevice +from .const import DOMAIN, KEY_NET_NET_MODE + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class HuaweiSelectEntityMixin: + """Mixin for Huawei LTE select entities, to ensure required fields are set.""" + + setter_fn: Callable[[str], None] + + +@dataclass +class HuaweiSelectEntityDescription(SelectEntityDescription, HuaweiSelectEntityMixin): + """Class describing Huawei LTE select entities.""" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.entry_id] + selects: list[Entity] = [] + + desc = HuaweiSelectEntityDescription( + key=KEY_NET_NET_MODE, + entity_category=EntityCategory.CONFIG, + icon="mdi:transmission-tower", + name="Preferred network mode", + translation_key="preferred_network_mode", + options=[ + NetworkModeEnum.MODE_AUTO.value, + NetworkModeEnum.MODE_4G_3G_AUTO.value, + NetworkModeEnum.MODE_4G_2G_AUTO.value, + NetworkModeEnum.MODE_4G_ONLY.value, + NetworkModeEnum.MODE_3G_2G_AUTO.value, + NetworkModeEnum.MODE_3G_ONLY.value, + NetworkModeEnum.MODE_2G_ONLY.value, + ], + setter_fn=partial( + router.client.net.set_net_mode, + LTEBandEnum.ALL, + NetworkBandEnum.ALL, + ), + ) + selects.append( + HuaweiLteSelectEntity( + router, + entity_description=desc, + key=desc.key, + item="NetworkMode", + ) + ) + + async_add_entities(selects, True) + + +@dataclass +class HuaweiLteSelectEntity(HuaweiLteBaseEntityWithDevice, SelectEntity): + """Huawei LTE select entity.""" + + entity_description: HuaweiSelectEntityDescription + key: str + item: str + + _raw_state: str | None = field(default=None, init=False) + + def __post_init__(self) -> None: + """Initialize remaining attributes.""" + name = None + if self.entity_description.name != UNDEFINED: + name = self.entity_description.name + self._attr_name = name or self.item + + def select_option(self, option: str) -> None: + """Change the selected option.""" + self.entity_description.setter_fn(option) + + @property + def current_option(self) -> str | None: + """Return current option.""" + return self._raw_state + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + async def async_added_to_hass(self) -> None: + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].append(f"{SELECT_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove(f"{SELECT_DOMAIN}/{self.item}") + + async def async_update(self) -> None: + """Update state.""" + try: + value = self.router.data[self.key][self.item] + except KeyError: + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True + self._raw_state = str(value) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 754f192e57e..225146799a3 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -286,6 +286,20 @@ "name": "SMS messages (SIM)" } }, + "select": { + "preferred_network_mode": { + "name": "Preferred network mode", + "state": { + "00": "4G/3G/2G auto", + "0302": "4G/3G auto", + "0301": "4G/2G auto", + "03": "4G only", + "0201": "3G/2G auto", + "02": "3G only", + "01": "2G only" + } + } + }, "switch": { "mobile_data": { "name": "Mobile data" diff --git a/tests/components/huawei_lte/test_select.py b/tests/components/huawei_lte/test_select.py new file mode 100644 index 00000000000..c3f6ded65b6 --- /dev/null +++ b/tests/components/huawei_lte/test_select.py @@ -0,0 +1,43 @@ +"""Tests for the Huawei LTE selects.""" +from unittest.mock import MagicMock, patch + +from huawei_lte_api.enums.net import LTEBandEnum, NetworkBandEnum, NetworkModeEnum + +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.select import SERVICE_SELECT_OPTION +from homeassistant.components.select.const import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_URL +from homeassistant.core import HomeAssistant + +from . import magic_client + +from tests.common import MockConfigEntry + +SELECT_NETWORK_MODE = "select.lte_preferred_network_mode" + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_set_net_mode(client, hass: HomeAssistant) -> None: + """Test setting network mode.""" + client.return_value = magic_client({}) + huawei_lte = MockConfigEntry( + domain=DOMAIN, data={CONF_URL: "http://huawei-lte.example.com"} + ) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: SELECT_NETWORK_MODE, + ATTR_OPTION: NetworkModeEnum.MODE_4G_3G_AUTO.value, + }, + blocking=True, + ) + await hass.async_block_till_done() + client.return_value.net.set_net_mode.assert_called_once() + client.return_value.net.set_net_mode.assert_called_with( + LTEBandEnum.ALL, NetworkBandEnum.ALL, NetworkModeEnum.MODE_4G_3G_AUTO.value + )