# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. # Copyright (C) 2026 Lukas Bandura # SPDX-License-Identifier: AGPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Diagnostics support for WashData.""" from __future__ import annotations from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN from .manager import WashDataManager # Keys that can identify the user or their home network - redacted in all contexts. _SENSITIVE_KEYS = { "auth", "entry_id", "flow_id", "flow_title", "handler", "name", "source", "title", "unique_id", "user_id", # Community-store account credentials / identifiers. "refresh_token", "id_token", "uid", # HA entity / service references that reveal home topology. "notify_service", "notify_start_services", "notify_finish_services", "notify_live_services", "notify_people", "notify_actions", "power_sensor", "external_end_trigger", "door_sensor_entity", "switch_entity", "energy_price_entity", "energy_sensor", } def _redact(obj: Any) -> Any: if isinstance(obj, dict): return { k: "**REDACTED**" if k in _SENSITIVE_KEYS else _redact(v) for k, v in obj.items() } if isinstance(obj, list): return [_redact(v) for v in obj] return obj async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" manager: WashDataManager = hass.data[DOMAIN][entry.entry_id] # Full store export - same payload as the export_config service, but the # entry_data / entry_options pass through the redactor to strip personal keys. exported: dict[str, Any] = manager.profile_store.export_data( entry_data=dict(entry.data), entry_options=dict(entry.options), ) return { "entry": _redact(entry.as_dict()), "manager_state": { "current_state": manager.check_state(), "current_program": manager.current_program, "time_remaining": manager.time_remaining, "cycle_progress": manager.cycle_progress, "sample_interval_stats": ( dict(manager.sample_interval_stats) if isinstance(manager.sample_interval_stats, dict) else {} ), "profile_sample_repair_stats": manager.profile_sample_repair_stats, "suggestions": manager.profile_store.get_suggestions(), "feature_flags": { "auto_maintenance": bool(getattr(manager, "_auto_maintenance", False)), "save_debug_traces": bool(getattr(manager, "_save_debug_traces", False)), "notify_fire_events": bool(getattr(manager, "_notify_fire_events", False)), }, }, "store_export": _redact(exported), # Rolling 24-hour in-memory buffers (redacted: msg fields stripped from logs). # power_trace: [[iso_ts, watts], ...] - every raw sensor reading # state_history: [{ts, from, to, program}, ...] - detector state changes # logs: [{ts, lvl}, ...] - log timestamps and levels (msg removed) "live_diagnostics": manager.diag_buffer.redacted_snapshot(), }