From b4e4a98f178638ed156d57f229e194aa273fc841 Mon Sep 17 00:00:00 2001
From: Joost Lekkerkerker <joostlek@outlook.com>
Date: Sun, 15 Oct 2023 20:29:20 +0200
Subject: [PATCH] Add diagnostics to Withings (#102066)

---
 .../components/withings/coordinator.py        |  2 +
 .../components/withings/diagnostics.py        | 45 +++++++++++
 .../withings/snapshots/test_diagnostics.ambr  | 79 ++++++++++++++++++
 tests/components/withings/test_diagnostics.py | 80 +++++++++++++++++++
 4 files changed, 206 insertions(+)
 create mode 100644 homeassistant/components/withings/diagnostics.py
 create mode 100644 tests/components/withings/snapshots/test_diagnostics.ambr
 create mode 100644 tests/components/withings/test_diagnostics.py

diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py
index c5192ba3466..ac320aae3ae 100644
--- a/homeassistant/components/withings/coordinator.py
+++ b/homeassistant/components/withings/coordinator.py
@@ -34,6 +34,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
     config_entry: ConfigEntry
     _default_update_interval: timedelta | None = UPDATE_INTERVAL
     _last_valid_update: datetime | None = None
+    webhooks_connected: bool = False
 
     def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
         """Initialize the Withings data coordinator."""
@@ -45,6 +46,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
 
     def webhook_subscription_listener(self, connected: bool) -> None:
         """Call when webhook status changed."""
+        self.webhooks_connected = connected
         if connected:
             self.update_interval = None
         else:
diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py
new file mode 100644
index 00000000000..2424452d0f5
--- /dev/null
+++ b/homeassistant/components/withings/diagnostics.py
@@ -0,0 +1,45 @@
+"""Diagnostics support for Withings."""
+from __future__ import annotations
+
+from typing import Any
+
+from yarl import URL
+
+from homeassistant.components.webhook import async_generate_url as webhook_generate_url
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.core import HomeAssistant
+
+from . import (
+    CONF_CLOUDHOOK_URL,
+    WithingsMeasurementDataUpdateCoordinator,
+    WithingsSleepDataUpdateCoordinator,
+)
+from .const import DOMAIN, MEASUREMENT_COORDINATOR, SLEEP_COORDINATOR
+
+
+async def async_get_config_entry_diagnostics(
+    hass: HomeAssistant, entry: ConfigEntry
+) -> dict[str, Any]:
+    """Return diagnostics for a config entry."""
+
+    webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
+    url = URL(webhook_url)
+    has_valid_external_webhook_url = url.scheme == "https" and url.port == 443
+
+    has_cloudhooks = CONF_CLOUDHOOK_URL in entry.data
+
+    measurement_coordinator: WithingsMeasurementDataUpdateCoordinator = hass.data[
+        DOMAIN
+    ][entry.entry_id][MEASUREMENT_COORDINATOR]
+    sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][
+        entry.entry_id
+    ][SLEEP_COORDINATOR]
+
+    return {
+        "has_valid_external_webhook_url": has_valid_external_webhook_url,
+        "has_cloudhooks": has_cloudhooks,
+        "webhooks_connected": measurement_coordinator.webhooks_connected,
+        "received_measurements": list(measurement_coordinator.data.keys()),
+        "received_sleep_data": sleep_coordinator.data is not None,
+    }
diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr
new file mode 100644
index 00000000000..c65c321a5ef
--- /dev/null
+++ b/tests/components/withings/snapshots/test_diagnostics.ambr
@@ -0,0 +1,79 @@
+# serializer version: 1
+# name: test_diagnostics_cloudhook_instance
+  dict({
+    'has_cloudhooks': True,
+    'has_valid_external_webhook_url': True,
+    'received_measurements': list([
+      1,
+      8,
+      5,
+      76,
+      88,
+      4,
+      12,
+      71,
+      73,
+      6,
+      9,
+      10,
+      11,
+      54,
+      77,
+      91,
+    ]),
+    'received_sleep_data': True,
+    'webhooks_connected': True,
+  })
+# ---
+# name: test_diagnostics_polling_instance
+  dict({
+    'has_cloudhooks': False,
+    'has_valid_external_webhook_url': False,
+    'received_measurements': list([
+      1,
+      8,
+      5,
+      76,
+      88,
+      4,
+      12,
+      71,
+      73,
+      6,
+      9,
+      10,
+      11,
+      54,
+      77,
+      91,
+    ]),
+    'received_sleep_data': True,
+    'webhooks_connected': False,
+  })
+# ---
+# name: test_diagnostics_webhook_instance
+  dict({
+    'has_cloudhooks': False,
+    'has_valid_external_webhook_url': True,
+    'received_measurements': list([
+      1,
+      8,
+      5,
+      76,
+      88,
+      4,
+      12,
+      71,
+      73,
+      6,
+      9,
+      10,
+      11,
+      54,
+      77,
+      91,
+    ]),
+    'received_sleep_data': True,
+    'webhooks_connected': True,
+  })
+# ---
diff --git a/tests/components/withings/test_diagnostics.py b/tests/components/withings/test_diagnostics.py
new file mode 100644
index 00000000000..bb5c93e1f09
--- /dev/null
+++ b/tests/components/withings/test_diagnostics.py
@@ -0,0 +1,80 @@
+"""Tests for the diagnostics data provided by the Withings integration."""
+from unittest.mock import AsyncMock, patch
+
+from freezegun.api import FrozenDateTimeFactory
+from syrupy import SnapshotAssertion
+
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+from tests.components.diagnostics import get_diagnostics_for_config_entry
+from tests.components.withings import prepare_webhook_setup, setup_integration
+from tests.typing import ClientSessionGenerator
+
+
+async def test_diagnostics_polling_instance(
+    hass: HomeAssistant,
+    hass_client: ClientSessionGenerator,
+    withings: AsyncMock,
+    polling_config_entry: MockConfigEntry,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test diagnostics."""
+    await setup_integration(hass, polling_config_entry, False)
+
+    assert (
+        await get_diagnostics_for_config_entry(hass, hass_client, polling_config_entry)
+        == snapshot
+    )
+
+
+async def test_diagnostics_webhook_instance(
+    hass: HomeAssistant,
+    hass_client: ClientSessionGenerator,
+    withings: AsyncMock,
+    webhook_config_entry: MockConfigEntry,
+    snapshot: SnapshotAssertion,
+    freezer: FrozenDateTimeFactory,
+) -> None:
+    """Test diagnostics."""
+    await setup_integration(hass, webhook_config_entry)
+    await prepare_webhook_setup(hass, freezer)
+
+    assert (
+        await get_diagnostics_for_config_entry(hass, hass_client, webhook_config_entry)
+        == snapshot
+    )
+
+
+async def test_diagnostics_cloudhook_instance(
+    hass: HomeAssistant,
+    hass_client: ClientSessionGenerator,
+    withings: AsyncMock,
+    webhook_config_entry: MockConfigEntry,
+    snapshot: SnapshotAssertion,
+    freezer: FrozenDateTimeFactory,
+) -> None:
+    """Test diagnostics."""
+    with patch(
+        "homeassistant.components.cloud.async_is_logged_in", return_value=True
+    ), patch(
+        "homeassistant.components.cloud.async_is_connected", return_value=True
+    ), patch(
+        "homeassistant.components.cloud.async_active_subscription", return_value=True
+    ), patch(
+        "homeassistant.components.cloud.async_create_cloudhook",
+        return_value="https://hooks.nabu.casa/ABCD",
+    ), patch(
+        "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+    ), patch(
+        "homeassistant.components.cloud.async_delete_cloudhook"
+    ), patch(
+        "homeassistant.components.withings.webhook_generate_url"
+    ):
+        await setup_integration(hass, webhook_config_entry)
+        await prepare_webhook_setup(hass, freezer)
+
+    assert (
+        await get_diagnostics_for_config_entry(hass, hass_client, webhook_config_entry)
+        == snapshot
+    )