diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 7638b14c111..a74656eef11 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -211,6 +211,10 @@ class FibaroController: """Return list of scenes.""" return self._scenes + def get_all_devices(self) -> list[DeviceModel]: + """Return list of all fibaro devices.""" + return self._fibaro_device_manager.get_devices() + def read_fibaro_info(self) -> InfoModel: """Return the general info about the hub.""" return self._fibaro_info diff --git a/homeassistant/components/fibaro/diagnostics.py b/homeassistant/components/fibaro/diagnostics.py new file mode 100644 index 00000000000..2f1f397a69a --- /dev/null +++ b/homeassistant/components/fibaro/diagnostics.py @@ -0,0 +1,56 @@ +"""Diagnostics support for fibaro integration.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from pyfibaro.fibaro_device import DeviceModel + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from . import CONF_IMPORT_PLUGINS, FibaroConfigEntry + +TO_REDACT = {"password"} + + +def _create_diagnostics_data( + config_entry: FibaroConfigEntry, devices: list[DeviceModel] +) -> dict[str, Any]: + """Combine diagnostics information and redact sensitive information.""" + return { + "config": {CONF_IMPORT_PLUGINS: config_entry.data.get(CONF_IMPORT_PLUGINS)}, + "fibaro_devices": async_redact_data([d.raw_data for d in devices], TO_REDACT), + } + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: FibaroConfigEntry +) -> Mapping[str, Any]: + """Return diagnostics for a config entry.""" + controller = config_entry.runtime_data + devices = controller.get_all_devices() + return _create_diagnostics_data(config_entry, devices) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: FibaroConfigEntry, device: DeviceEntry +) -> Mapping[str, Any]: + """Return diagnostics for a device.""" + controller = config_entry.runtime_data + devices = controller.get_all_devices() + + ha_device_id = next(iter(device.identifiers))[1] + if ha_device_id == controller.hub_serial: + # special case where the device is representing the fibaro hub + return _create_diagnostics_data(config_entry, devices) + + # normal devices are represented by a parent / child structure + filtered_devices = [ + device + for device in devices + if ha_device_id in (device.fibaro_id, device.parent_fibaro_id) + ] + return _create_diagnostics_data(config_entry, filtered_devices) diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index 9e7c2f6c003..53cecd78bb6 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -70,6 +70,11 @@ def mock_power_sensor() -> Mock: } sensor.actions = {} sensor.has_central_scene_event = False + sensor.raw_data = { + "fibaro_id": 1, + "name": "Test sensor", + "properties": {"power": 6.6, "password": "mysecret"}, + } value_mock = Mock() value_mock.has_value = False value_mock.is_bool_value = False @@ -123,6 +128,7 @@ def mock_light() -> Mock: light.properties = {"manufacturer": ""} light.actions = {"setValue": 1, "on": 0, "off": 0} light.supported_features = {} + light.raw_data = {"fibaro_id": 3, "name": "Test light", "properties": {"value": 20}} value_mock = Mock() value_mock.has_value = True value_mock.int_value.return_value = 20 diff --git a/tests/components/fibaro/snapshots/test_diagnostics.ambr b/tests/components/fibaro/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..e9d5e48e08c --- /dev/null +++ b/tests/components/fibaro/snapshots/test_diagnostics.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_config_entry_diagnostics + dict({ + 'config': dict({ + 'import_plugins': True, + }), + 'fibaro_devices': list([ + dict({ + 'fibaro_id': 3, + 'name': 'Test light', + 'properties': dict({ + 'value': 20, + }), + }), + ]), + }) +# --- +# name: test_device_diagnostics + dict({ + 'config': dict({ + 'import_plugins': True, + }), + 'fibaro_devices': list([ + dict({ + 'fibaro_id': 3, + 'name': 'Test light', + 'properties': dict({ + 'value': 20, + }), + }), + ]), + }) +# --- +# name: test_device_diagnostics_for_hub + dict({ + 'config': dict({ + 'import_plugins': True, + }), + 'fibaro_devices': list([ + dict({ + 'fibaro_id': 3, + 'name': 'Test light', + 'properties': dict({ + 'value': 20, + }), + }), + dict({ + 'fibaro_id': 1, + 'name': 'Test sensor', + 'properties': dict({ + 'password': '**REDACTED**', + 'power': 6.6, + }), + }), + ]), + }) +# --- diff --git a/tests/components/fibaro/test_diagnostics.py b/tests/components/fibaro/test_diagnostics.py new file mode 100644 index 00000000000..c6148e0cc33 --- /dev/null +++ b/tests/components/fibaro/test_diagnostics.py @@ -0,0 +1,96 @@ +"""Tests for the diagnostics data provided by the fibaro integration.""" + +from unittest.mock import Mock + +from syrupy import SnapshotAssertion + +from homeassistant.components.fibaro import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .conftest import TEST_SERIALNUMBER, init_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.typing import ClientSessionGenerator + + +async def test_config_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_light: Mock, + mock_room: Mock, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_light] + # Act + await init_integration(hass, mock_config_entry) + # Assert + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_light: Mock, + mock_room: Mock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_light] + # Act + await init_integration(hass, mock_config_entry) + entry = entity_registry.async_get("light.room_1_test_light_3") + device = device_registry.async_get(entry.device_id) + # Assert + assert device + assert ( + await get_diagnostics_for_device(hass, hass_client, mock_config_entry, device) + == snapshot + ) + + +async def test_device_diagnostics_for_hub( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_light: Mock, + mock_power_sensor: Mock, + mock_room: Mock, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics for the hub.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_light, mock_power_sensor] + # Act + await init_integration(hass, mock_config_entry) + device = device_registry.async_get_device({(DOMAIN, TEST_SERIALNUMBER)}) + # Assert + assert device + assert ( + await get_diagnostics_for_device(hass, hass_client, mock_config_entry, device) + == snapshot + )