Add diagnostics support for Nederlandse Spoorwegen integration (#158722)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Heindrich Paul
2025-12-12 14:36:35 +01:00
committed by GitHub
parent a94678cb06
commit 10f6d8d14f
3 changed files with 264 additions and 0 deletions
@@ -0,0 +1,122 @@
"""Diagnostics support for Nederlandse Spoorwegen."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN
from .coordinator import NSConfigEntry
TO_REDACT = [
CONF_API_KEY,
]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: NSConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators_data = {}
# Collect data from all coordinators
for subentry_id, coordinator in entry.runtime_data.items():
coordinators_data[subentry_id] = {
"coordinator_info": {
"name": coordinator.name,
"departure": coordinator.departure,
"destination": coordinator.destination,
"via": coordinator.via,
"departure_time": coordinator.departure_time,
},
"route_data": {
"trips_count": len(coordinator.data.trips) if coordinator.data else 0,
"has_first_trip": coordinator.data.first_trip is not None
if coordinator.data
else False,
"has_next_trip": coordinator.data.next_trip is not None
if coordinator.data
else False,
}
if coordinator.data
else None,
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"coordinators": coordinators_data,
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: NSConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a route."""
# Find the coordinator for this device
coordinator = None
subentry_id = None
# Each device has an identifier (DOMAIN, subentry_id)
for identifier in device.identifiers:
if identifier[0] == DOMAIN:
subentry_id = identifier[1]
coordinator = entry.runtime_data.get(subentry_id)
break
# Collect detailed diagnostics for this specific route
device_data = {
"device_info": {
"subentry_id": subentry_id,
"device_name": device.name,
"manufacturer": device.manufacturer,
"model": device.model,
},
"coordinator_info": {
"name": coordinator.name,
"departure": coordinator.departure,
"destination": coordinator.destination,
"via": coordinator.via,
"departure_time": coordinator.departure_time,
}
if coordinator
else None,
}
# Add detailed trip data if available
if coordinator and coordinator.data:
device_data["trip_details"] = {
"trips_count": len(coordinator.data.trips),
"has_first_trip": coordinator.data.first_trip is not None,
"has_next_trip": coordinator.data.next_trip is not None,
}
# Add first trip details if available
if coordinator.data.first_trip:
first_trip = coordinator.data.first_trip
device_data["first_trip"] = {
"departure_time_planned": str(first_trip.departure_time_planned)
if first_trip.departure_time_planned
else None,
"departure_time_actual": str(first_trip.departure_time_actual)
if first_trip.departure_time_actual
else None,
"arrival_time_planned": str(first_trip.arrival_time_planned)
if first_trip.arrival_time_planned
else None,
"arrival_time_actual": str(first_trip.arrival_time_actual)
if first_trip.arrival_time_actual
else None,
"departure_platform_planned": first_trip.departure_platform_planned,
"departure_platform_actual": first_trip.departure_platform_actual,
"arrival_platform_planned": first_trip.arrival_platform_planned,
"arrival_platform_actual": first_trip.arrival_platform_actual,
"status": str(first_trip.status) if first_trip.status else None,
"nr_transfers": first_trip.nr_transfers,
"going": first_trip.going,
}
return device_data
@@ -0,0 +1,73 @@
# serializer version: 1
# name: test_device_diagnostics
dict({
'coordinator_info': dict({
'departure': 'Ams',
'departure_time': None,
'destination': 'Rot',
'name': 'To work',
'via': 'Ht',
}),
'device_info': dict({
'device_name': 'To work',
'manufacturer': 'Nederlandse Spoorwegen',
'model': 'Route',
'subentry_id': '01K721DZPMEN39R5DK0ATBMSY8',
}),
'first_trip': dict({
'arrival_platform_actual': '12',
'arrival_platform_planned': '12',
'arrival_time_actual': '2025-09-15 18:37:00+02:00',
'arrival_time_planned': '2025-09-15 18:37:00+02:00',
'departure_platform_actual': '4',
'departure_platform_planned': '4',
'departure_time_actual': '2025-09-15 16:35:00+02:00',
'departure_time_planned': '2025-09-15 16:34:00+02:00',
'going': True,
'nr_transfers': 2,
'status': 'NORMAL',
}),
'trip_details': dict({
'has_first_trip': True,
'has_next_trip': True,
'trips_count': 8,
}),
})
# ---
# name: test_entry_diagnostics
dict({
'coordinators': dict({
'01K721DZPMEN39R5DK0ATBMSY8': dict({
'coordinator_info': dict({
'departure': 'Ams',
'departure_time': None,
'destination': 'Rot',
'name': 'To work',
'via': 'Ht',
}),
'route_data': dict({
'has_first_trip': True,
'has_next_trip': True,
'trips_count': 8,
}),
}),
'01K721DZPMEN39R5DK0ATBMSY9': dict({
'coordinator_info': dict({
'departure': 'Hag',
'departure_time': '08:00',
'destination': 'Utr',
'name': 'To home',
'via': None,
}),
'route_data': dict({
'has_first_trip': True,
'has_next_trip': True,
'trips_count': 8,
}),
}),
}),
'entry_data': dict({
'api_key': '**REDACTED**',
}),
})
# ---
@@ -0,0 +1,69 @@
"""Tests for the diagnostics data provided by the Nederlandse Spoorwegen integration."""
from unittest.mock import AsyncMock
import pytest
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from homeassistant.components.nederlandse_spoorwegen.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import SUBENTRY_ID_1
from tests.common import MockConfigEntry
from tests.components.diagnostics import (
get_diagnostics_for_config_entry,
get_diagnostics_for_device,
)
from tests.typing import ClientSessionGenerator
@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00")
async def test_entry_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
mock_nsapi: AsyncMock,
) -> None:
"""Test config entry diagnostics."""
mock_config_entry.add_to_hass(hass)
await setup_integration(hass, mock_config_entry)
# Trigger update for all coordinators before diagnostics
for coordinator in mock_config_entry.runtime_data.values():
await coordinator.async_refresh()
result = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
assert result == snapshot(exclude=props("created_at", "modified_at"))
@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00")
async def test_device_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
mock_nsapi: AsyncMock,
) -> None:
"""Test device diagnostics."""
# Ensure integration is set up so device exists
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device(identifiers={(DOMAIN, SUBENTRY_ID_1)})
assert device is not None
# Trigger update for the coordinator before diagnostics
coordinator = mock_config_entry.runtime_data[SUBENTRY_ID_1]
await coordinator.async_refresh()
result = await get_diagnostics_for_device(
hass, hass_client, mock_config_entry, device
)
assert result == snapshot(exclude=props("created_at", "modified_at"))