diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 604d3f94bf98..032efb3f4ae9 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -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" diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 73bf2cbffbfc..22cdd12bd20f 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -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" } } }, diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 61255e27a4df..dd91c1a966ba 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -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, diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 5d32c4705e77..65abfc3a4a0a 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -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.""" diff --git a/tests/components/fritz/snapshots/test_switch.ambr b/tests/components/fritz/snapshots/test_switch.ambr index 8789576d970c..9002c780243a 100644 --- a/tests/components/fritz/snapshots/test_switch.ambr +++ b/tests/components/fritz/snapshots/test_switch.ambr @@ -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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_wifi_2_4ghz', + 'entity_id': 'switch.mock_title_wi_fi_guest', 'last_changed': , 'last_reported': , 'last_updated': , '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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_wifi_5ghz', + 'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz', 'last_changed': , 'last_reported': , 'last_updated': , @@ -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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_wifi', + 'entity_id': 'switch.mock_title_wi_fi_guest', 'last_changed': , 'last_reported': , 'last_updated': , '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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_wifi2', + 'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz', 'last_changed': , 'last_reported': , 'last_updated': , @@ -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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_wifi_2_4ghz', + 'entity_id': 'switch.mock_title_wi_fi_guest', 'last_changed': , 'last_reported': , 'last_updated': , '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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_wifi_5ghz', + 'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz', 'last_changed': , 'last_reported': , 'last_updated': , @@ -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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_guestwifi', + 'entity_id': 'switch.mock_title_wi_fi_guest', 'last_changed': , 'last_reported': , 'last_updated': , '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': , - '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': , - 'entity_id': 'switch.mock_title_wi_fi_mywifi', + 'entity_id': 'switch.mock_title_wi_fi_main_2_4ghz', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index df32344ee04f..895b738451a8 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -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