Support cloudhooks in Withings (#100916)
* Support cloudhooks in Withings * Support cloudhooks in Withings * Support cloudhooks in Withings * Remove strings
This commit is contained in:
parent
42b006a108
commit
0f95de997f
15 changed files with 421 additions and 189 deletions
|
@ -5,21 +5,24 @@ For more details about this platform, please refer to the documentation at
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime
|
||||
|
||||
from aiohttp.hdrs import METH_HEAD, METH_POST
|
||||
from aiohttp.web import Request, Response
|
||||
import voluptuous as vol
|
||||
from withings_api.common import NotifyAppli
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.components import cloud
|
||||
from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.webhook import (
|
||||
async_generate_id,
|
||||
async_unregister as async_unregister_webhook,
|
||||
async_generate_id as webhook_generate_id,
|
||||
async_generate_url as webhook_generate_url,
|
||||
async_register as webhook_register,
|
||||
async_unregister as webhook_unregister,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -27,15 +30,24 @@ from homeassistant.const import (
|
|||
CONF_CLIENT_SECRET,
|
||||
CONF_TOKEN,
|
||||
CONF_WEBHOOK_ID,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import ConfigEntryWithingsApi
|
||||
from .const import CONF_PROFILES, CONF_USE_WEBHOOK, CONFIG, DOMAIN, LOGGER
|
||||
from .const import (
|
||||
CONF_CLOUDHOOK_URL,
|
||||
CONF_PROFILES,
|
||||
CONF_USE_WEBHOOK,
|
||||
CONFIG,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import WithingsDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
@ -100,24 +112,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Withings from a config entry."""
|
||||
if CONF_USE_WEBHOOK not in entry.options:
|
||||
if CONF_WEBHOOK_ID not in entry.data or entry.unique_id is None:
|
||||
new_data = entry.data.copy()
|
||||
new_options = {
|
||||
CONF_USE_WEBHOOK: new_data.get(CONF_USE_WEBHOOK, False),
|
||||
}
|
||||
unique_id = str(entry.data[CONF_TOKEN]["userid"])
|
||||
if CONF_WEBHOOK_ID not in new_data:
|
||||
new_data[CONF_WEBHOOK_ID] = async_generate_id()
|
||||
new_data[CONF_WEBHOOK_ID] = webhook_generate_id()
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, options=new_options, unique_id=unique_id
|
||||
entry, data=new_data, unique_id=unique_id
|
||||
)
|
||||
if (
|
||||
use_webhook := hass.data[DOMAIN][CONFIG].get(CONF_USE_WEBHOOK)
|
||||
) is not None and use_webhook != entry.options[CONF_USE_WEBHOOK]:
|
||||
new_options = entry.options.copy()
|
||||
new_options |= {CONF_USE_WEBHOOK: use_webhook}
|
||||
hass.config_entries.async_update_entry(entry, options=new_options)
|
||||
|
||||
client = ConfigEntryWithingsApi(
|
||||
hass=hass,
|
||||
|
@ -126,28 +129,66 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
hass, entry
|
||||
),
|
||||
)
|
||||
|
||||
use_webhooks = entry.options[CONF_USE_WEBHOOK]
|
||||
coordinator = WithingsDataUpdateCoordinator(hass, client, use_webhooks)
|
||||
if use_webhooks:
|
||||
|
||||
@callback
|
||||
def async_call_later_callback(now) -> None:
|
||||
hass.async_create_task(coordinator.async_subscribe_webhooks())
|
||||
|
||||
entry.async_on_unload(async_call_later(hass, 1, async_call_later_callback))
|
||||
webhook.async_register(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"Withings notify",
|
||||
entry.data[CONF_WEBHOOK_ID],
|
||||
get_webhook_handler(coordinator),
|
||||
)
|
||||
coordinator = WithingsDataUpdateCoordinator(hass, client)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
async def unregister_webhook(
|
||||
call_or_event_or_dt: ServiceCall | Event | datetime | None,
|
||||
) -> None:
|
||||
LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID])
|
||||
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
await hass.data[DOMAIN][entry.entry_id].async_unsubscribe_webhooks()
|
||||
|
||||
async def register_webhook(
|
||||
call_or_event_or_dt: ServiceCall | Event | datetime | None,
|
||||
) -> None:
|
||||
if cloud.async_active_subscription(hass):
|
||||
webhook_url = await async_cloudhook_generate_url(hass, entry)
|
||||
else:
|
||||
webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
if not webhook_url.startswith("https://"):
|
||||
LOGGER.warning(
|
||||
"Webhook not registered - "
|
||||
"https and port 443 is required to register the webhook"
|
||||
)
|
||||
return
|
||||
|
||||
webhook_register(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"Withings",
|
||||
entry.data[CONF_WEBHOOK_ID],
|
||||
get_webhook_handler(coordinator),
|
||||
)
|
||||
|
||||
await hass.data[DOMAIN][entry.entry_id].async_subscribe_webhooks(webhook_url)
|
||||
LOGGER.debug("Register Withings webhook: %s", webhook_url)
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
|
||||
)
|
||||
|
||||
async def manage_cloudhook(state: cloud.CloudConnectionState) -> None:
|
||||
if state is cloud.CloudConnectionState.CLOUD_CONNECTED:
|
||||
await register_webhook(None)
|
||||
|
||||
if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED:
|
||||
await unregister_webhook(None)
|
||||
async_call_later(hass, 30, register_webhook)
|
||||
|
||||
if cloud.async_active_subscription(hass):
|
||||
if cloud.async_is_connected(hass):
|
||||
await register_webhook(None)
|
||||
cloud.async_listen_connection_change(hass, manage_cloudhook)
|
||||
|
||||
elif hass.state == CoreState.running:
|
||||
await register_webhook(None)
|
||||
else:
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, register_webhook)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
@ -156,8 +197,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Withings config entry."""
|
||||
if entry.options[CONF_USE_WEBHOOK]:
|
||||
async_unregister_webhook(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
@ -169,6 +209,30 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
|
||||
"""Generate the full URL for a webhook_id."""
|
||||
if CONF_CLOUDHOOK_URL not in entry.data:
|
||||
webhook_url = await cloud.async_create_cloudhook(
|
||||
hass, entry.data[CONF_WEBHOOK_ID]
|
||||
)
|
||||
data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url}
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
return webhook_url
|
||||
return str(entry.data[CONF_CLOUDHOOK_URL])
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Cleanup when entry is removed."""
|
||||
if cloud.async_active_subscription(hass):
|
||||
try:
|
||||
LOGGER.debug(
|
||||
"Removing Withings cloudhook (%s)", entry.data[CONF_WEBHOOK_ID]
|
||||
)
|
||||
await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
except cloud.CloudNotAvailable:
|
||||
pass
|
||||
|
||||
|
||||
def json_message_response(message: str, message_code: int) -> Response:
|
||||
"""Produce common json output."""
|
||||
return HomeAssistantView.json({"message": message, "code": message_code})
|
||||
|
|
|
@ -47,12 +47,11 @@ async def async_setup_entry(
|
|||
"""Set up the sensor config entry."""
|
||||
coordinator: WithingsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
if coordinator.use_webhooks:
|
||||
entities = [
|
||||
WithingsBinarySensor(coordinator, attribute) for attribute in BINARY_SENSORS
|
||||
]
|
||||
entities = [
|
||||
WithingsBinarySensor(coordinator, attribute) for attribute in BINARY_SENSORS
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class WithingsBinarySensor(WithingsEntity, BinarySensorEntity):
|
||||
|
|
|
@ -5,13 +5,11 @@ from collections.abc import Mapping
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from withings_api.common import AuthScope
|
||||
|
||||
from homeassistant.components.webhook import async_generate_id
|
||||
from homeassistant.config_entries import ConfigEntry, OptionsFlowWithConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
|
@ -27,14 +25,6 @@ class WithingsFlowHandler(
|
|||
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> WithingsOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return WithingsOptionsFlowHandler(config_entry)
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
|
@ -83,27 +73,9 @@ class WithingsFlowHandler(
|
|||
)
|
||||
|
||||
if self.reauth_entry.unique_id == user_id:
|
||||
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.reauth_entry, data={**self.reauth_entry.data, **data}
|
||||
)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_abort(reason="wrong_account")
|
||||
|
||||
|
||||
class WithingsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Withings Options flow handler."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Initialize form."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema({vol.Required(CONF_USE_WEBHOOK): bool}),
|
||||
self.options,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ import logging
|
|||
DEFAULT_TITLE = "Withings"
|
||||
CONF_PROFILES = "profiles"
|
||||
CONF_USE_WEBHOOK = "use_webhook"
|
||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||
|
||||
DATA_MANAGER = "data_manager"
|
||||
|
||||
|
|
|
@ -15,9 +15,7 @@ from withings_api.common import (
|
|||
query_measure_groups,
|
||||
)
|
||||
|
||||
from homeassistant.components.webhook import async_generate_url
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
@ -72,6 +70,8 @@ WITHINGS_MEASURE_TYPE_MAP: dict[
|
|||
NotifyAppli.BED_IN: Measurement.IN_BED,
|
||||
}
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]]):
|
||||
"""Base coordinator."""
|
||||
|
@ -79,21 +79,12 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||
in_bed: bool | None = None
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, client: ConfigEntryWithingsApi, use_webhooks: bool
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
|
||||
"""Initialize the Withings data coordinator."""
|
||||
update_interval: timedelta | None = timedelta(minutes=10)
|
||||
if use_webhooks:
|
||||
update_interval = None
|
||||
super().__init__(hass, LOGGER, name="Withings", update_interval=update_interval)
|
||||
super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL)
|
||||
self._client = client
|
||||
self._webhook_url = async_generate_url(
|
||||
hass, self.config_entry.data[CONF_WEBHOOK_ID]
|
||||
)
|
||||
self.use_webhooks = use_webhooks
|
||||
|
||||
async def async_subscribe_webhooks(self) -> None:
|
||||
async def async_subscribe_webhooks(self, webhook_url: str) -> None:
|
||||
"""Subscribe to webhooks."""
|
||||
await self.async_unsubscribe_webhooks()
|
||||
|
||||
|
@ -102,7 +93,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||
subscribed_notifications = frozenset(
|
||||
profile.appli
|
||||
for profile in current_webhooks.profiles
|
||||
if profile.callbackurl == self._webhook_url
|
||||
if profile.callbackurl == webhook_url
|
||||
)
|
||||
|
||||
notification_to_subscribe = (
|
||||
|
@ -114,14 +105,15 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||
for notification in notification_to_subscribe:
|
||||
LOGGER.debug(
|
||||
"Subscribing %s for %s in %s seconds",
|
||||
self._webhook_url,
|
||||
webhook_url,
|
||||
notification,
|
||||
SUBSCRIBE_DELAY.total_seconds(),
|
||||
)
|
||||
# Withings will HTTP HEAD the callback_url and needs some downtime
|
||||
# between each call or there is a higher chance of failure.
|
||||
await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds())
|
||||
await self._client.async_notify_subscribe(self._webhook_url, notification)
|
||||
await self._client.async_notify_subscribe(webhook_url, notification)
|
||||
self.update_interval = None
|
||||
|
||||
async def async_unsubscribe_webhooks(self) -> None:
|
||||
"""Unsubscribe to webhooks."""
|
||||
|
@ -140,6 +132,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||
await self._client.async_notify_revoke(
|
||||
webhook_configuration.callbackurl, webhook_configuration.appli
|
||||
)
|
||||
self.update_interval = UPDATE_INTERVAL
|
||||
|
||||
async def _async_update_data(self) -> dict[Measurement, Any]:
|
||||
try:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "withings",
|
||||
"name": "Withings",
|
||||
"after_dependencies": ["cloud"],
|
||||
"codeowners": ["@vangorra", "@joostlek"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials", "http", "webhook"],
|
||||
|
|
|
@ -22,15 +22,6 @@
|
|||
"default": "Successfully authenticated with Withings."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"use_webhook": "Use webhooks"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"in_bed": {
|
||||
|
|
|
@ -6,10 +6,8 @@ from urllib.parse import urlparse
|
|||
from aiohttp.test_utils import TestClient
|
||||
|
||||
from homeassistant.components.webhook import async_generate_url
|
||||
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -42,26 +40,16 @@ async def call_webhook(
|
|||
return WebhookResponse(message=data["message"], message_code=data["code"])
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, enable_webhooks: bool = True
|
||||
) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"external_url": "http://example.local:8123"},
|
||||
)
|
||||
if enable_webhooks:
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"external_url": "https://example.local:8123"},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
|
||||
async def enable_webhooks(hass: HomeAssistant) -> None:
|
||||
"""Enable webhooks."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
DOMAIN: {
|
||||
CONF_USE_WEBHOOK: True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -79,8 +79,29 @@ def webhook_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
|||
"profile": TITLE,
|
||||
"webhook_id": WEBHOOK_ID,
|
||||
},
|
||||
options={
|
||||
"use_webhook": True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cloudhook_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
||||
"""Create Withings entry in Home Assistant."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=TITLE,
|
||||
unique_id=str(USER_ID),
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"status": 0,
|
||||
"userid": str(USER_ID),
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": expires_at,
|
||||
"scope": ",".join(scopes),
|
||||
},
|
||||
"profile": TITLE,
|
||||
"webhook_id": WEBHOOK_ID,
|
||||
"cloudhook_url": "https://hooks.nabu.casa/ABCD",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -105,9 +126,6 @@ def polling_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
|||
"profile": TITLE,
|
||||
"webhook_id": WEBHOOK_ID,
|
||||
},
|
||||
options={
|
||||
"use_webhook": False,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
@ -136,7 +154,7 @@ def mock_withings():
|
|||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(name="disable_webhook_delay")
|
||||
@pytest.fixture(name="disable_webhook_delay", autouse=True)
|
||||
def disable_webhook_delay():
|
||||
"""Disable webhook delay."""
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"profiles": []
|
||||
}
|
|
@ -8,13 +8,13 @@
|
|||
},
|
||||
{
|
||||
"appli": 50,
|
||||
"callbackurl": "http://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
||||
"callbackurl": "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
||||
"expires": 2147483647,
|
||||
"comment": null
|
||||
},
|
||||
{
|
||||
"appli": 51,
|
||||
"callbackurl": "http://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
||||
"callbackurl": "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
||||
"expires": 2147483647,
|
||||
"comment": null
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ from withings_api.common import NotifyAppli
|
|||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import call_webhook, enable_webhooks, setup_integration
|
||||
from . import call_webhook, setup_integration
|
||||
from .conftest import USER_ID, WEBHOOK_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -18,12 +18,10 @@ from tests.typing import ClientSessionGenerator
|
|||
async def test_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test binary sensor."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
|
@ -56,18 +54,17 @@ async def test_binary_sensor(
|
|||
async def test_polling_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test binary sensor."""
|
||||
await setup_integration(hass, polling_config_entry)
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
entity_id = "binary_sensor.henk_in_bed"
|
||||
|
||||
assert hass.states.get(entity_id) is None
|
||||
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||
|
||||
with pytest.raises(ClientResponseError):
|
||||
await call_webhook(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Tests for config flow."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
|
||||
from homeassistant.components.withings.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
@ -84,7 +84,6 @@ async def test_config_non_unique_profile(
|
|||
current_request_with_host: None,
|
||||
withings: AsyncMock,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
disable_webhook_delay,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test setup a non-unique profile."""
|
||||
|
@ -138,7 +137,6 @@ async def test_config_reauth_profile(
|
|||
aioclient_mock: AiohttpClientMocker,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
current_request_with_host,
|
||||
) -> None:
|
||||
"""Test reauth an existing profile reauthenticates the config entry."""
|
||||
|
@ -201,7 +199,6 @@ async def test_config_reauth_wrong_account(
|
|||
aioclient_mock: AiohttpClientMocker,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
current_request_with_host,
|
||||
) -> None:
|
||||
"""Test reauth with wrong account."""
|
||||
|
@ -256,31 +253,3 @@ async def test_config_reauth_wrong_account(
|
|||
assert result
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "wrong_account"
|
||||
|
||||
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
current_request_with_host,
|
||||
) -> None:
|
||||
"""Test options flow."""
|
||||
await setup_integration(hass, polling_config_entry)
|
||||
|
||||
result = await hass.config_entries.options.async_init(polling_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_USE_WEBHOOK: True},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_USE_WEBHOOK: True}
|
||||
|
|
|
@ -1,26 +1,43 @@
|
|||
"""Tests for the Withings component."""
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
from withings_api import NotifyListResponse
|
||||
from withings_api.common import AuthFailedException, NotifyAppli, UnauthorizedException
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.cloud import (
|
||||
SIGNAL_CLOUD_CONNECTION_STATE,
|
||||
CloudConnectionState,
|
||||
CloudNotAvailable,
|
||||
)
|
||||
from homeassistant.components.webhook import async_generate_url
|
||||
from homeassistant.components.withings import CONFIG_SCHEMA, async_setup
|
||||
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_WEBHOOK_ID,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
)
|
||||
from homeassistant.core import CoreState, HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import call_webhook, enable_webhooks, setup_integration
|
||||
from . import call_webhook, setup_integration
|
||||
from .conftest import USER_ID, WEBHOOK_ID
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
load_json_object_fixture,
|
||||
)
|
||||
from tests.components.cloud import mock_cloud
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
|
@ -108,12 +125,10 @@ async def test_async_setup_no_config(hass: HomeAssistant) -> None:
|
|||
async def test_data_manager_webhook_subscription(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test data manager webhook subscriptions."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
await hass_client_no_auth()
|
||||
await hass.async_block_till_done()
|
||||
|
@ -122,7 +137,7 @@ async def test_data_manager_webhook_subscription(
|
|||
|
||||
assert withings.async_notify_subscribe.call_count == 4
|
||||
|
||||
webhook_url = "http://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e"
|
||||
webhook_url = "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e"
|
||||
|
||||
withings.async_notify_subscribe.assert_any_call(webhook_url, NotifyAppli.WEIGHT)
|
||||
withings.async_notify_subscribe.assert_any_call(
|
||||
|
@ -138,7 +153,6 @@ async def test_data_manager_webhook_subscription(
|
|||
async def test_webhook_subscription_polling_config(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
|
@ -169,10 +183,8 @@ async def test_requests(
|
|||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
method: str,
|
||||
disable_webhook_delay,
|
||||
) -> None:
|
||||
"""Test we handle request methods Withings sends."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
client = await hass_client_no_auth()
|
||||
webhook_url = async_generate_url(hass, WEBHOOK_ID)
|
||||
|
@ -189,10 +201,8 @@ async def test_webhooks_request_data(
|
|||
withings: AsyncMock,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
disable_webhook_delay,
|
||||
) -> None:
|
||||
"""Test calling a webhook requests data."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
|
@ -207,6 +217,35 @@ async def test_webhooks_request_data(
|
|||
assert withings.async_measure_get_meas.call_count == 2
|
||||
|
||||
|
||||
async def test_delayed_startup(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test delayed start up."""
|
||||
hass.state = CoreState.not_running
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
|
||||
withings.async_notify_subscribe.assert_not_called()
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
assert withings.async_measure_get_meas.call_count == 1
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await call_webhook(
|
||||
hass,
|
||||
WEBHOOK_ID,
|
||||
{"userid": USER_ID, "appli": NotifyAppli.WEIGHT},
|
||||
client,
|
||||
)
|
||||
assert withings.async_measure_get_meas.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[
|
||||
|
@ -221,7 +260,7 @@ async def test_triggering_reauth(
|
|||
error: Exception,
|
||||
) -> None:
|
||||
"""Test triggering reauth."""
|
||||
await setup_integration(hass, polling_config_entry)
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
withings.async_measure_get_meas.side_effect = error
|
||||
future = dt_util.utcnow() + timedelta(minutes=10)
|
||||
|
@ -275,9 +314,211 @@ async def test_config_flow_upgrade(
|
|||
assert entry.unique_id == "123"
|
||||
assert entry.data["token"]["userid"] == 123
|
||||
assert CONF_WEBHOOK_ID in entry.data
|
||||
assert entry.options == {
|
||||
"use_webhook": False,
|
||||
}
|
||||
|
||||
|
||||
async def test_setup_with_cloudhook(
|
||||
hass: HomeAssistant, cloudhook_config_entry: MockConfigEntry, withings: AsyncMock
|
||||
) -> None:
|
||||
"""Test if set up with active cloud subscription and cloud hook."""
|
||||
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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",
|
||||
) as fake_create_cloudhook, patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
), patch(
|
||||
"homeassistant.components.cloud.async_delete_cloudhook"
|
||||
) as fake_delete_cloudhook, patch(
|
||||
"homeassistant.components.withings.webhook_generate_url"
|
||||
):
|
||||
await setup_integration(hass, cloudhook_config_entry)
|
||||
assert hass.components.cloud.async_active_subscription() is True
|
||||
|
||||
assert (
|
||||
hass.config_entries.async_entries(DOMAIN)[0].data["cloudhook_url"]
|
||||
== "https://hooks.nabu.casa/ABCD"
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
fake_create_cloudhook.assert_not_called()
|
||||
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN):
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
fake_delete_cloudhook.assert_called_once()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
|
||||
async def test_removing_entry_with_cloud_unavailable(
|
||||
hass: HomeAssistant, cloudhook_config_entry: MockConfigEntry, withings: AsyncMock
|
||||
) -> None:
|
||||
"""Test handling cloud unavailable when deleting entry."""
|
||||
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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",
|
||||
side_effect=CloudNotAvailable(),
|
||||
), patch(
|
||||
"homeassistant.components.withings.webhook_generate_url"
|
||||
):
|
||||
await setup_integration(hass, cloudhook_config_entry)
|
||||
assert hass.components.cloud.async_active_subscription() is True
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN):
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
|
||||
async def test_setup_with_cloud(
|
||||
hass: HomeAssistant, webhook_config_entry: MockConfigEntry, withings: AsyncMock
|
||||
) -> None:
|
||||
"""Test if set up with active cloud subscription."""
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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",
|
||||
) as fake_create_cloudhook, patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
), patch(
|
||||
"homeassistant.components.cloud.async_delete_cloudhook"
|
||||
) as fake_delete_cloudhook, patch(
|
||||
"homeassistant.components.withings.webhook_generate_url"
|
||||
):
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
assert hass.components.cloud.async_active_subscription() is True
|
||||
assert hass.components.cloud.async_is_connected() is True
|
||||
fake_create_cloudhook.assert_called_once()
|
||||
|
||||
assert (
|
||||
hass.config_entries.async_entries("withings")[0].data["cloudhook_url"]
|
||||
== "https://hooks.nabu.casa/ABCD"
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
for config_entry in hass.config_entries.async_entries("withings"):
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
fake_delete_cloudhook.assert_called_once()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
|
||||
async def test_setup_without_https(
|
||||
hass: HomeAssistant,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
withings: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test if set up with cloud link and without https."""
|
||||
hass.config.components.add("cloud")
|
||||
with patch(
|
||||
"homeassistant.helpers.network.get_url",
|
||||
return_value="http://example.nabu.casa",
|
||||
), patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
), patch(
|
||||
"homeassistant.components.withings.webhook_generate_url"
|
||||
) as mock_async_generate_url:
|
||||
mock_async_generate_url.return_value = "http://example.com"
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
mock_async_generate_url.assert_called_once()
|
||||
|
||||
assert "https and port 443 is required to register the webhook" in caplog.text
|
||||
|
||||
|
||||
async def test_cloud_disconnect(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test disconnecting from the cloud."""
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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)
|
||||
assert hass.components.cloud.async_active_subscription() is True
|
||||
assert hass.components.cloud.async_is_connected() is True
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
withings.async_notify_list.return_value = NotifyListResponse(
|
||||
**load_json_object_fixture("withings/empty_notify_list.json")
|
||||
)
|
||||
|
||||
assert withings.async_notify_subscribe.call_count == 6
|
||||
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert withings.async_notify_revoke.call_count == 3
|
||||
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert withings.async_notify_subscribe.call_count == 12
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -300,13 +541,11 @@ async def test_webhook_post(
|
|||
withings: AsyncMock,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
disable_webhook_delay,
|
||||
body: dict[str, Any],
|
||||
expected_code: int,
|
||||
current_request_with_host: None,
|
||||
) -> None:
|
||||
"""Test webhook callback."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
client = await hass_client_no_auth()
|
||||
webhook_url = async_generate_url(hass, WEBHOOK_ID)
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, State
|
|||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
|
||||
from . import call_webhook, enable_webhooks, setup_integration
|
||||
from . import call_webhook, setup_integration
|
||||
from .conftest import USER_ID, WEBHOOK_ID
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
@ -95,11 +95,9 @@ async def test_sensor_default_enabled_entities(
|
|||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
webhook_config_entry: MockConfigEntry,
|
||||
disable_webhook_delay,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test entities enabled by default."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, webhook_config_entry)
|
||||
entity_registry: EntityRegistry = er.async_get(hass)
|
||||
|
||||
|
@ -137,7 +135,6 @@ async def test_all_entities(
|
|||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
polling_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
|
@ -156,7 +153,7 @@ async def test_update_failed(
|
|||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
await setup_integration(hass, polling_config_entry)
|
||||
await setup_integration(hass, polling_config_entry, False)
|
||||
|
||||
withings.async_measure_get_meas.side_effect = Exception
|
||||
freezer.tick(timedelta(minutes=10))
|
||||
|
|
Loading…
Add table
Reference in a new issue