From 1f8a98609cc108c8a8cf318fb4ba2e3d0d6a3070 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 27 Jan 2026 10:48:51 +0100 Subject: [PATCH] Improve test coverage for switch in Fritz (#161630) --- homeassistant/components/fritz/switch.py | 14 +- tests/components/fritz/conftest.py | 27 ++ tests/components/fritz/const.py | 42 +- .../fritz/snapshots/test_switch.ambr | 400 ++++++++++++++++++ tests/components/fritz/test_switch.py | 229 +++++++++- 5 files changed, 698 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index f4cde1bef1e..a452c4821bd 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.components.network import async_get_source_ip from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -382,7 +382,7 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch): self, avm_wrapper: AvmWrapper, device_friendly_name: str, - port_mapping: dict[str, Any] | None, + port_mapping: dict[str, Any], port_name: str, idx: int, connection_type: str, @@ -396,9 +396,6 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch): self._idx = idx # needed for update routine self._attr_entity_category = EntityCategory.CONFIG - if port_mapping is None: - return - switch_info = SwitchInfo( description=f"Port forward {port_name}", friendly_name=device_friendly_name, @@ -438,9 +435,6 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch): self._attributes[attr] = self.port_mapping[key] async def _async_switch_on_off_executor(self, turn_on: bool) -> bool: - if self.port_mapping is None: - return False - self.port_mapping["NewEnabled"] = "1" if turn_on else "0" resp = await self._avm_wrapper.async_add_port_mapping( @@ -530,8 +524,8 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): async def _async_handle_turn_on_off(self, turn_on: bool) -> bool: """Handle switch state change request.""" - if not self.ip_address: - return False + if TYPE_CHECKING: + assert self.ip_address await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on) self._avm_wrapper.devices[self._mac].wan_access = turn_on self.async_write_ha_state() diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 017328ea0eb..04e59082b27 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -5,6 +5,8 @@ from unittest.mock import MagicMock, patch from fritzconnection.core.processor import Service from fritzconnection.lib.fritzhosts import FritzHosts +from fritzconnection.lib.fritzstatus import FritzStatus +from fritzconnection.lib.fritztools import ArgumentNamespace import pytest from .const import ( @@ -12,6 +14,9 @@ from .const import ( MOCK_HOST_ATTRIBUTES_DATA, MOCK_MESH_DATA, MOCK_MODELNAME, + MOCK_STATUS_AVM_DEVICE_LOG_DATA, + MOCK_STATUS_CONNECTION_DATA, + MOCK_STATUS_DEVICE_INFO_DATA, ) LOGGER = logging.getLogger(__name__) @@ -121,3 +126,25 @@ def fh_class_mock(): result.get_mesh_topology = MagicMock(return_value=MOCK_MESH_DATA) result.get_hosts_attributes = MagicMock(return_value=MOCK_HOST_ATTRIBUTES_DATA) yield result + + +@pytest.fixture +def fs_class_mock(): + """Fixture that sets up a mocked FritzStatus class.""" + with patch( + "homeassistant.components.fritz.coordinator.FritzStatus", + new=FritzStatus, + ) as result: + result.get_default_connection_service = MagicMock( + return_value=MOCK_STATUS_CONNECTION_DATA + ) + result.get_device_info = MagicMock( + return_value=ArgumentNamespace(MOCK_STATUS_DEVICE_INFO_DATA) + ) + result.get_monitor_data = MagicMock(return_value={}) + result.get_cpu_temperatures = MagicMock(return_value=[42, 38]) + result.get_avm_device_log = MagicMock( + return_value=MOCK_STATUS_AVM_DEVICE_LOG_DATA + ) + result.has_wan_enabled = True + yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 8154162000a..e2bb2ce048f 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -1,5 +1,7 @@ """Common stuff for Fritz!Tools tests.""" +from fritzconnection.lib.fritzstatus import DefaultConnectionService + from homeassistant.components.fritz.const import DOMAIN from homeassistant.const import ( CONF_DEVICES, @@ -57,9 +59,17 @@ MOCK_FB_SERVICES: dict[str, dict] = { "GetInfo": { "NewSerialNumber": MOCK_MESH_MASTER_MAC, "NewName": "TheName", + "NewManufacturerName": "AVM", + "NewManufacturerOUI": "00040E", "NewModelName": MOCK_MODELNAME, + "NewDescription": f"{MOCK_MODELNAME} {MOCK_FIRMWARE}", + "NewProductClass": "AVMFB7590AX", "NewSoftwareVersion": MOCK_FIRMWARE, + "NewHardwareVersion": MOCK_MODELNAME, + "NewSpecVersion": "1.0", + "NewDeviceLog": "long string here ...", "NewUpTime": 2518179, + "NewProvisioningCode": "000.044.004.000", }, }, "Hosts1": { @@ -147,7 +157,27 @@ MOCK_FB_SERVICES: dict[str, dict] = { "NewDownstreamMaxBitRate": 43430530, "NewExternalIPAddress": "1.2.3.4", }, - "GetPortMappingNumberOfEntries": {}, + "GetPortMappingNumberOfEntries": { + "NewPortMappingNumberOfEntries": 2, + }, + "GetGenericPortMappingEntry": { + 0: { + "NewInternalClient": "10.10.10.10", + "NewPortMappingDescription": "Test Port Mapping", + "NewInternalPort": 8080, + "NewExternalPort": 80, + "NewProtocol": "TCP", + "NewEnabled": True, + }, + 1: { + "NewInternalClient": "10.10.10.10", + "NewPortMappingDescription": "Test Port Mapping", + "NewInternalPort": 8081, + "NewExternalPort": 81, + "NewProtocol": "UDP", + "NewEnabled": True, + }, + }, }, "WLANConfiguration1": { "GetInfo": { @@ -158,11 +188,15 @@ MOCK_FB_SERVICES: dict[str, dict] = { "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", "NewStandard": "ax", "NewBSSID": "1C:ED:6F:12:34:13", + "NewMACAddressControlEnabled": True, }, "GetSSID": { "NewSSID": "MyWifi", }, "GetSecurityKeys": {"NewKeyPassphrase": "1234567890"}, + "GetBeaconAdvertisement": { + "NewBeaconAdvertisementEnabled": True, + }, }, "X_AVM-DE_Homeauto1": { "GetGenericDeviceInfos": [ @@ -938,6 +972,12 @@ MOCK_CALL_DEFLECTION_DATA = { } } +MOCK_STATUS_CONNECTION_DATA = DefaultConnectionService("1", "WANPPPConnection", "1") +MOCK_STATUS_DEVICE_INFO_DATA = MOCK_FB_SERVICES["DeviceInfo1"]["GetInfo"] +MOCK_STATUS_AVM_DEVICE_LOG_DATA = MOCK_FB_SERVICES["DeviceInfo1"]["GetInfo"][ + "NewDeviceLog" +] + MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_USER_INPUT_ADVANCED = MOCK_USER_DATA MOCK_USER_INPUT_SIMPLE = { diff --git a/tests/components/fritz/snapshots/test_switch.ambr b/tests/components/fritz/snapshots/test_switch.ambr index 0124c5f944e..600cc9838f5 100644 --- a/tests/components/fritz/snapshots/test_switch.ambr +++ b/tests/components/fritz/snapshots/test_switch.ambr @@ -1,4 +1,104 @@ # serializer version: 1 +# name: test_switch_setup[fc_data0][switch.mock_title_port_forward_test_port_mapping-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data0][switch.mock_title_port_forward_test_port_mapping-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[fc_data0][switch.mock_title_port_forward_test_port_mapping_81-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping 81', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping 81', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping_81', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data0][switch.mock_title_port_forward_test_port_mapping_81-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping 81', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_wifi_2_4ghz-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -148,6 +248,106 @@ 'state': 'on', }) # --- +# name: test_switch_setup[fc_data1][switch.mock_title_port_forward_test_port_mapping-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data1][switch.mock_title_port_forward_test_port_mapping-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[fc_data1][switch.mock_title_port_forward_test_port_mapping_81-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping 81', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping 81', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping_81', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data1][switch.mock_title_port_forward_test_port_mapping_81-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping 81', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_wifi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -297,6 +497,106 @@ 'state': 'on', }) # --- +# name: test_switch_setup[fc_data2][switch.mock_title_port_forward_test_port_mapping-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data2][switch.mock_title_port_forward_test_port_mapping-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[fc_data2][switch.mock_title_port_forward_test_port_mapping_81-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping 81', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping 81', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping_81', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data2][switch.mock_title_port_forward_test_port_mapping_81-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping 81', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_wifi_2_4ghz-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -502,6 +802,106 @@ 'state': 'off', }) # --- +# name: test_switch_setup[fc_data3][switch.mock_title_port_forward_test_port_mapping-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data3][switch.mock_title_port_forward_test_port_mapping-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[fc_data3][switch.mock_title_port_forward_test_port_mapping_81-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': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mock Title Port forward Test Port Mapping 81', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:check-network', + 'original_name': 'Mock Title Port forward Test Port Mapping 81', + 'platform': 'fritz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1C:ED:6F:12:34:11-port_forward_test_port_mapping_81', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[fc_data3][switch.mock_title_port_forward_test_port_mapping_81-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Port forward Test Port Mapping 81', + 'icon': 'mdi:check-network', + }), + 'context': , + 'entity_id': 'switch.mock_title_port_forward_test_port_mapping_81', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_mywifi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index fdf76d54588..8410cf02162 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -2,17 +2,38 @@ from __future__ import annotations -from unittest.mock import patch +from copy import deepcopy +from unittest.mock import MagicMock, patch +from fritzconnection.core.exceptions import FritzActionError +from fritzconnection.lib.fritzstatus import DefaultConnectionService import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.fritz.const import DOMAIN -from homeassistant.const import Platform +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .const import MOCK_CALL_DEFLECTION_DATA, MOCK_FB_SERVICES, MOCK_USER_DATA +from .conftest import FritzConnectionMock +from .const import ( + MOCK_CALL_DEFLECTION_DATA, + MOCK_FB_SERVICES, + MOCK_HOST_ATTRIBUTES_DATA, + MOCK_USER_DATA, +) from tests.common import MockConfigEntry, snapshot_platform @@ -194,3 +215,205 @@ async def test_switch_setup( await hass.async_block_till_done(wait_background_tasks=True) await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +async def test_switch_no_device_conn_type( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, + fs_class_mock, +) -> None: + """Test Fritz!Tools switches when no device connection type is available.""" + + entity_id = "switch.mock_title_port_forward_test_port_mapping" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + fs_class_mock.get_default_connection_service.return_value = ( + DefaultConnectionService("", "", "") + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get(entity_id) is None + + +async def test_switch_empty_port_entities_list( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, + fs_class_mock, +) -> None: + """Test Fritz!Tools switches with empty port entities.""" + + entity_id = "switch.mock_title_port_forward_test_port_mapping" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_get_num_port_mapping", + return_value=None, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get(entity_id) is None + + +async def test_switch_no_port_entities_list( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, + fs_class_mock, +) -> None: + """Test Fritz!Tools switches with no port entities.""" + + entity_id = "switch.mock_title_port_forward_test_port_mapping" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_get_port_mapping", + return_value=None, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get(entity_id) is None + + +async def test_switch_no_profile_entities_list( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, +) -> None: + """Test Fritz!Tools switches with no profile entities.""" + + entity_id = "sswitch.printer_internet_access" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + services = deepcopy(MOCK_FB_SERVICES) + services.pop("X_AVM-DE_HostFilter1") + fc_class_mock.return_value = FritzConnectionMock(services) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get(entity_id) is None + + +async def test_switch_no_mesh_wifi_uplink( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, +) -> None: + """Test Fritz!Tools switches when no mesh WiFi uplink.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + fh_class_mock.get_mesh_topology.side_effect = FritzActionError( + "No mesh WiFi uplink" + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_switch_device_no_wan_access( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, +) -> None: + """Test Fritz!Tools switches when device has no WAN access.""" + + entity_id = "switch.printer_internet_access" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + attributes = [ + {k: v for k, v in host.items() if k != "X_AVM-DE_WANAccess"} + for host in MOCK_HOST_ATTRIBUTES_DATA + ] + fh_class_mock.get_hosts_attributes = MagicMock(return_value=attributes) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("entity_id", "wrapper_method", "state_value"), + [ + ( + "switch.mock_title_port_forward_test_port_mapping", + "async_add_port_mapping", + STATE_OFF, + ), + ( + "switch.printer_internet_access", + "async_set_allow_wan_access", + STATE_ON, + ), + ], +) +async def test_switch_turn_on_off( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, + entity_id: str, + wrapper_method: str, + state_value: str, +) -> None: + """Test Fritz!Tools switches turn on and turn off.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + fc_class_mock.return_value = FritzConnectionMock( + MOCK_FB_SERVICES | MOCK_CALL_DEFLECTION_DATA + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + assert entry.state is ConfigEntryState.LOADED + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_ON + + with patch( + f"homeassistant.components.fritz.coordinator.AvmWrapper.{wrapper_method}", + ) as mock_set_action: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_set_action.assert_called_once() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_OFF + + with patch( + f"homeassistant.components.fritz.coordinator.AvmWrapper.{wrapper_method}", + ) as mock_set_action_2: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_set_action_2.assert_called_once() + + assert (state := hass.states.get(entity_id)) + assert state.state == state_value