mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
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
@ -5,21 +5,24 @@ For more details about this platform, please refer to the documentation at
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiohttp.hdrs import METH_HEAD, METH_POST
|
from aiohttp.hdrs import METH_HEAD, METH_POST
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from withings_api.common import NotifyAppli
|
from withings_api.common import NotifyAppli
|
||||||
|
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import cloud
|
||||||
from homeassistant.components.application_credentials import (
|
from homeassistant.components.application_credentials import (
|
||||||
ClientCredential,
|
ClientCredential,
|
||||||
async_import_client_credential,
|
async_import_client_credential,
|
||||||
)
|
)
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.webhook import (
|
from homeassistant.components.webhook import (
|
||||||
async_generate_id,
|
async_generate_id as webhook_generate_id,
|
||||||
async_unregister as async_unregister_webhook,
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -27,15 +30,24 @@ from homeassistant.const import (
|
|||||||
CONF_CLIENT_SECRET,
|
CONF_CLIENT_SECRET,
|
||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
CONF_WEBHOOK_ID,
|
CONF_WEBHOOK_ID,
|
||||||
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
Platform,
|
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 import config_entry_oauth2_flow, config_validation as cv
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .api import ConfigEntryWithingsApi
|
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
|
from .coordinator import WithingsDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Withings from a config entry."""
|
"""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_data = entry.data.copy()
|
||||||
new_options = {
|
|
||||||
CONF_USE_WEBHOOK: new_data.get(CONF_USE_WEBHOOK, False),
|
|
||||||
}
|
|
||||||
unique_id = str(entry.data[CONF_TOKEN]["userid"])
|
unique_id = str(entry.data[CONF_TOKEN]["userid"])
|
||||||
if CONF_WEBHOOK_ID not in new_data:
|
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(
|
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(
|
client = ConfigEntryWithingsApi(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
@ -126,28 +129,66 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass, entry
|
hass, entry
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
coordinator = WithingsDataUpdateCoordinator(hass, client)
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
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)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
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:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload Withings config entry."""
|
"""Unload Withings config entry."""
|
||||||
if entry.options[CONF_USE_WEBHOOK]:
|
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||||
async_unregister_webhook(hass, entry.data[CONF_WEBHOOK_ID])
|
|
||||||
|
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
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)
|
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:
|
def json_message_response(message: str, message_code: int) -> Response:
|
||||||
"""Produce common json output."""
|
"""Produce common json output."""
|
||||||
return HomeAssistantView.json({"message": message, "code": message_code})
|
return HomeAssistantView.json({"message": message, "code": message_code})
|
||||||
|
@ -47,12 +47,11 @@ async def async_setup_entry(
|
|||||||
"""Set up the sensor config entry."""
|
"""Set up the sensor config entry."""
|
||||||
coordinator: WithingsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: WithingsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
if coordinator.use_webhooks:
|
entities = [
|
||||||
entities = [
|
WithingsBinarySensor(coordinator, attribute) for attribute in BINARY_SENSORS
|
||||||
WithingsBinarySensor(coordinator, attribute) for attribute in BINARY_SENSORS
|
]
|
||||||
]
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class WithingsBinarySensor(WithingsEntity, BinarySensorEntity):
|
class WithingsBinarySensor(WithingsEntity, BinarySensorEntity):
|
||||||
|
@ -5,13 +5,11 @@ from collections.abc import Mapping
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
from withings_api.common import AuthScope
|
from withings_api.common import AuthScope
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_id
|
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.const import CONF_TOKEN, CONF_WEBHOOK_ID
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
@ -27,14 +25,6 @@ class WithingsFlowHandler(
|
|||||||
|
|
||||||
reauth_entry: ConfigEntry | None = None
|
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
|
@property
|
||||||
def logger(self) -> logging.Logger:
|
def logger(self) -> logging.Logger:
|
||||||
"""Return logger."""
|
"""Return logger."""
|
||||||
@ -83,27 +73,9 @@ class WithingsFlowHandler(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.reauth_entry.unique_id == user_id:
|
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="reauth_successful")
|
||||||
|
|
||||||
return self.async_abort(reason="wrong_account")
|
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"
|
DEFAULT_TITLE = "Withings"
|
||||||
CONF_PROFILES = "profiles"
|
CONF_PROFILES = "profiles"
|
||||||
CONF_USE_WEBHOOK = "use_webhook"
|
CONF_USE_WEBHOOK = "use_webhook"
|
||||||
|
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||||
|
|
||||||
DATA_MANAGER = "data_manager"
|
DATA_MANAGER = "data_manager"
|
||||||
|
|
||||||
|
@ -15,9 +15,7 @@ from withings_api.common import (
|
|||||||
query_measure_groups,
|
query_measure_groups,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_url
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
@ -72,6 +70,8 @@ WITHINGS_MEASURE_TYPE_MAP: dict[
|
|||||||
NotifyAppli.BED_IN: Measurement.IN_BED,
|
NotifyAppli.BED_IN: Measurement.IN_BED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
|
||||||
class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]]):
|
class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]]):
|
||||||
"""Base coordinator."""
|
"""Base coordinator."""
|
||||||
@ -79,21 +79,12 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||||||
in_bed: bool | None = None
|
in_bed: bool | None = None
|
||||||
config_entry: ConfigEntry
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
|
||||||
self, hass: HomeAssistant, client: ConfigEntryWithingsApi, use_webhooks: bool
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Withings data coordinator."""
|
"""Initialize the Withings data coordinator."""
|
||||||
update_interval: timedelta | None = timedelta(minutes=10)
|
super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL)
|
||||||
if use_webhooks:
|
|
||||||
update_interval = None
|
|
||||||
super().__init__(hass, LOGGER, name="Withings", update_interval=update_interval)
|
|
||||||
self._client = client
|
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."""
|
"""Subscribe to webhooks."""
|
||||||
await self.async_unsubscribe_webhooks()
|
await self.async_unsubscribe_webhooks()
|
||||||
|
|
||||||
@ -102,7 +93,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||||||
subscribed_notifications = frozenset(
|
subscribed_notifications = frozenset(
|
||||||
profile.appli
|
profile.appli
|
||||||
for profile in current_webhooks.profiles
|
for profile in current_webhooks.profiles
|
||||||
if profile.callbackurl == self._webhook_url
|
if profile.callbackurl == webhook_url
|
||||||
)
|
)
|
||||||
|
|
||||||
notification_to_subscribe = (
|
notification_to_subscribe = (
|
||||||
@ -114,14 +105,15 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||||||
for notification in notification_to_subscribe:
|
for notification in notification_to_subscribe:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Subscribing %s for %s in %s seconds",
|
"Subscribing %s for %s in %s seconds",
|
||||||
self._webhook_url,
|
webhook_url,
|
||||||
notification,
|
notification,
|
||||||
SUBSCRIBE_DELAY.total_seconds(),
|
SUBSCRIBE_DELAY.total_seconds(),
|
||||||
)
|
)
|
||||||
# Withings will HTTP HEAD the callback_url and needs some downtime
|
# Withings will HTTP HEAD the callback_url and needs some downtime
|
||||||
# between each call or there is a higher chance of failure.
|
# between each call or there is a higher chance of failure.
|
||||||
await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds())
|
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:
|
async def async_unsubscribe_webhooks(self) -> None:
|
||||||
"""Unsubscribe to webhooks."""
|
"""Unsubscribe to webhooks."""
|
||||||
@ -140,6 +132,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||||||
await self._client.async_notify_revoke(
|
await self._client.async_notify_revoke(
|
||||||
webhook_configuration.callbackurl, webhook_configuration.appli
|
webhook_configuration.callbackurl, webhook_configuration.appli
|
||||||
)
|
)
|
||||||
|
self.update_interval = UPDATE_INTERVAL
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[Measurement, Any]:
|
async def _async_update_data(self) -> dict[Measurement, Any]:
|
||||||
try:
|
try:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "withings",
|
"domain": "withings",
|
||||||
"name": "Withings",
|
"name": "Withings",
|
||||||
|
"after_dependencies": ["cloud"],
|
||||||
"codeowners": ["@vangorra", "@joostlek"],
|
"codeowners": ["@vangorra", "@joostlek"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["application_credentials", "http", "webhook"],
|
"dependencies": ["application_credentials", "http", "webhook"],
|
||||||
|
@ -22,15 +22,6 @@
|
|||||||
"default": "Successfully authenticated with Withings."
|
"default": "Successfully authenticated with Withings."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
|
||||||
"step": {
|
|
||||||
"init": {
|
|
||||||
"data": {
|
|
||||||
"use_webhook": "Use webhooks"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"in_bed": {
|
"in_bed": {
|
||||||
|
@ -6,10 +6,8 @@ from urllib.parse import urlparse
|
|||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_url
|
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.config import async_process_ha_core_config
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -42,26 +40,16 @@ async def call_webhook(
|
|||||||
return WebhookResponse(message=data["message"], message_code=data["code"])
|
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."""
|
"""Fixture for setting up the component."""
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
await async_process_ha_core_config(
|
if enable_webhooks:
|
||||||
hass,
|
await async_process_ha_core_config(
|
||||||
{"external_url": "http://example.local:8123"},
|
hass,
|
||||||
)
|
{"external_url": "https://example.local:8123"},
|
||||||
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
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,
|
"profile": TITLE,
|
||||||
"webhook_id": WEBHOOK_ID,
|
"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,
|
"profile": TITLE,
|
||||||
"webhook_id": WEBHOOK_ID,
|
"webhook_id": WEBHOOK_ID,
|
||||||
},
|
},
|
||||||
options={
|
|
||||||
"use_webhook": False,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -136,7 +154,7 @@ def mock_withings():
|
|||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="disable_webhook_delay")
|
@pytest.fixture(name="disable_webhook_delay", autouse=True)
|
||||||
def disable_webhook_delay():
|
def disable_webhook_delay():
|
||||||
"""Disable webhook delay."""
|
"""Disable webhook delay."""
|
||||||
|
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"profiles": []
|
||||||
|
}
|
@ -8,13 +8,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appli": 50,
|
"appli": 50,
|
||||||
"callbackurl": "http://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
"callbackurl": "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
||||||
"expires": 2147483647,
|
"expires": 2147483647,
|
||||||
"comment": null
|
"comment": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appli": 51,
|
"appli": 51,
|
||||||
"callbackurl": "http://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
"callbackurl": "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e",
|
||||||
"expires": 2147483647,
|
"expires": 2147483647,
|
||||||
"comment": null
|
"comment": null
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ from withings_api.common import NotifyAppli
|
|||||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant
|
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 .conftest import USER_ID, WEBHOOK_ID
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
@ -18,12 +18,10 @@ from tests.typing import ClientSessionGenerator
|
|||||||
async def test_binary_sensor(
|
async def test_binary_sensor(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
webhook_config_entry: MockConfigEntry,
|
webhook_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test binary sensor."""
|
"""Test binary sensor."""
|
||||||
await enable_webhooks(hass)
|
|
||||||
await setup_integration(hass, webhook_config_entry)
|
await setup_integration(hass, webhook_config_entry)
|
||||||
|
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
@ -56,18 +54,17 @@ async def test_binary_sensor(
|
|||||||
async def test_polling_binary_sensor(
|
async def test_polling_binary_sensor(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
polling_config_entry: MockConfigEntry,
|
polling_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test binary sensor."""
|
"""Test binary sensor."""
|
||||||
await setup_integration(hass, polling_config_entry)
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
|
|
||||||
entity_id = "binary_sensor.henk_in_bed"
|
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):
|
with pytest.raises(ClientResponseError):
|
||||||
await call_webhook(
|
await call_webhook(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Tests for config flow."""
|
"""Tests for config flow."""
|
||||||
from unittest.mock import AsyncMock, patch
|
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.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
@ -84,7 +84,6 @@ async def test_config_non_unique_profile(
|
|||||||
current_request_with_host: None,
|
current_request_with_host: None,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
polling_config_entry: MockConfigEntry,
|
polling_config_entry: MockConfigEntry,
|
||||||
disable_webhook_delay,
|
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test setup a non-unique profile."""
|
"""Test setup a non-unique profile."""
|
||||||
@ -138,7 +137,6 @@ async def test_config_reauth_profile(
|
|||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
polling_config_entry: MockConfigEntry,
|
polling_config_entry: MockConfigEntry,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
current_request_with_host,
|
current_request_with_host,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth an existing profile reauthenticates the config entry."""
|
"""Test reauth an existing profile reauthenticates the config entry."""
|
||||||
@ -201,7 +199,6 @@ async def test_config_reauth_wrong_account(
|
|||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
polling_config_entry: MockConfigEntry,
|
polling_config_entry: MockConfigEntry,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
current_request_with_host,
|
current_request_with_host,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth with wrong account."""
|
"""Test reauth with wrong account."""
|
||||||
@ -256,31 +253,3 @@ async def test_config_reauth_wrong_account(
|
|||||||
assert result
|
assert result
|
||||||
assert result["type"] == FlowResultType.ABORT
|
assert result["type"] == FlowResultType.ABORT
|
||||||
assert result["reason"] == "wrong_account"
|
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."""
|
"""Tests for the Withings component."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from withings_api import NotifyListResponse
|
||||||
from withings_api.common import AuthFailedException, NotifyAppli, UnauthorizedException
|
from withings_api.common import AuthFailedException, NotifyAppli, UnauthorizedException
|
||||||
|
|
||||||
from homeassistant import config_entries
|
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.webhook import async_generate_url
|
||||||
from homeassistant.components.withings import CONFIG_SCHEMA, async_setup
|
from homeassistant.components.withings import CONFIG_SCHEMA, async_setup
|
||||||
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
|
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.const import (
|
||||||
from homeassistant.core import HomeAssistant
|
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 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 .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
|
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(
|
async def test_data_manager_webhook_subscription(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
webhook_config_entry: MockConfigEntry,
|
webhook_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test data manager webhook subscriptions."""
|
"""Test data manager webhook subscriptions."""
|
||||||
await enable_webhooks(hass)
|
|
||||||
await setup_integration(hass, webhook_config_entry)
|
await setup_integration(hass, webhook_config_entry)
|
||||||
await hass_client_no_auth()
|
await hass_client_no_auth()
|
||||||
await hass.async_block_till_done()
|
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
|
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(webhook_url, NotifyAppli.WEIGHT)
|
||||||
withings.async_notify_subscribe.assert_any_call(
|
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(
|
async def test_webhook_subscription_polling_config(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
polling_config_entry: MockConfigEntry,
|
polling_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
@ -169,10 +183,8 @@ async def test_requests(
|
|||||||
webhook_config_entry: MockConfigEntry,
|
webhook_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
method: str,
|
method: str,
|
||||||
disable_webhook_delay,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we handle request methods Withings sends."""
|
"""Test we handle request methods Withings sends."""
|
||||||
await enable_webhooks(hass)
|
|
||||||
await setup_integration(hass, webhook_config_entry)
|
await setup_integration(hass, webhook_config_entry)
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
webhook_url = async_generate_url(hass, WEBHOOK_ID)
|
webhook_url = async_generate_url(hass, WEBHOOK_ID)
|
||||||
@ -189,10 +201,8 @@ async def test_webhooks_request_data(
|
|||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
webhook_config_entry: MockConfigEntry,
|
webhook_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
disable_webhook_delay,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test calling a webhook requests data."""
|
"""Test calling a webhook requests data."""
|
||||||
await enable_webhooks(hass)
|
|
||||||
await setup_integration(hass, webhook_config_entry)
|
await setup_integration(hass, webhook_config_entry)
|
||||||
client = await hass_client_no_auth()
|
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
|
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(
|
@pytest.mark.parametrize(
|
||||||
"error",
|
"error",
|
||||||
[
|
[
|
||||||
@ -221,7 +260,7 @@ async def test_triggering_reauth(
|
|||||||
error: Exception,
|
error: Exception,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test triggering reauth."""
|
"""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
|
withings.async_measure_get_meas.side_effect = error
|
||||||
future = dt_util.utcnow() + timedelta(minutes=10)
|
future = dt_util.utcnow() + timedelta(minutes=10)
|
||||||
@ -275,9 +314,211 @@ async def test_config_flow_upgrade(
|
|||||||
assert entry.unique_id == "123"
|
assert entry.unique_id == "123"
|
||||||
assert entry.data["token"]["userid"] == 123
|
assert entry.data["token"]["userid"] == 123
|
||||||
assert CONF_WEBHOOK_ID in entry.data
|
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(
|
@pytest.mark.parametrize(
|
||||||
@ -300,13 +541,11 @@ async def test_webhook_post(
|
|||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
webhook_config_entry: MockConfigEntry,
|
webhook_config_entry: MockConfigEntry,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
disable_webhook_delay,
|
|
||||||
body: dict[str, Any],
|
body: dict[str, Any],
|
||||||
expected_code: int,
|
expected_code: int,
|
||||||
current_request_with_host: None,
|
current_request_with_host: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test webhook callback."""
|
"""Test webhook callback."""
|
||||||
await enable_webhooks(hass)
|
|
||||||
await setup_integration(hass, webhook_config_entry)
|
await setup_integration(hass, webhook_config_entry)
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
webhook_url = async_generate_url(hass, WEBHOOK_ID)
|
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 import entity_registry as er
|
||||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
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 .conftest import USER_ID, WEBHOOK_ID
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
@ -95,11 +95,9 @@ async def test_sensor_default_enabled_entities(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
webhook_config_entry: MockConfigEntry,
|
webhook_config_entry: MockConfigEntry,
|
||||||
disable_webhook_delay,
|
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test entities enabled by default."""
|
"""Test entities enabled by default."""
|
||||||
await enable_webhooks(hass)
|
|
||||||
await setup_integration(hass, webhook_config_entry)
|
await setup_integration(hass, webhook_config_entry)
|
||||||
entity_registry: EntityRegistry = er.async_get(hass)
|
entity_registry: EntityRegistry = er.async_get(hass)
|
||||||
|
|
||||||
@ -137,7 +135,6 @@ async def test_all_entities(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
withings: AsyncMock,
|
withings: AsyncMock,
|
||||||
disable_webhook_delay,
|
|
||||||
polling_config_entry: MockConfigEntry,
|
polling_config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test all entities."""
|
"""Test all entities."""
|
||||||
@ -156,7 +153,7 @@ async def test_update_failed(
|
|||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test all entities."""
|
"""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
|
withings.async_measure_get_meas.side_effect = Exception
|
||||||
freezer.tick(timedelta(minutes=10))
|
freezer.tick(timedelta(minutes=10))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user