Migrate wifi switch unique_id for Fritz (#166751)

This commit is contained in:
Simone Chemelli
2026-04-06 11:18:42 +02:00
committed by GitHub
parent 893d9306d4
commit 459fc43625
6 changed files with 339 additions and 108 deletions
-1
View File
@@ -80,6 +80,5 @@ FRITZ_EXCEPTIONS = (
FRITZ_AUTH_EXCEPTIONS = (FritzAuthorizationError, FritzSecurityError)
WIFI_STANDARD = {1: "2.4Ghz", 2: "5Ghz", 3: "5Ghz", 4: "Guest"}
CONNECTION_TYPE_LAN = "LAN"
@@ -169,6 +169,18 @@
"switch": {
"internet_access": {
"name": "Internet access"
},
"wi_fi_guest": {
"name": "Guest"
},
"wi_fi_main_2_4ghz": {
"name": "Main 2.4 GHz"
},
"wi_fi_main_5ghz": {
"name": "Main 5 GHz"
},
"wi_fi_main_5ghz_high_6ghz": {
"name": "Main 5 GHz High / 6 GHz"
}
}
},
+104 -31
View File
@@ -9,6 +9,7 @@ from homeassistant.components.network import async_get_source_ip
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -22,8 +23,8 @@ from .const import (
SWITCH_TYPE_PORTFORWARD,
SWITCH_TYPE_PROFILE,
SWITCH_TYPE_WIFINETWORK,
WIFI_STANDARD,
MeshRoles,
Platform,
)
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
from .entity import FritzBoxBaseEntity
@@ -35,6 +36,101 @@ _LOGGER = logging.getLogger(__name__)
# Set a sane value to avoid too many updates
PARALLEL_UPDATES = 5
WIFI_STANDARD = {1: "2.4Ghz", 2: "5Ghz", 3: "5Ghz", 4: "Guest"}
WIFI_BAND = {
0: {"band": "2.4Ghz"},
1: {"band": "5Ghz"},
3: {"band": "5Ghz High / 6Ghz"},
}
def _wifi_naming(
network_info: dict[str, Any], wifi_index: int, wifi_count: int
) -> str | None:
"""Return a friendly name for a Wi-Fi network."""
if wifi_index == 2 and wifi_count == 4:
# In case of 4 Wi-Fi networks, the 2nd one is used for internal communication
# between mesh devices and should not be named like the others to avoid confusion
return None
if (wifi_index + 1) == wifi_count:
# Last Wi-Fi network in the guest network, both bands available
return "Guest"
# Cast to correct type for type checker
if (result := WIFI_BAND.get(wifi_index)) is not None:
return f"Main {result['band']}"
return None
async def _get_wifi_networks_list(avm_wrapper: AvmWrapper) -> dict[int, dict[str, Any]]:
"""Get a list of wifi networks with friendly names."""
wifi_count = len(
[
s
for s in avm_wrapper.connection.services
if s.startswith("WLANConfiguration")
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
if (switch_name := _wifi_naming(network_info, i - 1, wifi_count)) is None:
continue
networks[i] = network_info
networks[i]["switch_name"] = switch_name
_LOGGER.debug("WiFi networks list: %s", networks)
return networks
async def _migrate_to_new_unique_id(
hass: HomeAssistant, avm_wrapper: AvmWrapper
) -> None:
"""Migrate old unique ids to new unique ids."""
_LOGGER.debug("Migrating Wi-Fi switches")
entity_registry = er.async_get(hass)
networks = await _get_wifi_networks_list(avm_wrapper)
for index, network in networks.items():
description = f"Wi-Fi {network['NewSSID']}"
if (
len(
[
j
for j, n in networks.items()
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
]
)
> 1
):
description += f" ({WIFI_STANDARD[index]})"
old_unique_id = f"{avm_wrapper.unique_id}-{slugify(description)}"
new_unique_id = f"{avm_wrapper.unique_id}-wi_fi_{slugify(_wifi_naming(network, index - 1, len(networks)))}"
entity_id = entity_registry.async_get_entity_id(
Platform.SWITCH, DOMAIN, old_unique_id
)
if entity_id is not None:
entity_registry.async_update_entity(
entity_id,
new_unique_id=new_unique_id,
)
_LOGGER.debug(
"Migrating Wi-FI switch unique_id from [%s] to [%s]",
old_unique_id,
new_unique_id,
)
_LOGGER.debug("Migration completed")
async def _async_deflection_entities_list(
avm_wrapper: AvmWrapper, device_friendly_name: str
@@ -125,35 +221,7 @@ async def _async_wifi_entities_list(
#
# https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf
#
wifi_count = len(
[
s
for s in avm_wrapper.connection.services
if s.startswith("WLANConfiguration")
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2):
networks[i] = network_info
for i, network in networks.copy().items():
networks[i]["switch_name"] = network["NewSSID"]
if (
len(
[
j
for j, n in networks.items()
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
]
)
> 1
):
networks[i]["switch_name"] += f" ({WIFI_STANDARD[i]})"
_LOGGER.debug("WiFi networks list: %s", networks)
networks = await _get_wifi_networks_list(avm_wrapper)
return [
FritzBoxWifiSwitch(avm_wrapper, device_friendly_name, index, data)
for index, data in networks.items()
@@ -225,6 +293,8 @@ async def async_setup_entry(
local_ip = await async_get_source_ip(avm_wrapper.hass, target_ip=avm_wrapper.host)
await _migrate_to_new_unique_id(hass, avm_wrapper)
entities_list = await async_all_entities_list(
avm_wrapper,
entry.title,
@@ -554,8 +624,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
)
self._network_num = network_num
description = f"Wi-Fi {network_data['switch_name']}"
self._attr_translation_key = slugify(description)
switch_info = SwitchInfo(
description=f"Wi-Fi {network_data['switch_name']}",
description=description,
friendly_name=device_friendly_name,
icon="mdi:wifi",
type=SWITCH_TYPE_WIFINETWORK,
+9
View File
@@ -106,6 +106,15 @@ class FritzConnectionMock:
return action_data
def wifi_services_with_ssids(ssid_1: str, ssid_2: str) -> dict[str, dict[str, Any]]:
"""Return Fritz services with overridden Wi-Fi SSIDs."""
services = deepcopy(MOCK_FB_SERVICES)
for index, ssid in enumerate((ssid_1, ssid_2), start=1):
services[f"WLANConfiguration{index}"]["GetInfo"]["NewSSID"] = ssid
services[f"WLANConfiguration{index}"]["GetSSID"]["NewSSID"] = ssid
return services
@pytest.fixture(name="fc_data")
def fc_data_mock() -> dict[str, dict[str, Any]]:
"""Fixture for default fc_data."""
@@ -101,7 +101,7 @@
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_wifi_2_4ghz-entry]
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_guest-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -115,7 +115,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_wifi_2_4ghz',
'entity_id': 'switch.mock_title_wi_fi_guest',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -123,36 +123,36 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi WiFi (2.4Ghz)',
'object_id_base': 'Mock Title Wi-Fi Guest',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi WiFi (2.4Ghz)',
'original_name': 'Mock Title Wi-Fi Guest',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_2_4ghz',
'translation_key': 'wi_fi_guest',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_guest',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_wifi_2_4ghz-state]
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_guest-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi WiFi (2.4Ghz)',
'friendly_name': 'Mock Title Wi-Fi Guest',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_wifi_2_4ghz',
'entity_id': 'switch.mock_title_wi_fi_guest',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_wifi_5ghz-entry]
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_main_2_4ghz-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -166,7 +166,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_wifi_5ghz',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -174,29 +174,29 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi WiFi (5Ghz)',
'object_id_base': 'Mock Title Wi-Fi Main 2.4Ghz',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi WiFi (5Ghz)',
'original_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_5ghz',
'translation_key': 'wi_fi_main_2_4ghz',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_main_2_4ghz',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_wifi_5ghz-state]
# name: test_switch_setup[fc_data0][switch.mock_title_wi_fi_main_2_4ghz-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi WiFi (5Ghz)',
'friendly_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_wifi_5ghz',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -355,7 +355,7 @@
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_wifi-entry]
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_guest-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -369,7 +369,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_wifi',
'entity_id': 'switch.mock_title_wi_fi_guest',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -377,36 +377,36 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi WiFi',
'object_id_base': 'Mock Title Wi-Fi Guest',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi WiFi',
'original_name': 'Mock Title Wi-Fi Guest',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi',
'translation_key': 'wi_fi_guest',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_guest',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_wifi-state]
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_guest-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi WiFi',
'friendly_name': 'Mock Title Wi-Fi Guest',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_wifi',
'entity_id': 'switch.mock_title_wi_fi_guest',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_wifi2-entry]
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_main_2_4ghz-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -420,7 +420,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_wifi2',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -428,29 +428,29 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi WiFi2',
'object_id_base': 'Mock Title Wi-Fi Main 2.4Ghz',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi WiFi2',
'original_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi2',
'translation_key': 'wi_fi_main_2_4ghz',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_main_2_4ghz',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_wifi2-state]
# name: test_switch_setup[fc_data1][switch.mock_title_wi_fi_main_2_4ghz-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi WiFi2',
'friendly_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_wifi2',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -609,7 +609,7 @@
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_wifi_2_4ghz-entry]
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_guest-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -623,7 +623,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_wifi_2_4ghz',
'entity_id': 'switch.mock_title_wi_fi_guest',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -631,36 +631,36 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi WiFi (2.4Ghz)',
'object_id_base': 'Mock Title Wi-Fi Guest',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi WiFi (2.4Ghz)',
'original_name': 'Mock Title Wi-Fi Guest',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_2_4ghz',
'translation_key': 'wi_fi_guest',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_guest',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_wifi_2_4ghz-state]
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_guest-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi WiFi (2.4Ghz)',
'friendly_name': 'Mock Title Wi-Fi Guest',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_wifi_2_4ghz',
'entity_id': 'switch.mock_title_wi_fi_guest',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_wifi_5ghz-entry]
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_main_2_4ghz-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -674,7 +674,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_wifi_5ghz',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -682,29 +682,29 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi WiFi+ (5Ghz)',
'object_id_base': 'Mock Title Wi-Fi Main 2.4Ghz',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi WiFi+ (5Ghz)',
'original_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_5ghz',
'translation_key': 'wi_fi_main_2_4ghz',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_main_2_4ghz',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_wifi_5ghz-state]
# name: test_switch_setup[fc_data2][switch.mock_title_wi_fi_main_2_4ghz-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi WiFi+ (5Ghz)',
'friendly_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_wifi_5ghz',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -920,7 +920,7 @@
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_guestwifi-entry]
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_guest-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -934,7 +934,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_guestwifi',
'entity_id': 'switch.mock_title_wi_fi_guest',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -942,36 +942,36 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi GuestWifi',
'object_id_base': 'Mock Title Wi-Fi Guest',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi GuestWifi',
'original_name': 'Mock Title Wi-Fi Guest',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_guestwifi',
'translation_key': 'wi_fi_guest',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_guest',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_guestwifi-state]
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_guest-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi GuestWifi',
'friendly_name': 'Mock Title Wi-Fi Guest',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_guestwifi',
'entity_id': 'switch.mock_title_wi_fi_guest',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_mywifi-entry]
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_main_2_4ghz-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -985,7 +985,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_wi_fi_mywifi',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
@@ -993,29 +993,29 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Mock Title Wi-Fi MyWifi',
'object_id_base': 'Mock Title Wi-Fi Main 2.4Ghz',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:wifi',
'original_name': 'Mock Title Wi-Fi MyWifi',
'original_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'platform': 'fritz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '1C:ED:6F:12:34:11-wi_fi_mywifi',
'translation_key': 'wi_fi_main_2_4ghz',
'unique_id': '1C:ED:6F:12:34:11-wi_fi_main_2_4ghz',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_mywifi-state]
# name: test_switch_setup[fc_data3][switch.mock_title_wi_fi_main_2_4ghz-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Wi-Fi MyWifi',
'friendly_name': 'Mock Title Wi-Fi Main 2.4Ghz',
'icon': 'mdi:wifi',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_wi_fi_mywifi',
'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
+142 -4
View File
@@ -3,13 +3,14 @@
from __future__ import annotations
from copy import deepcopy
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, 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 import switch as fritz_switch
from homeassistant.components.fritz.const import DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
@@ -25,13 +26,16 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import slugify
from .conftest import FritzConnectionMock
from .conftest import FritzConnectionMock, wifi_services_with_ssids
from .const import (
MOCK_CALL_DEFLECTION_DATA,
MOCK_FB_SERVICES,
MOCK_HOST_ATTRIBUTES_DATA,
MOCK_MESH_MASTER_MAC,
MOCK_USER_DATA,
)
@@ -397,7 +401,7 @@ async def test_switch_device_no_ip_address(
STATE_ON,
),
(
"switch.mock_title_wi_fi_mywifi",
"switch.mock_title_wi_fi_guest",
"async_set_wlan_configuration",
STATE_ON,
),
@@ -455,3 +459,137 @@ async def test_switch_turn_on_off(
assert (state := hass.states.get(entity_id))
assert state.state == state_value
@pytest.mark.parametrize(
("ssid_1", "ssid_2", "old_descriptions", "new_identifiers"),
[
(
"Main WiFi / +",
"Guest WiFi / +",
[
"Wi-Fi Main WiFi / +",
"Wi-Fi Guest WiFi / +",
],
["main_2_4ghz", "guest"],
),
(
"My WiFi / +",
"My WiFi / +",
[
"Wi-Fi My WiFi / + (2.4Ghz)",
"Wi-Fi My WiFi / + (5Ghz)",
],
["main_2_4ghz", "guest"],
),
],
)
async def test_migrate_to_new_unique_id(
hass: HomeAssistant,
fc_class_mock,
fh_class_mock,
fs_class_mock,
entity_registry: EntityRegistry,
device_registry: dr.DeviceRegistry,
ssid_1: str,
ssid_2: str,
old_descriptions: list[str],
new_identifiers: list[str],
) -> None:
"""Test migrate from old unique ids to new unique ids."""
MOCK_UNIQUE_ID = "1234567890"
fc_class_mock.return_value.override_services(
wifi_services_with_ssids(ssid_1, ssid_2)
)
entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_USER_DATA, unique_id=MOCK_UNIQUE_ID
)
entry.add_to_hass(hass)
entity_ids: list[str] = []
old_unique_ids: list[str] = []
new_unique_ids: list[str] = []
for old_description, new_identifier in zip(
old_descriptions, new_identifiers, strict=True
):
old_unique_id = f"{MOCK_MESH_MASTER_MAC}-{slugify(old_description)}"
new_unique_id = f"{MOCK_MESH_MASTER_MAC}-wi_fi_{new_identifier}"
old_unique_ids.append(old_unique_id)
new_unique_ids.append(new_unique_id)
entity_ids.append(f"switch.fritz_{slugify(old_unique_id)}")
entity_registry.async_get_or_create(
disabled_by=None,
domain=SWITCH_DOMAIN,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=entry,
)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, MOCK_UNIQUE_ID)},
connections={
(dr.CONNECTION_NETWORK_MAC, MOCK_MESH_MASTER_MAC),
},
)
await hass.async_block_till_done()
for entity_id, old_unique_id in zip(entity_ids, old_unique_ids, strict=True):
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry
assert entity_entry.unique_id == old_unique_id
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
for entity_id, new_unique_id in zip(entity_ids, new_unique_ids, strict=True):
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry
assert entity_entry.unique_id == new_unique_id
async def test_wifi_naming_internal_comm_and_skipped() -> None:
"""Test skip internal Wi-Fi network."""
# Prepare AvmWrapper mock with 4 Wi-Fi networks
avm_wrapper = MagicMock()
avm_wrapper.connection.services = [
"WLANConfiguration1",
"WLANConfiguration2",
"WLANConfiguration3",
"WLANConfiguration4",
]
# The 3rd network (index 2) should be skipped
wifi_configs = [
{"NewSSID": "wifi1"},
{"NewSSID": "wifi2"},
{"NewSSID": "wifi3"},
{"NewSSID": "wifi4"},
]
avm_wrapper.async_get_wlan_configuration = AsyncMock(side_effect=wifi_configs)
networks = await fritz_switch._get_wifi_networks_list(avm_wrapper)
# The 3rd network (index 2) should be skipped
assert 3 not in networks # 1-based index, so 3 is the 3rd
# The rest should be present
assert set(networks.keys()) == {1, 2, 4}
@pytest.mark.parametrize(
("wifi_index", "wifi_count", "expected_name"),
[
(0, 2, "Main 2.4Ghz"),
(1, 3, "Main 5Ghz"),
(1, 2, "Guest"),
(2, 4, None),
(2, 5, None),
],
)
def test_wifi_naming_helper(
wifi_index: int, wifi_count: int, expected_name: str | None
) -> None:
"""Test Wi-Fi naming helper covers supported and fallback branches."""
assert fritz_switch._wifi_naming({}, wifi_index, wifi_count) == expected_name