diff --git a/homeassistant/components/vodafone_station/__init__.py b/homeassistant/components/vodafone_station/__init__.py index 6d8bbf7b8ea..b792e4904f2 100644 --- a/homeassistant/components/vodafone_station/__init__.py +++ b/homeassistant/components/vodafone_station/__init__.py @@ -15,6 +15,7 @@ PLATFORMS = [ Platform.DEVICE_TRACKER, Platform.IMAGE, Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/vodafone_station/icons.json b/homeassistant/components/vodafone_station/icons.json index 6188876fa3a..1cb2f0d102b 100644 --- a/homeassistant/components/vodafone_station/icons.json +++ b/homeassistant/components/vodafone_station/icons.json @@ -39,6 +39,32 @@ "sys_reboot_cause": { "default": "mdi:restart-alert" } + }, + "switch": { + "guest": { + "default": "mdi:wifi", + "state": { + "off": "mdi:wifi-off" + } + }, + "guest_5g": { + "default": "mdi:wifi", + "state": { + "off": "mdi:wifi-off" + } + }, + "main": { + "default": "mdi:wifi", + "state": { + "off": "mdi:wifi-off" + } + }, + "main_5g": { + "default": "mdi:wifi", + "state": { + "off": "mdi:wifi-off" + } + } } } } diff --git a/homeassistant/components/vodafone_station/strings.json b/homeassistant/components/vodafone_station/strings.json index bf07ce79af1..5a32f7ecc47 100644 --- a/homeassistant/components/vodafone_station/strings.json +++ b/homeassistant/components/vodafone_station/strings.json @@ -119,6 +119,20 @@ "up_stream": { "name": "WAN upload rate" } + }, + "switch": { + "guest": { + "name": "Guest network" + }, + "guest_5g": { + "name": "Guest 5GHz network" + }, + "main": { + "name": "Main network" + }, + "main_5g": { + "name": "Main 5GHz network" + } } }, "exceptions": { diff --git a/homeassistant/components/vodafone_station/switch.py b/homeassistant/components/vodafone_station/switch.py new file mode 100644 index 00000000000..c0dd130c3dd --- /dev/null +++ b/homeassistant/components/vodafone_station/switch.py @@ -0,0 +1,141 @@ +"""Support for switches.""" + +from __future__ import annotations + +from dataclasses import dataclass +from json.decoder import JSONDecodeError +from typing import Any, Final + +from aiovodafone.const import WIFI_DATA, WifiBand, WifiType +from aiovodafone.exceptions import ( + AlreadyLogged, + CannotAuthenticate, + CannotConnect, + GenericLoginError, + GenericResponseError, +) + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import VodafoneConfigEntry, VodafoneStationRouter + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class VodafoneStationEntityDescription(SwitchEntityDescription): + """Vodafone Station entity description.""" + + band: WifiBand + typology: WifiType + + +SWITCHES: Final = ( + VodafoneStationEntityDescription( + key="main", + translation_key="main", + band=WifiBand.BAND_2_4_GHZ, + typology=WifiType.MAIN, + ), + VodafoneStationEntityDescription( + key="guest", + translation_key="guest", + band=WifiBand.BAND_2_4_GHZ, + typology=WifiType.GUEST, + ), + VodafoneStationEntityDescription( + key="main_5g", + translation_key="main_5g", + band=WifiBand.BAND_5_GHZ, + typology=WifiType.MAIN, + ), + VodafoneStationEntityDescription( + key="guest_5g", + translation_key="guest_5g", + band=WifiBand.BAND_5_GHZ, + typology=WifiType.GUEST, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: VodafoneConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Vodafone Station switches based on a config entry.""" + + coordinator = entry.runtime_data + + wifi = coordinator.data.wifi + + async_add_entities( + VodafoneSwitchEntity(coordinator, switch_desc) + for switch_desc in SWITCHES + if switch_desc.key in wifi[WIFI_DATA] + ) + + +class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntity): + """Switch device.""" + + _attr_has_entity_name = True + entity_description: VodafoneStationEntityDescription + + def __init__( + self, + coordinator: VodafoneStationRouter, + description: VodafoneStationEntityDescription, + ) -> None: + """Initialize switch device.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.serial_number}_{description.key}" + + async def _set_wifi_status(self, status: bool) -> None: + """Set the wifi status.""" + try: + await self.coordinator.api.set_wifi_status( + status, self.entity_description.typology, self.entity_description.band + ) + except CannotAuthenticate as err: + self.coordinator.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_authenticate", + translation_placeholders={"error": repr(err)}, + ) from err + except ( + CannotConnect, + AlreadyLogged, + GenericLoginError, + GenericResponseError, + JSONDecodeError, + ) as err: + self.coordinator.last_update_success = False + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_execute_action", + translation_placeholders={"error": repr(err)}, + ) from err + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self._set_wifi_status(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self._set_wifi_status(False) + + @property + def is_on(self) -> bool: + """Return True if switch is on.""" + return bool( + self.coordinator.data.wifi[WIFI_DATA][self.entity_description.key]["on"] + ) diff --git a/tests/components/vodafone_station/snapshots/test_switch.ambr b/tests/components/vodafone_station/snapshots/test_switch.ambr new file mode 100644 index 00000000000..f1e33c4d3b3 --- /dev/null +++ b/tests/components/vodafone_station/snapshots/test_switch.ambr @@ -0,0 +1,99 @@ +# serializer version: 1 +# name: test_all_entities[switch.vodafone_station_m123456789_guest_5ghz_network-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.vodafone_station_m123456789_guest_5ghz_network', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Guest 5GHz network', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Guest 5GHz network', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'guest_5g', + 'unique_id': 'm123456789_guest_5g', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.vodafone_station_m123456789_guest_5ghz_network-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vodafone Station (m123456789) Guest 5GHz network', + }), + 'context': , + 'entity_id': 'switch.vodafone_station_m123456789_guest_5ghz_network', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[switch.vodafone_station_m123456789_guest_network-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.vodafone_station_m123456789_guest_network', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Guest network', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Guest network', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'guest', + 'unique_id': 'm123456789_guest', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.vodafone_station_m123456789_guest_network-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vodafone Station (m123456789) Guest network', + }), + 'context': , + 'entity_id': 'switch.vodafone_station_m123456789_guest_network', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/vodafone_station/test_switch.py b/tests/components/vodafone_station/test_switch.py new file mode 100644 index 00000000000..5d34e01ffa5 --- /dev/null +++ b/tests/components/vodafone_station/test_switch.py @@ -0,0 +1,141 @@ +"""Tests for Vodafone Station switch platform.""" + +from unittest.mock import AsyncMock, patch + +from aiovodafone.const import WIFI_DATA +from aiovodafone.exceptions import ( + AlreadyLogged, + CannotAuthenticate, + CannotConnect, + GenericLoginError, +) +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TOGGLE +from homeassistant.components.vodafone_station.const import DOMAIN +from homeassistant.components.vodafone_station.coordinator import SCAN_INTERVAL +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er +from homeassistant.util import slugify + +from . import setup_integration +from .const import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.vodafone_station.PLATFORMS", [Platform.SWITCH] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("wifi_key", "wifi_name", "wifi_state"), + [ + ("guest", "guest_network", "on"), + ("guest_5g", "guest_5ghz_network", "off"), + ], +) +async def test_switch( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + wifi_key: str, + wifi_name: str, + wifi_state: str, +) -> None: + """Test switch.""" + + mock_vodafone_station_router.get_wifi_data.return_value = { + WIFI_DATA: { + f"{wifi_key}": { + "on": 1 if wifi_state == "on" else 0, + "ssid": f"{wifi_name}", + } + } + } + + await setup_integration(hass, mock_config_entry) + + entity_id = f"switch.vodafone_station_{TEST_SERIAL_NUMBER}_{slugify(wifi_name)}" + assert (state := hass.states.get(entity_id)) + assert state.state == wifi_state + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_vodafone_station_router.set_wifi_status.call_count == 1 + + mock_vodafone_station_router.get_wifi_data.return_value = { + WIFI_DATA: { + f"{wifi_key}": { + "on": 0 if wifi_state == "on" else 1, + "ssid": f"{wifi_name}", + } + } + } + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state == ("off" if wifi_state == "on" else "on") + + +@pytest.mark.parametrize( + ("side_effect", "key", "error"), + [ + (CannotConnect, "cannot_execute_action", "CannotConnect()"), + (AlreadyLogged, "cannot_execute_action", "AlreadyLogged()"), + (GenericLoginError, "cannot_execute_action", "GenericLoginError()"), + (CannotAuthenticate, "cannot_authenticate", "CannotAuthenticate()"), + ], +) +async def test_switch_fails( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + key: str, + error: str, +) -> None: + """Test switch action fails.""" + + await setup_integration(hass, mock_config_entry) + + mock_vodafone_station_router.set_wifi_status.side_effect = side_effect + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + { + ATTR_ENTITY_ID: f"switch.vodafone_station_{TEST_SERIAL_NUMBER}_guest_5ghz_network" + }, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == key + assert exc_info.value.translation_placeholders == {"error": error}