diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py new file mode 100644 index 00000000000..ce296499398 --- /dev/null +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -0,0 +1,79 @@ +"""Diagnostics platform for Traccar Server.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import DOMAIN +from .coordinator import TraccarServerCoordinator + +TO_REDACT = {CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entity_registry = er.async_get(hass) + + entities = er.async_entries_for_config_entry( + entity_registry, + config_entry_id=config_entry.entry_id, + ) + + return async_redact_data( + { + "config_entry_options": dict(config_entry.options), + "coordinator_data": coordinator.data, + "entities": [ + { + "enity_id": entity.entity_id, + "disabled": entity.disabled, + "state": {"state": state.state, "attributes": state.attributes}, + } + for entity in entities + if (state := hass.states.get(entity.entity_id)) is not None + ], + }, + TO_REDACT, + ) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, + device: dr.DeviceEntry, +) -> dict[str, Any]: + """Return device diagnostics.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id] + entity_registry = er.async_get(hass) + + entities = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + + return async_redact_data( + { + "config_entry_options": dict(entry.options), + "coordinator_data": coordinator.data, + "entities": [ + { + "enity_id": entity.entity_id, + "disabled": entity.disabled, + "state": {"state": state.state, "attributes": state.attributes}, + } + for entity in entities + if (state := hass.states.get(entity.entity_id)) is not None + ], + }, + TO_REDACT, + ) diff --git a/tests/components/traccar_server/common.py b/tests/components/traccar_server/common.py new file mode 100644 index 00000000000..b85f7b672f8 --- /dev/null +++ b/tests/components/traccar_server/common.py @@ -0,0 +1,11 @@ +"""Common test tools for Traccar Server.""" +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the integration.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..0492291384d --- /dev/null +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -0,0 +1,199 @@ +# serializer version: 1 +# name: test_device_diagnostics[X-Wing] + dict({ + 'config_entry_options': dict({ + 'custom_attributes': list([ + 'custom_attr_1', + ]), + 'events': list([ + 'device_moving', + ]), + 'max_accuracy': 5.0, + 'skip_accuracy_filter_for': list([ + ]), + }), + 'coordinator_data': dict({ + 'abc123': dict({ + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'device': dict({ + 'attributes': dict({ + }), + 'category': 'starfighter', + 'contact': None, + 'disabled': False, + 'groupId': 0, + 'id': 0, + 'lastUpdate': '1970-01-01T00:00:00Z', + 'model': '1337', + 'name': 'X-Wing', + 'phone': None, + 'positionId': 0, + 'status': 'unknown', + 'uniqueId': 'abc123', + }), + 'geofence': dict({ + 'area': 'string', + 'attributes': dict({ + }), + 'calendarId': 0, + 'description': "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + 'id': 0, + 'name': 'Tatooine', + }), + 'position': dict({ + 'accuracy': 3.5, + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'course': 360, + 'deviceId': 0, + 'deviceTime': '1970-01-01T00:00:00Z', + 'fixTime': '1970-01-01T00:00:00Z', + 'geofenceIds': list([ + 0, + ]), + 'id': 0, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'network': dict({ + }), + 'outdated': True, + 'protocol': 'C-3PO', + 'serverTime': '1970-01-01T00:00:00Z', + 'speed': 4568795, + 'valid': True, + }), + }), + }), + 'entities': list([ + dict({ + 'disabled': False, + 'enity_id': 'device_tracker.x_wing', + 'state': dict({ + 'attributes': dict({ + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'battery_level': -1, + 'category': 'starfighter', + 'custom_attr_1': 'custom_attr_1_value', + 'friendly_name': 'X-Wing', + 'geofence': 'Tatooine', + 'gps_accuracy': 3.5, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'motion': False, + 'source_type': 'gps', + 'speed': 4568795, + 'status': 'unknown', + 'traccar_id': 0, + 'tracker': 'traccar_server', + }), + 'state': 'not_home', + }), + }), + ]), + }) +# --- +# name: test_entry_diagnostics[entry] + dict({ + 'config_entry_options': dict({ + 'custom_attributes': list([ + 'custom_attr_1', + ]), + 'events': list([ + 'device_moving', + ]), + 'max_accuracy': 5.0, + 'skip_accuracy_filter_for': list([ + ]), + }), + 'coordinator_data': dict({ + 'abc123': dict({ + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'device': dict({ + 'attributes': dict({ + }), + 'category': 'starfighter', + 'contact': None, + 'disabled': False, + 'groupId': 0, + 'id': 0, + 'lastUpdate': '1970-01-01T00:00:00Z', + 'model': '1337', + 'name': 'X-Wing', + 'phone': None, + 'positionId': 0, + 'status': 'unknown', + 'uniqueId': 'abc123', + }), + 'geofence': dict({ + 'area': 'string', + 'attributes': dict({ + }), + 'calendarId': 0, + 'description': "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + 'id': 0, + 'name': 'Tatooine', + }), + 'position': dict({ + 'accuracy': 3.5, + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'course': 360, + 'deviceId': 0, + 'deviceTime': '1970-01-01T00:00:00Z', + 'fixTime': '1970-01-01T00:00:00Z', + 'geofenceIds': list([ + 0, + ]), + 'id': 0, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'network': dict({ + }), + 'outdated': True, + 'protocol': 'C-3PO', + 'serverTime': '1970-01-01T00:00:00Z', + 'speed': 4568795, + 'valid': True, + }), + }), + }), + 'entities': list([ + dict({ + 'disabled': False, + 'enity_id': 'device_tracker.x_wing', + 'state': dict({ + 'attributes': dict({ + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'battery_level': -1, + 'category': 'starfighter', + 'custom_attr_1': 'custom_attr_1_value', + 'friendly_name': 'X-Wing', + 'geofence': 'Tatooine', + 'gps_accuracy': 3.5, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'motion': False, + 'source_type': 'gps', + 'speed': 4568795, + 'status': 'unknown', + 'traccar_id': 0, + 'tracker': 'traccar_server', + }), + 'state': 'not_home', + }), + }), + ]), + }) +# --- diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py new file mode 100644 index 00000000000..ebefaab6df8 --- /dev/null +++ b/tests/components/traccar_server/test_diagnostics.py @@ -0,0 +1,64 @@ +"""Test Traccar Server diagnostics.""" +from collections.abc import Generator +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .common import setup_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_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) + + result = await get_diagnostics_for_config_entry( + hass, + hass_client, + mock_config_entry, + ) + + assert result == snapshot(name="entry") + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device diagnostics.""" + await setup_integration(hass, mock_config_entry) + + devices = dr.async_entries_for_config_entry( + hass.helpers.device_registry.async_get(hass), + mock_config_entry.entry_id, + ) + + assert len(devices) == 1 + + for device in dr.async_entries_for_config_entry( + hass.helpers.device_registry.async_get(hass), mock_config_entry.entry_id + ): + result = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, device=device + ) + + assert result == snapshot(name=device.name)