diff --git a/.coveragerc b/.coveragerc index 1a752626988..1784bdad2f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -394,13 +394,10 @@ omit = homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py - homeassistant/components/fritz/__init__.py homeassistant/components/fritz/binary_sensor.py - homeassistant/components/fritz/button.py homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py - homeassistant/components/fritz/diagnostics.py homeassistant/components/fritz/sensor.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 1dc60f4a59e..139a8448b08 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,115 +1,86 @@ """Common stuff for AVM Fritz!Box tests.""" -from unittest import mock +import logging from unittest.mock import patch +from fritzconnection.core.processor import Service +from fritzconnection.lib.fritzhosts import FritzHosts import pytest +from .const import MOCK_FB_SERVICES, MOCK_MESH_DATA, MOCK_MODELNAME -@pytest.fixture() -def fc_class_mock(): - """Fixture that sets up a mocked FritzConnection class.""" - with patch("fritzconnection.FritzConnection", autospec=True) as result: - result.return_value = FritzConnectionMock() - yield result +LOGGER = logging.getLogger(__name__) + + +class FritzServiceMock(Service): + """Service mocking.""" + + def __init__(self, serviceId: str, actions: dict) -> None: + """Init Service mock.""" + super().__init__() + self._actions = actions + self.serviceId = serviceId class FritzConnectionMock: # pylint: disable=too-few-public-methods """FritzConnection mocking.""" - FRITZBOX_DATA = { - ("WANIPConn:1", "GetStatusInfo"): { - "NewConnectionStatus": "Connected", - "NewUptime": 35307, - }, - ("WANIPConnection:1", "GetStatusInfo"): {}, - ("WANCommonIFC:1", "GetCommonLinkProperties"): { - "NewLayer1DownstreamMaxBitRate": 10087000, - "NewLayer1UpstreamMaxBitRate": 2105000, - "NewPhysicalLinkStatus": "Up", - }, - ("WANCommonIFC:1", "GetAddonInfos"): { - "NewByteSendRate": 3438, - "NewByteReceiveRate": 67649, - "NewTotalBytesSent": 1712232562, - "NewTotalBytesReceived": 5221019883, - }, - ("LANEthernetInterfaceConfig:1", "GetStatistics"): { - "NewBytesSent": 23004321, - "NewBytesReceived": 12045, - }, - ("DeviceInfo:1", "GetInfo"): { - "NewSerialNumber": "abcdefgh", - "NewName": "TheName", - "NewModelName": "FRITZ!Box 7490", - }, - } - - FRITZBOX_DATA_INDEXED = { - ("X_AVM-DE_Homeauto:1", "GetGenericDeviceInfos"): [ - { - "NewSwitchIsValid": "VALID", - "NewMultimeterIsValid": "VALID", - "NewTemperatureIsValid": "VALID", - "NewDeviceId": 16, - "NewAIN": "08761 0114116", - "NewDeviceName": "FRITZ!DECT 200 #1", - "NewTemperatureOffset": "0", - "NewSwitchLock": "0", - "NewProductName": "FRITZ!DECT 200", - "NewPresent": "CONNECTED", - "NewMultimeterPower": 1673, - "NewHkrComfortTemperature": "0", - "NewSwitchMode": "AUTO", - "NewManufacturer": "AVM", - "NewMultimeterIsEnabled": "ENABLED", - "NewHkrIsTemperature": "0", - "NewFunctionBitMask": 2944, - "NewTemperatureIsEnabled": "ENABLED", - "NewSwitchState": "ON", - "NewSwitchIsEnabled": "ENABLED", - "NewFirmwareVersion": "03.87", - "NewHkrSetVentilStatus": "CLOSED", - "NewMultimeterEnergy": 5182, - "NewHkrComfortVentilStatus": "CLOSED", - "NewHkrReduceTemperature": "0", - "NewHkrReduceVentilStatus": "CLOSED", - "NewHkrIsEnabled": "DISABLED", - "NewHkrSetTemperature": "0", - "NewTemperatureCelsius": "225", - "NewHkrIsValid": "INVALID", - }, - {}, - ], - ("Hosts1", "GetGenericHostEntry"): [ - { - "NewSerialNumber": 1234, - "NewName": "TheName", - "NewModelName": "FRITZ!Box 7490", - }, - {}, - ], - } - - MODELNAME = "FRITZ!Box 7490" - - def __init__(self): + def __init__(self, services): """Inint Mocking class.""" - self.modelname = self.MODELNAME - self.call_action = mock.Mock(side_effect=self._side_effect_call_action) - type(self).action_names = mock.PropertyMock( - side_effect=self._side_effect_action_names - ) + self.modelname = MOCK_MODELNAME + self.call_action = self._call_action + self._services = services self.services = { - srv: None - for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) + srv: FritzServiceMock(serviceId=srv, actions=actions) + for srv, actions in services.items() } + LOGGER.debug("-" * 80) + LOGGER.debug("FritzConnectionMock - services: %s", self.services) + + def _call_action(self, service: str, action: str, **kwargs): + LOGGER.debug( + "_call_action service: %s, action: %s, **kwargs: %s", + service, + action, + {**kwargs}, + ) + if ":" in service: + service, number = service.split(":", 1) + service = service + number + elif not service[-1].isnumeric(): + service = service + "1" - def _side_effect_call_action(self, service, action, **kwargs): if kwargs: - index = next(iter(kwargs.values())) - return self.FRITZBOX_DATA_INDEXED[(service, action)][index] - return self.FRITZBOX_DATA[(service, action)] + if (index := kwargs.get("NewIndex")) is None: + index = next(iter(kwargs.values())) - def _side_effect_action_names(self): - return list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) + return self._services[service][action][index] + return self._services[service][action] + + +class FritzHostMock(FritzHosts): + """FritzHosts mocking.""" + + def get_mesh_topology(self, raw=False): + """Retrurn mocked mesh data.""" + return MOCK_MESH_DATA + + +@pytest.fixture() +def fc_class_mock(): + """Fixture that sets up a mocked FritzConnection class.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", autospec=True + ) as result: + result.return_value = FritzConnectionMock(MOCK_FB_SERVICES) + yield result + + +@pytest.fixture() +def fh_class_mock(): + """Fixture that sets up a mocked FritzHosts class.""" + with patch( + "homeassistant.components.fritz.common.FritzHosts", + new=FritzHostMock, + ) as result: + yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 3212794fc85..79d92a1e22d 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -26,9 +26,758 @@ MOCK_CONFIG = { } } MOCK_HOST = "fake_host" -MOCK_IP = "192.168.178.1" +MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} +MOCK_MODELNAME = "FRITZ!Box 7530 AX" +MOCK_FIRMWARE = "256.07.29" MOCK_SERIAL_NUMBER = "fake_serial_number" MOCK_FIRMWARE_INFO = [True, "1.1.1"] +MOCK_MESH_SSID = "TestSSID" +MOCK_MESH_MASTER_MAC = "1C:ED:6F:12:34:11" +MOCK_MESH_MASTER_WIFI1_MAC = "1C:ED:6F:12:34:12" +MOCK_MESH_SLAVE_MAC = "1C:ED:6F:12:34:21" +MOCK_MESH_SLAVE_WIFI1_MAC = "1C:ED:6F:12:34:22" + +MOCK_FB_SERVICES: dict[str, dict] = { + "DeviceInfo1": { + "GetInfo": { + "NewSerialNumber": MOCK_MESH_MASTER_MAC, + "NewName": "TheName", + "NewModelName": MOCK_MODELNAME, + "NewSoftwareVersion": MOCK_FIRMWARE, + "NewUpTime": 2518179, + }, + }, + "Hosts1": { + "GetGenericHostEntry": [ + { + "NewIPAddress": MOCK_IPS["fritz.box"], + "NewAddressSource": "Static", + "NewLeaseTimeRemaining": 0, + "NewMACAddress": MOCK_MESH_MASTER_MAC, + "NewInterfaceType": "", + "NewActive": True, + "NewHostName": "fritz.box", + }, + { + "NewIPAddress": MOCK_IPS["printer"], + "NewAddressSource": "DHCP", + "NewLeaseTimeRemaining": 0, + "NewMACAddress": "AA:BB:CC:00:11:22", + "NewInterfaceType": "Ethernet", + "NewActive": True, + "NewHostName": "printer", + }, + ], + "X_AVM-DE_GetMeshListPath": {}, + }, + "LANEthernetInterfaceConfig1": { + "GetStatistics": { + "NewBytesSent": 23004321, + "NewBytesReceived": 12045, + }, + }, + "Layer3Forwarding1": { + "GetDefaultConnectionService": { + "NewDefaultConnectionService": "1.WANPPPConnection.1" + } + }, + "UserInterface1": { + "GetInfo": {}, + }, + "WANCommonIFC1": { + "GetCommonLinkProperties": { + "NewLayer1DownstreamMaxBitRate": 10087000, + "NewLayer1UpstreamMaxBitRate": 2105000, + "NewPhysicalLinkStatus": "Up", + }, + "GetAddonInfos": { + "NewByteSendRate": 3438, + "NewByteReceiveRate": 67649, + "NewTotalBytesSent": 1712232562, + "NewTotalBytesReceived": 5221019883, + "NewX_AVM_DE_TotalBytesSent64": 1712232562, + "NeWX_AVM_DE_TotalBytesReceived64": 5221019883, + }, + "GetTotalBytesSent": {"NewTotalBytesSent": 1712232562}, + "GetTotalBytesReceived": {"NewTotalBytesReceived": 5221019883}, + }, + "WANCommonInterfaceConfig1": { + "GetCommonLinkProperties": { + "NewWANAccessType": "DSL", + "NewLayer1UpstreamMaxBitRate": 51805000, + "NewLayer1DownstreamMaxBitRate": 318557000, + "NewPhysicalLinkStatus": "Up", + } + }, + "WANDSLInterfaceConfig1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewDataPath": "Interleaved", + "NewUpstreamCurrRate": 46720, + "NewDownstreamCurrRate": 292030, + "NewUpstreamMaxRate": 51348, + "NewDownstreamMaxRate": 315978, + "NewUpstreamNoiseMargin": 90, + "NewDownstreamNoiseMargin": 80, + "NewUpstreamAttenuation": 70, + "NewDownstreamAttenuation": 120, + "NewATURVendor": "41564d00", + "NewATURCountry": "0400", + "NewUpstreamPower": 500, + "NewDownstreamPower": 500, + } + }, + "WANIPConn1": { + "GetStatusInfo": { + "NewConnectionStatus": "Connected", + "NewUptime": 35307, + }, + "GetExternalIPAddress": {"NewExternalIPAddress": "1.2.3.4"}, + }, + "WANPPPConnection1": { + "GetInfo": { + "NewEnable": True, + "NewConnectionStatus": "Connected", + "NewUptime": 57199, + "NewUpstreamMaxBitRate": 46531924, + "NewDownstreamMaxBitRate": 43430530, + "NewExternalIPAddress": "1.2.3.4", + }, + "GetPortMappingNumberOfEntries": {}, + }, + "X_AVM-DE_Homeauto1": { + "GetGenericDeviceInfos": [ + { + "NewSwitchIsValid": "VALID", + "NewMultimeterIsValid": "VALID", + "NewTemperatureIsValid": "VALID", + "NewDeviceId": 16, + "NewAIN": "08761 0114116", + "NewDeviceName": "FRITZ!DECT 200 #1", + "NewTemperatureOffset": "0", + "NewSwitchLock": "0", + "NewProductName": "FRITZ!DECT 200", + "NewPresent": "CONNECTED", + "NewMultimeterPower": 1673, + "NewHkrComfortTemperature": "0", + "NewSwitchMode": "AUTO", + "NewManufacturer": "AVM", + "NewMultimeterIsEnabled": "ENABLED", + "NewHkrIsTemperature": "0", + "NewFunctionBitMask": 2944, + "NewTemperatureIsEnabled": "ENABLED", + "NewSwitchState": "ON", + "NewSwitchIsEnabled": "ENABLED", + "NewFirmwareVersion": "03.87", + "NewHkrSetVentilStatus": "CLOSED", + "NewMultimeterEnergy": 5182, + "NewHkrComfortVentilStatus": "CLOSED", + "NewHkrReduceTemperature": "0", + "NewHkrReduceVentilStatus": "CLOSED", + "NewHkrIsEnabled": "DISABLED", + "NewHkrSetTemperature": "0", + "NewTemperatureCelsius": "225", + "NewHkrIsValid": "INVALID", + }, + {}, + ], + }, + "X_AVM-DE_HostFilter1": { + "GetWANAccessByIP": { + MOCK_IPS["printer"]: {"NewDisallow": False, "NewWANAccess": "granted"} + } + }, +} + +MOCK_MESH_DATA = { + "schema_version": "1.9", + "nodes": [ + { + "uid": "n-1", + "device_name": "fritz.box", + "device_model": "FRITZ!Box 7530 AX", + "device_manufacturer": "AVM", + "device_firmware_version": "256.07.29", + "device_mac_address": MOCK_MESH_MASTER_MAC, + "is_meshed": True, + "mesh_role": "master", + "meshd_version": "3.13", + "node_interfaces": [ + { + "uid": "ni-5", + "name": "LANBridge", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-30", + "name": "LAN:2", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-32", + "name": "LAN:3", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-31", + "name": "LAN:1", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [ + { + "uid": "nl-78", + "type": "LAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-76", + "node_interface_1_uid": "ni-31", + "node_interface_2_uid": "ni-77", + "max_data_rate_rx": 1000000, + "max_data_rate_tx": 1000000, + "cur_data_rate_rx": 0, + "cur_data_rate_tx": 0, + "cur_availability_rx": 99, + "cur_availability_tx": 99, + } + ], + }, + { + "uid": "ni-33", + "name": "LAN:4", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-230", + "name": "AP:2G:0", + "type": "WLAN", + "mac_address": MOCK_MESH_MASTER_WIFI1_MAC, + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-219", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-89", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-90", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 51, + "tx_rsni": 255, + "rx_rcpi": -38, + "tx_rcpi": 255, + }, + { + "uid": "nl-168", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1645162418, + "node_1_uid": "n-1", + "node_2_uid": "n-118", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-119", + "max_data_rate_rx": 144400, + "max_data_rate_tx": 144400, + "cur_data_rate_rx": 144400, + "cur_data_rate_tx": 130000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 37, + "tx_rsni": 255, + "rx_rcpi": -52, + "tx_rcpi": 255, + }, + { + "uid": "nl-185", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1645273363, + "node_1_uid": "n-1", + "node_2_uid": "n-100", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-99", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 1000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 35, + "tx_rsni": 255, + "rx_rcpi": -54, + "tx_rcpi": 255, + }, + { + "uid": "nl-166", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618912, + "node_1_uid": "n-1", + "node_2_uid": "n-16", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-15", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 41, + "tx_rsni": 255, + "rx_rcpi": -48, + "tx_rcpi": 255, + }, + { + "uid": "nl-239", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618828, + "node_1_uid": "n-1", + "node_2_uid": "n-59", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-58", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 43, + "tx_rsni": 255, + "rx_rcpi": -46, + "tx_rcpi": 255, + }, + { + "uid": "nl-173", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1645331764, + "node_1_uid": "n-1", + "node_2_uid": "n-137", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-138", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 72200, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 38, + "tx_rsni": 255, + "rx_rcpi": -51, + "tx_rcpi": 255, + }, + { + "uid": "nl-217", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618833, + "node_1_uid": "n-1", + "node_2_uid": "n-128", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-127", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 72200, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 41, + "tx_rsni": 255, + "rx_rcpi": -48, + "tx_rcpi": 255, + }, + { + "uid": "nl-198", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-105", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-106", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 58500, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 28, + "tx_rsni": 255, + "rx_rcpi": -61, + "tx_rcpi": 255, + }, + { + "uid": "nl-213", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-111", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-112", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 44, + "tx_rsni": 255, + "rx_rcpi": -45, + "tx_rcpi": 255, + }, + { + "uid": "nl-224", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618831, + "node_1_uid": "n-1", + "node_2_uid": "n-197", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-196", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 51, + "tx_rsni": 255, + "rx_rcpi": -38, + "tx_rcpi": 255, + }, + { + "uid": "nl-182", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618822, + "node_1_uid": "n-1", + "node_2_uid": "n-56", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-55", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 72200, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 34, + "tx_rsni": 255, + "rx_rcpi": -55, + "tx_rcpi": 255, + }, + { + "uid": "nl-205", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-109", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-108", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 43, + "tx_rsni": 255, + "rx_rcpi": -46, + "tx_rcpi": 255, + }, + { + "uid": "nl-240", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618827, + "node_1_uid": "n-1", + "node_2_uid": "n-95", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-96", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 58500, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 25, + "tx_rsni": 255, + "rx_rcpi": -64, + "tx_rcpi": 255, + }, + { + "uid": "nl-146", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-167", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-134", + "max_data_rate_rx": 144400, + "max_data_rate_tx": 144400, + "cur_data_rate_rx": 144400, + "cur_data_rate_tx": 130000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 48, + "tx_rsni": 255, + "rx_rcpi": -41, + "tx_rcpi": 255, + }, + { + "uid": "nl-232", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618829, + "node_1_uid": "n-1", + "node_2_uid": "n-18", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-17", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 21700, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 22, + "tx_rsni": 255, + "rx_rcpi": -67, + "tx_rcpi": 255, + }, + ], + "ssid": MOCK_MESH_SSID, + "opmode": "AP", + "security": "WPA2_WPA3_MIXED", + "supported_streams_tx": [ + ["20 MHz", 2], + ["40 MHz", 0], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "supported_streams_rx": [ + ["20 MHz", 2], + ["40 MHz", 0], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "current_channel": 13, + "phymodes": ["g", "n", "ax"], + "channel_utilization": 0, + "anpi": -91, + "steering_enabled": True, + "11k_friendly": True, + "11v_friendly": True, + "legacy_friendly": True, + "rrm_compliant": False, + "channel_list": [ + {"channel": 1}, + {"channel": 2}, + {"channel": 3}, + {"channel": 4}, + {"channel": 5}, + {"channel": 6}, + {"channel": 7}, + {"channel": 8}, + {"channel": 9}, + {"channel": 10}, + {"channel": 11}, + {"channel": 12}, + {"channel": 13}, + ], + }, + ], + }, + { + "uid": "n-76", + "device_name": "printer", + "device_model": "", + "device_manufacturer": "", + "device_firmware_version": "", + "device_mac_address": "AA:BB:CC:00:11:22", + "is_meshed": False, + "mesh_role": "unknown", + "meshd_version": "0.0", + "node_interfaces": [ + { + "uid": "ni-77", + "name": "eth0", + "type": "LAN", + "mac_address": "AA:BB:CC:00:11:22", + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-78", + "type": "LAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-76", + "node_interface_1_uid": "ni-31", + "node_interface_2_uid": "ni-77", + "max_data_rate_rx": 1000000, + "max_data_rate_tx": 1000000, + "cur_data_rate_rx": 0, + "cur_data_rate_tx": 0, + "cur_availability_rx": 99, + "cur_availability_tx": 99, + } + ], + } + ], + }, + { + "uid": "n-167", + "device_name": "fritz-repeater", + "device_model": "FRITZ!Box 7490", + "device_manufacturer": "AVM", + "device_firmware_version": "113.07.29", + "device_mac_address": MOCK_MESH_SLAVE_MAC, + "is_meshed": True, + "mesh_role": "slave", + "meshd_version": "3.13", + "node_interfaces": [ + { + "uid": "ni-140", + "name": "LAN:3", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-139", + "name": "LAN:4", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-141", + "name": "LAN:2", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-134", + "name": "UPLINK:2G:0", + "type": "WLAN", + "mac_address": MOCK_MESH_SLAVE_WIFI1_MAC, + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-146", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-167", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-134", + "max_data_rate_rx": 144400, + "max_data_rate_tx": 144400, + "cur_data_rate_rx": 144400, + "cur_data_rate_tx": 130000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 48, + "tx_rsni": 255, + "rx_rcpi": -41, + "tx_rcpi": 255, + } + ], + "ssid": "", + "opmode": "WDS_REPEATER", + "security": "WPA3PSK", + "supported_streams_tx": [ + ["20 MHz", 3], + ["40 MHz", 3], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "supported_streams_rx": [ + ["20 MHz", 3], + ["40 MHz", 3], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "current_channel": 13, + "phymodes": ["b", "g", "n"], + "channel_utilization": 0, + "anpi": 255, + "steering_enabled": True, + "11k_friendly": False, + "11v_friendly": True, + "legacy_friendly": True, + "rrm_compliant": False, + "channel_list": [ + {"channel": 1}, + {"channel": 2}, + {"channel": 3}, + {"channel": 4}, + {"channel": 5}, + {"channel": 6}, + {"channel": 7}, + {"channel": 8}, + {"channel": 9}, + {"channel": 10}, + {"channel": 11}, + {"channel": 12}, + {"channel": 13}, + ], + "client_position": "unknown", + }, + { + "uid": "ni-143", + "name": "LANBridge", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-142", + "name": "LAN:1", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + ], + }, + ], +} + MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_DEVICE_INFO = { @@ -38,7 +787,7 @@ MOCK_DEVICE_INFO = { MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location=f"https://{MOCK_IP}:12345/test", + ssdp_location=f"https://{MOCK_IPS['fritz.box']}:12345/test", upnp={ ATTR_UPNP_FRIENDLY_NAME: "fake_name", ATTR_UPNP_UDN: "uuid:only-a-test", diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py new file mode 100644 index 00000000000..a6ff579958a --- /dev/null +++ b/tests/components/fritz/test_button.py @@ -0,0 +1,75 @@ +"""Tests for Shelly button platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_button_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test setup of Fritz!Tools buttons.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + buttons = hass.states.async_all(BUTTON_DOMAIN) + assert len(buttons) == 4 + + for button in buttons: + assert button.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "entity_id, wrapper_method", + [ + ("button.mock_title_firmware_update", "async_trigger_firmware_update"), + ("button.mock_title_reboot", "async_trigger_reboot"), + ("button.mock_title_reconnect", "async_trigger_reconnect"), + ("button.mock_title_cleanup", "async_trigger_cleanup"), + ], +) +async def test_buttons( + hass: HomeAssistant, + entity_id: str, + wrapper_method: str, + fc_class_mock, + fh_class_mock, +): + """Test Fritz!Tools buttons.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + button = hass.states.get(entity_id) + assert button + assert button.state == STATE_UNKNOWN + with patch( + f"homeassistant.components.fritz.common.AvmWrapper.{wrapper_method}" + ) as mock_press_action: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + mock_press_action.assert_called_once() + + button = hass.states.get(entity_id) + assert button.state != STATE_UNKNOWN diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 6505ee2bcaa..502757c2c67 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -26,7 +26,7 @@ from homeassistant.data_entry_flow import ( from .const import ( MOCK_FIRMWARE_INFO, - MOCK_IP, + MOCK_IPS, MOCK_REQUEST, MOCK_SSDP_DATA, MOCK_USER_DATA, @@ -51,7 +51,7 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "requests.post" ) as mock_request_post, patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): mock_request_get.return_value.status_code = 200 @@ -102,7 +102,7 @@ async def test_user_already_configured( "requests.post" ) as mock_request_post, patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): mock_request_get.return_value.status_code = 200 @@ -295,7 +295,7 @@ async def test_ssdp_already_configured( side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): result = await hass.config_entries.flow.async_init( @@ -322,7 +322,7 @@ async def test_ssdp_already_configured_host( side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): result = await hass.config_entries.flow.async_init( @@ -349,7 +349,7 @@ async def test_ssdp_already_configured_host_uuid( side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): result = await hass.config_entries.flow.async_init( @@ -420,7 +420,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_HOST] == MOCK_IP + assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py new file mode 100644 index 00000000000..892210d0844 --- /dev/null +++ b/tests/components/fritz/test_diagnostics.py @@ -0,0 +1,80 @@ +"""Tests for the AVM Fritz!Box integration.""" +from __future__ import annotations + +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.fritz.common import AvmWrapper +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.fritz.diagnostics import TO_REDACT +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, fc_class_mock, fh_class_mock +): + """Test config entry diagnostics.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + entry_dict = entry.as_dict() + for key in TO_REDACT: + entry_dict["data"][key] = REDACTED + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + assert result == { + "entry": entry_dict, + "device_info": { + "client_devices": [ + { + "connected_to": device.connected_to, + "connection_type": device.connection_type, + "hostname": device.hostname, + "is_connected": device.is_connected, + "last_activity": device.last_activity.isoformat(), + "wan_access": device.wan_access, + } + for _, device in avm_wrapper.devices.items() + ], + "connection_type": "WANPPPConnection", + "current_firmware": "256.07.29", + "discovered_services": [ + "DeviceInfo1", + "Hosts1", + "LANEthernetInterfaceConfig1", + "Layer3Forwarding1", + "UserInterface1", + "WANCommonIFC1", + "WANCommonInterfaceConfig1", + "WANDSLInterfaceConfig1", + "WANIPConn1", + "WANPPPConnection1", + "X_AVM-DE_Homeauto1", + "X_AVM-DE_HostFilter1", + ], + "is_router": True, + "last_exception": None, + "last_update success": True, + "latest_firmware": None, + "mesh_role": "master", + "model": "FRITZ!Box 7530 AX", + "update_available": False, + "wan_link_properties": { + "NewLayer1DownstreamMaxBitRate": 318557000, + "NewLayer1UpstreamMaxBitRate": 51805000, + "NewPhysicalLinkStatus": "Up", + "NewWANAccessType": "DSL", + }, + }, + } diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py new file mode 100644 index 00000000000..fd67321d235 --- /dev/null +++ b/tests/components/fritz/test_init.py @@ -0,0 +1,96 @@ +"""Tests for AVM Fritz!Box.""" +from unittest.mock import patch + +from fritzconnection.core.exceptions import FritzSecurityError +import pytest + +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) +from homeassistant.components.fritz.const import DOMAIN, FRITZ_EXCEPTIONS +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test setup and unload of Fritz!Tools.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED + + +async def test_options_reload(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test reload of Fritz!Tools, when options changed.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_USER_DATA, + options={CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds()}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=None, + ) as mock_reload: + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_CONSIDER_HOME: 60}, + ) + await hass.async_block_till_done() + mock_reload.assert_called_once() + + +async def test_setup_auth_fail(hass: HomeAssistant): + """Test starting a flow by user with an already configured device.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=FritzSecurityError, + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.SETUP_ERROR + + +@pytest.mark.parametrize( + "error", + FRITZ_EXCEPTIONS, +) +async def test_setup_fail(hass: HomeAssistant, error): + """Test starting a flow by user with an already configured device.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=error, + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.SETUP_RETRY