Skip to content

Commit

Permalink
Add coordinator to Withings (home-assistant#100378)
Browse files Browse the repository at this point in the history
* Add coordinator to Withings

* Add coordinator to Withings

* Fix tests

* Remove common files

* Fix tests

* Fix tests

* Rename to Entity

* Fix

* Rename webhook handler

* Fix

* Fix external url

* Update homeassistant/components/withings/entity.py

Co-authored-by: Luke Lashley <[email protected]>

* Update homeassistant/components/withings/entity.py

Co-authored-by: Luke Lashley <[email protected]>

* Update homeassistant/components/withings/entity.py

Co-authored-by: Luke Lashley <[email protected]>

* Update homeassistant/components/withings/entity.py

Co-authored-by: Luke Lashley <[email protected]>

* fix imports

* Simplify

* Simplify

* Fix feedback

* Test if this makes changes clearer

* Test if this makes changes clearer

* Fix tests

* Remove name

* Fix feedback

---------

Co-authored-by: Luke Lashley <[email protected]>
  • Loading branch information
joostlek and Lash-L authored Sep 26, 2023
1 parent 8ba6fd7 commit 4f63c79
Show file tree
Hide file tree
Showing 12 changed files with 394 additions and 876 deletions.
144 changes: 70 additions & 74 deletions homeassistant/components/withings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"""
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable

from aiohttp.hdrs import METH_HEAD, METH_POST
from aiohttp.web import Request, Response
import voluptuous as vol
from withings_api.common import NotifyAppli
Expand All @@ -15,6 +16,7 @@
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,
Expand All @@ -28,17 +30,13 @@
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import 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.typing import ConfigType

from . import const
from .common import (
async_get_data_manager,
async_remove_data_manager,
get_data_manager_by_webhook_id,
json_message_response,
)
from .api import ConfigEntryWithingsApi
from .common import WithingsDataUpdateCoordinator
from .const import CONF_USE_WEBHOOK, CONFIG, LOGGER

DOMAIN = const.DOMAIN
Expand All @@ -56,7 +54,7 @@
vol.Optional(CONF_CLIENT_SECRET): vol.All(
cv.string, vol.Length(min=1)
),
vol.Optional(const.CONF_USE_WEBHOOK, default=False): cv.boolean,
vol.Optional(const.CONF_USE_WEBHOOK): cv.boolean,
vol.Optional(const.CONF_PROFILES): vol.All(
cv.ensure_list,
vol.Unique(),
Expand Down Expand Up @@ -116,37 +114,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_update_entry(
entry, data=new_data, options=new_options, unique_id=unique_id
)
use_webhook = hass.data[DOMAIN][CONFIG][CONF_USE_WEBHOOK]
if use_webhook is not None and use_webhook != entry.options[CONF_USE_WEBHOOK]:
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)

data_manager = await async_get_data_manager(hass, entry)

LOGGER.debug("Confirming %s is authenticated to withings", entry.title)
await data_manager.poll_data_update_coordinator.async_config_entry_first_refresh()

webhook.async_register(
hass,
const.DOMAIN,
"Withings notify",
data_manager.webhook_config.id,
async_webhook_handler,
client = ConfigEntryWithingsApi(
hass=hass,
config_entry=entry,
implementation=await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
),
)

# Perform first webhook subscription check.
if data_manager.webhook_config.enabled:
data_manager.async_start_polling_webhook_subscriptions()
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(
data_manager.subscription_update_coordinator.async_refresh()
)
hass.async_create_task(coordinator.async_subscribe_webhooks())

# Start subscription check in the background, outside this component's setup.
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()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
Expand All @@ -156,64 +158,58 @@ def async_call_later_callback(now) -> None:

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Withings config entry."""
data_manager = await async_get_data_manager(hass, entry)
data_manager.async_stop_polling_webhook_subscriptions()

async_unregister_webhook(hass, data_manager.webhook_config.id)
if entry.options[CONF_USE_WEBHOOK]:
async_unregister_webhook(hass, entry.data[CONF_WEBHOOK_ID])

await asyncio.gather(
data_manager.async_unsubscribe_webhook(),
hass.config_entries.async_unload_platforms(entry, PLATFORMS),
)

async_remove_data_manager(hass, entry)

return True
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok


async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_webhook_handler(
hass: HomeAssistant, webhook_id: str, request: Request
) -> Response | None:
"""Handle webhooks calls."""
# Handle http head calls to the path.
# When creating a notify subscription, Withings will check that the endpoint is running by sending a HEAD request.
if request.method.upper() == "HEAD":
return Response()
def json_message_response(message: str, message_code: int) -> Response:
"""Produce common json output."""
return HomeAssistantView.json({"message": message, "code": message_code})

if request.method.upper() != "POST":
return json_message_response("Invalid method", message_code=2)

# Handle http post calls to the path.
if not request.body_exists:
return json_message_response("No request body", message_code=12)
def get_webhook_handler(
coordinator: WithingsDataUpdateCoordinator,
) -> Callable[[HomeAssistant, str, Request], Awaitable[Response | None]]:
"""Return webhook handler."""

params = await request.post()
async def async_webhook_handler(
hass: HomeAssistant, webhook_id: str, request: Request
) -> Response | None:
# Handle http head calls to the path.
# When creating a notify subscription, Withings will check that the endpoint is running by sending a HEAD request.
if request.method == METH_HEAD:
return Response()

if "appli" not in params:
return json_message_response("Parameter appli not provided", message_code=20)
if request.method != METH_POST:
return json_message_response("Invalid method", message_code=2)

try:
appli = NotifyAppli(int(params.getone("appli"))) # type: ignore[arg-type]
except ValueError:
return json_message_response("Invalid appli provided", message_code=21)
# Handle http post calls to the path.
if not request.body_exists:
return json_message_response("No request body", message_code=12)

data_manager = get_data_manager_by_webhook_id(hass, webhook_id)
if not data_manager:
LOGGER.error(
(
"Webhook id %s not handled by data manager. This is a bug and should be"
" reported"
),
webhook_id,
)
return json_message_response("User not found", message_code=1)
params = await request.post()

if "appli" not in params:
return json_message_response(
"Parameter appli not provided", message_code=20
)

try:
appli = NotifyAppli(int(params.getone("appli"))) # type: ignore[arg-type]
except ValueError:
return json_message_response("Invalid appli provided", message_code=21)

await coordinator.async_webhook_data_updated(appli)

# Run this in the background and return immediately.
hass.async_create_task(data_manager.async_webhook_data_updated(appli))
return json_message_response("Success", message_code=0)

return json_message_response("Success", message_code=0)
return async_webhook_handler
23 changes: 11 additions & 12 deletions homeassistant/components/withings/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .common import UpdateType, async_get_data_manager
from .const import Measurement
from .entity import BaseWithingsSensor, WithingsEntityDescription
from .common import WithingsDataUpdateCoordinator
from .const import DOMAIN, Measurement
from .entity import WithingsEntity, WithingsEntityDescription


@dataclass
Expand All @@ -34,7 +34,6 @@ class WithingsBinarySensorEntityDescription(
measure_type=NotifyAppli.BED_IN,
translation_key="in_bed",
icon="mdi:bed",
update_type=UpdateType.WEBHOOK,
device_class=BinarySensorDeviceClass.OCCUPANCY,
),
]
Expand All @@ -46,22 +45,22 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor config entry."""
data_manager = await async_get_data_manager(hass, entry)
coordinator: WithingsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]

entities = [
WithingsHealthBinarySensor(data_manager, attribute)
for attribute in BINARY_SENSORS
]
if coordinator.use_webhooks:
entities = [
WithingsBinarySensor(coordinator, attribute) for attribute in BINARY_SENSORS
]

async_add_entities(entities, True)
async_add_entities(entities)


class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity):
class WithingsBinarySensor(WithingsEntity, BinarySensorEntity):
"""Implementation of a Withings sensor."""

entity_description: WithingsBinarySensorEntityDescription

@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self._state_data
return self.coordinator.in_bed
Loading

0 comments on commit 4f63c79

Please sign in to comment.