Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions homeassistant/components/duco/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from .const import DOMAIN
from .coordinator import DucoConfigEntry

# MAC addresses and serial numbers are redacted because a Duco installer or
# manufacturer could cross-reference them against an installation registry to
# identify the physical location of the device.
TO_REDACT = {
CONF_HOST,
"mac",
Expand All @@ -31,9 +34,15 @@ async def async_get_config_entry_diagnostics(
coordinator = entry.runtime_data

board = asdict(coordinator.board_info)
# `time` is a Unix epoch timestamp of the last board info fetch; not useful for support triage.
board.pop("time")
if board["public_api_version"] is None:
board.pop("public_api_version")
if board["software_version"] is None:
board.pop("software_version")
Comment thread
ronaldvdmeer marked this conversation as resolved.

try:
api_info_obj = await coordinator.client.async_get_api_info()
lan_info = await coordinator.client.async_get_lan_info()
duco_diags = await coordinator.client.async_get_diagnostics()
write_remaining = await coordinator.client.async_get_write_req_remaining()
Expand All @@ -43,10 +52,15 @@ async def async_get_config_entry_diagnostics(
translation_key="connection_error",
) from err

api_info: dict[str, Any] = {"public_api_version": api_info_obj.public_api_version}
if api_info_obj.reported_api_version is not None:
api_info["reported_api_version"] = api_info_obj.reported_api_version

return async_redact_data(
{
"entry_data": entry.data,
"board_info": board,
"api_info": api_info,
"lan_info": asdict(lan_info),
"nodes": {
str(node_id): asdict(node)
Expand Down
23 changes: 23 additions & 0 deletions tests/components/duco/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from unittest.mock import AsyncMock, patch

from duco.models import (
ApiEndpointInfo,
ApiInfo,
BoardInfo,
DiagComponent,
DiagStatus,
Expand Down Expand Up @@ -49,6 +51,25 @@ def mock_board_info() -> BoardInfo:
serial_duco_box="GHI789",
serial_duco_comm="JKL012",
time=1700000000,
public_api_version="2.5",
software_version="1.2.3",
)


@pytest.fixture
def mock_api_info() -> ApiInfo:
"""Return mock API info."""
return ApiInfo(
api_version="2.5",
reported_api_version="2.5.1",
endpoints=[
ApiEndpointInfo(
url="/info",
query_parameters=["module", "submodule"],
methods=["GET"],
modules=["General", "Diag"],
)
],
)


Expand Down Expand Up @@ -180,6 +201,7 @@ def mock_nodes() -> list[Node]:

@pytest.fixture
def mock_duco_client(
mock_api_info: ApiInfo,
mock_board_info: BoardInfo,
mock_lan_info: LanInfo,
mock_nodes: list[Node],
Expand All @@ -202,6 +224,7 @@ def mock_duco_client(
),
):
client = mock_class.return_value
client.async_get_api_info.return_value = mock_api_info
client.async_get_board_info.return_value = mock_board_info
client.async_get_lan_info.return_value = mock_lan_info
client.async_get_nodes.return_value = mock_nodes
Expand Down
8 changes: 6 additions & 2 deletions tests/components/duco/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# serializer version: 1
# name: test_diagnostics
dict({
'api_info': dict({
'public_api_version': '2.5',
'reported_api_version': '2.5.1',
}),
'board_info': dict({
'box_name': 'SILENT_CONNECT',
'box_sub_type_name': 'Eu',
'public_api_version': None,
'public_api_version': '2.5',
'serial_board_box': '**REDACTED**',
'serial_board_comm': '**REDACTED**',
'serial_duco_box': '**REDACTED**',
'serial_duco_comm': '**REDACTED**',
'software_version': None,
'software_version': '1.2.3',
}),
'duco_diagnostics': list([
dict({
Expand Down
54 changes: 52 additions & 2 deletions tests/components/duco/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Tests for the Duco diagnostics."""

from dataclasses import replace
from http import HTTPStatus
from unittest.mock import AsyncMock

from duco.exceptions import DucoConnectionError
from duco.models import ApiInfo
import pytest
from syrupy.assertion import SnapshotAssertion

Expand All @@ -24,7 +26,7 @@ async def test_diagnostics(
mock_duco_client: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics."""
"""Test that the full diagnostics payload matches the snapshot."""
assert (
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
== snapshot
Expand All @@ -34,7 +36,12 @@ async def test_diagnostics(
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
"failing_method",
["async_get_lan_info", "async_get_diagnostics", "async_get_write_req_remaining"],
[
"async_get_api_info",
"async_get_lan_info",
"async_get_diagnostics",
"async_get_write_req_remaining",
],
)
async def test_diagnostics_connection_error(
hass: HomeAssistant,
Expand All @@ -54,3 +61,46 @@ async def test_diagnostics_connection_error(
f"/api/diagnostics/config_entry/{mock_config_entry.entry_id}"
)
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR


async def test_diagnostics_without_optional_board_metadata(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
) -> None:
"""Test that None board fields are omitted from the diagnostics payload."""
# BoardInfo is a frozen dataclass, so the mock must be updated before
# integration setup — the coordinator stores board_info during async_setup.
mock_duco_client.async_get_board_info.return_value = replace(
mock_duco_client.async_get_board_info.return_value,
public_api_version=None,
software_version=None,
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

diagnostics = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)

assert "public_api_version" not in diagnostics["board_info"]
assert "software_version" not in diagnostics["board_info"]


@pytest.mark.usefixtures("init_integration")
async def test_diagnostics_without_optional_api_metadata(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
) -> None:
"""Test diagnostics when optional API metadata is absent."""
mock_duco_client.async_get_api_info.return_value = ApiInfo(api_version="2.5")

diagnostics = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)

assert diagnostics["api_info"] == {"public_api_version": "2.5"}
Loading