custom_components/govee_bluetooth/diagnostics.py

"""Diagnostics support: what the light last told us and how reachable it is."""

from __future__ import annotations

from dataclasses import asdict
from typing import Any

from homeassistant.components import bluetooth
from homeassistant.core import HomeAssistant

from . import GoveeConfigEntry
from .const import CONF_COLOR_SUBMODE, CONF_DETECTED_SUBMODE, CONF_MODEL, CONF_PROBE_HISTORY
from .models import table_profile_for


def _reachability(hass: HomeAssistant, address: str) -> str | None:
    """Home Assistant's own explanation of why an address is (un)reachable.

    The API arrived in 2026; older cores simply omit the field.
    """
    try:
        from homeassistant.components.bluetooth import BluetoothReachabilityIntent

        return bluetooth.async_address_reachability_diagnostics(
            hass, address, BluetoothReachabilityIntent.CONNECTION
        )
    except (ImportError, AttributeError):
        return None


async def async_get_config_entry_diagnostics(
    hass: HomeAssistant, entry: GoveeConfigEntry
) -> dict[str, Any]:
    coordinator = entry.runtime_data
    device = coordinator.device
    # The probe history is large (it carries raw frames); surface it once at
    # the top level rather than buried in the entry data.
    entry_data = {k: v for k, v in entry.data.items() if k != CONF_PROBE_HISTORY}
    return {
        "entry": {"data": entry_data, "options": dict(entry.options)},
        "profile": asdict(device.profile),
        "color_submode": {
            "static_table": table_profile_for(entry.data.get(CONF_MODEL)).color_submode,
            "detected": entry.data.get(CONF_DETECTED_SUBMODE),
            "manual_override": entry.options.get(CONF_COLOR_SUBMODE),
            "last_observed": device.state.extra.get("submode"),
            "in_use": device.profile.color_submode,
        },
        "state": asdict(device.state),
        "readback_profile": asdict(device.read_profile),
        "pact": device.pact,
        "desired_state": device.desired_state,
        "optional_reads": device.optional_reads,
        "host_diagnostics": {
            "optional_read_failures": device.optional_read_failures,
            "command_rejections": device.command_rejections,
            "last_state_observation": device.last_observation,
            "last_command_acceptance": device.last_command_acceptance,
        },
        "firmware_history": list(device.firmware_history),
        "bluetooth": {
            "seen_by_connectable_adapter": coordinator.available,
            "connectable_scanners": bluetooth.async_scanner_count(hass, connectable=True),
            "reachability": _reachability(hass, device.address),
        },
        "transport": {
            "control_properties": list(device.control_properties),
            "write_response": device.write_response,
        },
        "last_update_success": coordinator.last_update_success,
        # Raw frames, oldest first: "tx" is what this integration sent, "rx"
        # what the light answered. Hex, with the 0x33/0xAA type byte first.
        "frames": device.frames,
        # Dialect probe runs from setup or Configure, newest last, with the
        # frames each one exchanged over its own short-lived connection.
        "probe_history": list(entry.data.get(CONF_PROBE_HISTORY, [])),
    }