Fix integration failed when freebox is configured in bridge mode (#103221)

This commit is contained in:
jflefebvre06 2023-11-19 00:02:00 +01:00 committed by GitHub
parent c2e81bbafb
commit 9c86adf644
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 5 deletions

View File

@ -425,9 +425,7 @@ omit =
homeassistant/components/foursquare/* homeassistant/components/foursquare/*
homeassistant/components/free_mobile/notify.py homeassistant/components/free_mobile/notify.py
homeassistant/components/freebox/camera.py homeassistant/components/freebox/camera.py
homeassistant/components/freebox/device_tracker.py
homeassistant/components/freebox/home_base.py homeassistant/components/freebox/home_base.py
homeassistant/components/freebox/router.py
homeassistant/components/freebox/switch.py homeassistant/components/freebox/switch.py
homeassistant/components/fritz/common.py homeassistant/components/fritz/common.py
homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/device_tracker.py

View File

@ -4,9 +4,11 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from contextlib import suppress from contextlib import suppress
from datetime import datetime from datetime import datetime
import json
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
import re
from typing import Any from typing import Any
from freebox_api import Freepybox from freebox_api import Freepybox
@ -36,6 +38,20 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def is_json(json_str):
"""Validate if a String is a JSON value or not."""
try:
json.loads(json_str)
return True
except (ValueError, TypeError) as err:
_LOGGER.error(
"Failed to parse JSON '%s', error '%s'",
json_str,
err,
)
return False
async def get_api(hass: HomeAssistant, host: str) -> Freepybox: async def get_api(hass: HomeAssistant, host: str) -> Freepybox:
"""Get the Freebox API.""" """Get the Freebox API."""
freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path
@ -69,6 +85,7 @@ class FreeboxRouter:
self._sw_v: str = freebox_config["firmware_version"] self._sw_v: str = freebox_config["firmware_version"]
self._attrs: dict[str, Any] = {} self._attrs: dict[str, Any] = {}
self.supports_hosts = True
self.devices: dict[str, dict[str, Any]] = {} self.devices: dict[str, dict[str, Any]] = {}
self.disks: dict[int, dict[str, Any]] = {} self.disks: dict[int, dict[str, Any]] = {}
self.supports_raid = True self.supports_raid = True
@ -89,7 +106,32 @@ class FreeboxRouter:
async def update_device_trackers(self) -> None: async def update_device_trackers(self) -> None:
"""Update Freebox devices.""" """Update Freebox devices."""
new_device = False new_device = False
fbx_devices: list[dict[str, Any]] = await self._api.lan.get_hosts_list()
fbx_devices: list[dict[str, Any]] = []
# Access to Host list not available in bridge mode, API return error_code 'nodev'
if self.supports_hosts:
try:
fbx_devices = await self._api.lan.get_hosts_list()
except HttpRequestError as err:
if (
(
matcher := re.search(
r"Request failed \(APIResponse: (.+)\)", str(err)
)
)
and is_json(json_str := matcher.group(1))
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
):
# No need to retry, Host list not available
self.supports_hosts = False
_LOGGER.debug(
"Host list is not available using bridge mode (%s)",
json_resp.get("msg"),
)
else:
raise err
# Adds the Freebox itself # Adds the Freebox itself
fbx_devices.append( fbx_devices.append(

View File

@ -1,6 +1,8 @@
"""Test helpers for Freebox.""" """Test helpers for Freebox."""
import json
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import AsyncMock, PropertyMock, patch
from freebox_api.exceptions import HttpRequestError
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -12,6 +14,7 @@ from .const import (
DATA_HOME_GET_NODES, DATA_HOME_GET_NODES,
DATA_HOME_PIR_GET_VALUES, DATA_HOME_PIR_GET_VALUES,
DATA_LAN_GET_HOSTS_LIST, DATA_LAN_GET_HOSTS_LIST,
DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE,
DATA_STORAGE_GET_DISKS, DATA_STORAGE_GET_DISKS,
DATA_STORAGE_GET_RAIDS, DATA_STORAGE_GET_RAIDS,
DATA_SYSTEM_GET_CONFIG, DATA_SYSTEM_GET_CONFIG,
@ -41,7 +44,9 @@ def enable_all_entities():
@pytest.fixture @pytest.fixture
def mock_device_registry_devices(hass: HomeAssistant, device_registry): def mock_device_registry_devices(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
):
"""Create device registry devices so the device tracker entities are enabled.""" """Create device registry devices so the device tracker entities are enabled."""
config_entry = MockConfigEntry(domain="something_else") config_entry = MockConfigEntry(domain="something_else")
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -87,3 +92,17 @@ def mock_router(mock_device_registry_devices):
) )
instance.close = AsyncMock() instance.close = AsyncMock()
yield service_mock yield service_mock
@pytest.fixture(name="router_bridge_mode")
def mock_router_bridge_mode(mock_device_registry_devices, router):
"""Mock a successful connection to Freebox Bridge mode."""
router().lan.get_hosts_list = AsyncMock(
side_effect=HttpRequestError(
"Request failed (APIResponse: %s)"
% json.dumps(DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE)
)
)
return router

View File

@ -25,7 +25,9 @@ WIFI_GET_GLOBAL_CONFIG = load_json_object_fixture("freebox/wifi_get_global_confi
# device_tracker # device_tracker
DATA_LAN_GET_HOSTS_LIST = load_json_array_fixture("freebox/lan_get_hosts_list.json") DATA_LAN_GET_HOSTS_LIST = load_json_array_fixture("freebox/lan_get_hosts_list.json")
DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE = load_json_object_fixture(
"freebox/lan_get_hosts_list_bridge.json"
)
# Home # Home
# ALL # ALL

View File

@ -0,0 +1,5 @@
{
"msg": "Erreur lors de la récupération de la liste des hôtes : Interface invalide",
"success": false,
"error_code": "nodev"
}

View File

@ -0,0 +1,49 @@
"""Tests for the Freebox device trackers."""
from unittest.mock import Mock
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.freebox import SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from .common import setup_platform
from tests.common import async_fire_time_changed
async def test_router_mode(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
router: Mock,
) -> None:
"""Test get_hosts_list invoqued multiple times if freebox into router mode."""
await setup_platform(hass, DEVICE_TRACKER_DOMAIN)
assert router().lan.get_hosts_list.call_count == 1
# Simulate an update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert router().lan.get_hosts_list.call_count == 2
async def test_bridge_mode(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
router_bridge_mode: Mock,
) -> None:
"""Test get_hosts_list invoqued once if freebox into bridge mode."""
await setup_platform(hass, DEVICE_TRACKER_DOMAIN)
assert router_bridge_mode().lan.get_hosts_list.call_count == 1
# Simulate an update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# If get_hosts_list failed, not called again
assert router_bridge_mode().lan.get_hosts_list.call_count == 1

View File

@ -0,0 +1,22 @@
"""Tests for the Freebox utility methods."""
import json
from homeassistant.components.freebox.router import is_json
from .const import DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE, WIFI_GET_GLOBAL_CONFIG
async def test_is_json() -> None:
"""Test is_json method."""
# Valid JSON values
assert is_json("{}")
assert is_json('{ "simple":"json" }')
assert is_json(json.dumps(WIFI_GET_GLOBAL_CONFIG))
assert is_json(json.dumps(DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE))
# Not valid JSON values
assert not is_json(None)
assert not is_json("")
assert not is_json("XXX")
assert not is_json("{XXX}")