mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Use aiowithings (#101819)
* Subscribe to Withings webhooks outside of coordinator * Subscribe to Withings webhooks outside of coordinator * Split Withings coordinator * Split Withings coordinator * Use aiowithings * Use aiowithings * Use aiowithings * Update homeassistant/components/withings/sensor.py * Merge * Remove startdate * Minor fixes * Bump to 0.4.1 * Fix snapshot * Fix datapoint * Bump to 0.4.2 * Bump to 0.4.3 * Bump to 0.4.4
This commit is contained in:
parent
89d86fe983
commit
8a4fe5add1
@ -1514,7 +1514,6 @@ omit =
|
|||||||
homeassistant/components/wiffi/sensor.py
|
homeassistant/components/wiffi/sensor.py
|
||||||
homeassistant/components/wiffi/wiffi_strings.py
|
homeassistant/components/wiffi/wiffi_strings.py
|
||||||
homeassistant/components/wirelesstag/*
|
homeassistant/components/wirelesstag/*
|
||||||
homeassistant/components/withings/api.py
|
|
||||||
homeassistant/components/wolflink/__init__.py
|
homeassistant/components/wolflink/__init__.py
|
||||||
homeassistant/components/wolflink/sensor.py
|
homeassistant/components/wolflink/sensor.py
|
||||||
homeassistant/components/worldtidesinfo/sensor.py
|
homeassistant/components/worldtidesinfo/sensor.py
|
||||||
|
@ -12,8 +12,9 @@ from typing import Any
|
|||||||
|
|
||||||
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
|
||||||
|
from aiowithings import NotificationCategory, WithingsClient
|
||||||
|
from aiowithings.util import to_enum
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from withings_api.common import NotifyAppli
|
|
||||||
|
|
||||||
from homeassistant.components import cloud
|
from homeassistant.components import cloud
|
||||||
from homeassistant.components.application_credentials import (
|
from homeassistant.components.application_credentials import (
|
||||||
@ -29,6 +30,7 @@ from homeassistant.components.webhook import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
CONF_CLIENT_ID,
|
CONF_CLIENT_ID,
|
||||||
CONF_CLIENT_SECRET,
|
CONF_CLIENT_SECRET,
|
||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
@ -37,12 +39,16 @@ from homeassistant.const import (
|
|||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||||
|
OAuth2Session,
|
||||||
|
async_get_config_entry_implementation,
|
||||||
|
)
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .api import ConfigEntryWithingsApi
|
|
||||||
from .const import (
|
from .const import (
|
||||||
BED_PRESENCE_COORDINATOR,
|
BED_PRESENCE_COORDINATOR,
|
||||||
CONF_PROFILES,
|
CONF_PROFILES,
|
||||||
@ -134,14 +140,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry, data=new_data, unique_id=unique_id
|
entry, data=new_data, unique_id=unique_id
|
||||||
)
|
)
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
client = WithingsClient(session=session)
|
||||||
|
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||||
|
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||||
|
|
||||||
client = ConfigEntryWithingsApi(
|
async def _refresh_token() -> str:
|
||||||
hass=hass,
|
await oauth_session.async_ensure_token_valid()
|
||||||
config_entry=entry,
|
return oauth_session.token[CONF_ACCESS_TOKEN]
|
||||||
implementation=await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
|
||||||
hass, entry
|
client.refresh_token_function = _refresh_token
|
||||||
),
|
|
||||||
)
|
|
||||||
coordinators: dict[str, WithingsDataUpdateCoordinator] = {
|
coordinators: dict[str, WithingsDataUpdateCoordinator] = {
|
||||||
MEASUREMENT_COORDINATOR: WithingsMeasurementDataUpdateCoordinator(hass, client),
|
MEASUREMENT_COORDINATOR: WithingsMeasurementDataUpdateCoordinator(hass, client),
|
||||||
SLEEP_COORDINATOR: WithingsSleepDataUpdateCoordinator(hass, client),
|
SLEEP_COORDINATOR: WithingsSleepDataUpdateCoordinator(hass, client),
|
||||||
@ -230,19 +238,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def async_subscribe_webhooks(
|
async def async_subscribe_webhooks(client: WithingsClient, webhook_url: str) -> None:
|
||||||
client: ConfigEntryWithingsApi, webhook_url: str
|
|
||||||
) -> None:
|
|
||||||
"""Subscribe to Withings webhooks."""
|
"""Subscribe to Withings webhooks."""
|
||||||
await async_unsubscribe_webhooks(client)
|
await async_unsubscribe_webhooks(client)
|
||||||
|
|
||||||
notification_to_subscribe = {
|
notification_to_subscribe = {
|
||||||
NotifyAppli.WEIGHT,
|
NotificationCategory.WEIGHT,
|
||||||
NotifyAppli.CIRCULATORY,
|
NotificationCategory.PRESSURE,
|
||||||
NotifyAppli.ACTIVITY,
|
NotificationCategory.ACTIVITY,
|
||||||
NotifyAppli.SLEEP,
|
NotificationCategory.SLEEP,
|
||||||
NotifyAppli.BED_IN,
|
NotificationCategory.IN_BED,
|
||||||
NotifyAppli.BED_OUT,
|
NotificationCategory.OUT_BED,
|
||||||
}
|
}
|
||||||
|
|
||||||
for notification in notification_to_subscribe:
|
for notification in notification_to_subscribe:
|
||||||
@ -255,25 +261,26 @@ async def async_subscribe_webhooks(
|
|||||||
# 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 client.async_notify_subscribe(webhook_url, notification)
|
await client.subscribe_notification(webhook_url, notification)
|
||||||
|
|
||||||
|
|
||||||
async def async_unsubscribe_webhooks(client: ConfigEntryWithingsApi) -> None:
|
async def async_unsubscribe_webhooks(client: WithingsClient) -> None:
|
||||||
"""Unsubscribe to all Withings webhooks."""
|
"""Unsubscribe to all Withings webhooks."""
|
||||||
current_webhooks = await client.async_notify_list()
|
current_webhooks = await client.list_notification_configurations()
|
||||||
|
|
||||||
for webhook_configuration in current_webhooks.profiles:
|
for webhook_configuration in current_webhooks:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Unsubscribing %s for %s in %s seconds",
|
"Unsubscribing %s for %s in %s seconds",
|
||||||
webhook_configuration.callbackurl,
|
webhook_configuration.callback_url,
|
||||||
webhook_configuration.appli,
|
webhook_configuration.notification_category,
|
||||||
UNSUBSCRIBE_DELAY.total_seconds(),
|
UNSUBSCRIBE_DELAY.total_seconds(),
|
||||||
)
|
)
|
||||||
# Quick calls to Withings can result in the service returning errors.
|
# Quick calls to Withings can result in the service returning errors.
|
||||||
# Give them some time to cool down.
|
# Give them some time to cool down.
|
||||||
await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds())
|
await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds())
|
||||||
await client.async_notify_revoke(
|
await client.revoke_notification_configurations(
|
||||||
webhook_configuration.callbackurl, webhook_configuration.appli
|
webhook_configuration.callback_url,
|
||||||
|
webhook_configuration.notification_category,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -336,14 +343,15 @@ def get_webhook_handler(
|
|||||||
"Parameter appli not provided", message_code=20
|
"Parameter appli not provided", message_code=20
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
notification_category = to_enum(
|
||||||
appli = NotifyAppli(int(params.getone("appli"))) # type: ignore[arg-type]
|
NotificationCategory,
|
||||||
except ValueError:
|
int(params.getone("appli")), # type: ignore[arg-type]
|
||||||
return json_message_response("Invalid appli provided", message_code=21)
|
NotificationCategory.UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
for coordinator in coordinators.values():
|
for coordinator in coordinators.values():
|
||||||
if appli in coordinator.notification_categories:
|
if notification_category in coordinator.notification_categories:
|
||||||
await coordinator.async_webhook_data_updated(appli)
|
await coordinator.async_webhook_data_updated(notification_category)
|
||||||
|
|
||||||
return json_message_response("Success", message_code=0)
|
return json_message_response("Success", message_code=0)
|
||||||
|
|
||||||
|
@ -1,170 +0,0 @@
|
|||||||
"""Api for Withings."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Awaitable, Callable, Iterable
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import arrow
|
|
||||||
import requests
|
|
||||||
from withings_api import AbstractWithingsApi, DateType
|
|
||||||
from withings_api.common import (
|
|
||||||
GetSleepSummaryField,
|
|
||||||
MeasureGetMeasGroupCategory,
|
|
||||||
MeasureGetMeasResponse,
|
|
||||||
MeasureType,
|
|
||||||
NotifyAppli,
|
|
||||||
NotifyListResponse,
|
|
||||||
SleepGetSummaryResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
|
||||||
AbstractOAuth2Implementation,
|
|
||||||
OAuth2Session,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import LOGGER
|
|
||||||
|
|
||||||
_RETRY_COEFFICIENT = 0.5
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigEntryWithingsApi(AbstractWithingsApi):
|
|
||||||
"""Withing API that uses HA resources."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: ConfigEntry,
|
|
||||||
implementation: AbstractOAuth2Implementation,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize object."""
|
|
||||||
self._hass = hass
|
|
||||||
self.config_entry = config_entry
|
|
||||||
self._implementation = implementation
|
|
||||||
self.session = OAuth2Session(hass, config_entry, implementation)
|
|
||||||
|
|
||||||
def _request(
|
|
||||||
self, path: str, params: dict[str, Any], method: str = "GET"
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Perform an async request."""
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
self.session.async_ensure_token_valid(), self._hass.loop
|
|
||||||
).result()
|
|
||||||
|
|
||||||
access_token = self.config_entry.data["token"]["access_token"]
|
|
||||||
response = requests.request(
|
|
||||||
method,
|
|
||||||
f"{self.URL}/{path}",
|
|
||||||
params=params,
|
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def _do_retry(self, func: Callable[[], Awaitable[Any]], attempts=3) -> Any:
|
|
||||||
"""Retry a function call.
|
|
||||||
|
|
||||||
Withings' API occasionally and incorrectly throws errors.
|
|
||||||
Retrying the call tends to work.
|
|
||||||
"""
|
|
||||||
exception = None
|
|
||||||
for attempt in range(1, attempts + 1):
|
|
||||||
LOGGER.debug("Attempt %s of %s", attempt, attempts)
|
|
||||||
try:
|
|
||||||
return await func()
|
|
||||||
except Exception as exception1: # pylint: disable=broad-except
|
|
||||||
LOGGER.debug(
|
|
||||||
"Failed attempt %s of %s (%s)", attempt, attempts, exception1
|
|
||||||
)
|
|
||||||
# Make each backoff pause a little bit longer
|
|
||||||
await asyncio.sleep(_RETRY_COEFFICIENT * attempt)
|
|
||||||
exception = exception1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if exception:
|
|
||||||
raise exception
|
|
||||||
|
|
||||||
async def async_measure_get_meas(
|
|
||||||
self,
|
|
||||||
meastype: MeasureType | None = None,
|
|
||||||
category: MeasureGetMeasGroupCategory | None = None,
|
|
||||||
startdate: DateType | None = arrow.utcnow(),
|
|
||||||
enddate: DateType | None = arrow.utcnow(),
|
|
||||||
offset: int | None = None,
|
|
||||||
lastupdate: DateType | None = arrow.utcnow(),
|
|
||||||
) -> MeasureGetMeasResponse:
|
|
||||||
"""Get measurements."""
|
|
||||||
|
|
||||||
async def call_super() -> MeasureGetMeasResponse:
|
|
||||||
return await self._hass.async_add_executor_job(
|
|
||||||
self.measure_get_meas,
|
|
||||||
meastype,
|
|
||||||
category,
|
|
||||||
startdate,
|
|
||||||
enddate,
|
|
||||||
offset,
|
|
||||||
lastupdate,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await self._do_retry(call_super)
|
|
||||||
|
|
||||||
async def async_sleep_get_summary(
|
|
||||||
self,
|
|
||||||
data_fields: Iterable[GetSleepSummaryField],
|
|
||||||
startdateymd: DateType | None = arrow.utcnow(),
|
|
||||||
enddateymd: DateType | None = arrow.utcnow(),
|
|
||||||
offset: int | None = None,
|
|
||||||
lastupdate: DateType | None = arrow.utcnow(),
|
|
||||||
) -> SleepGetSummaryResponse:
|
|
||||||
"""Get sleep data."""
|
|
||||||
|
|
||||||
async def call_super() -> SleepGetSummaryResponse:
|
|
||||||
return await self._hass.async_add_executor_job(
|
|
||||||
self.sleep_get_summary,
|
|
||||||
data_fields,
|
|
||||||
startdateymd,
|
|
||||||
enddateymd,
|
|
||||||
offset,
|
|
||||||
lastupdate,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await self._do_retry(call_super)
|
|
||||||
|
|
||||||
async def async_notify_list(
|
|
||||||
self, appli: NotifyAppli | None = None
|
|
||||||
) -> NotifyListResponse:
|
|
||||||
"""List webhooks."""
|
|
||||||
|
|
||||||
async def call_super() -> NotifyListResponse:
|
|
||||||
return await self._hass.async_add_executor_job(self.notify_list, appli)
|
|
||||||
|
|
||||||
return await self._do_retry(call_super)
|
|
||||||
|
|
||||||
async def async_notify_subscribe(
|
|
||||||
self,
|
|
||||||
callbackurl: str,
|
|
||||||
appli: NotifyAppli | None = None,
|
|
||||||
comment: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Subscribe to webhook."""
|
|
||||||
|
|
||||||
async def call_super() -> None:
|
|
||||||
await self._hass.async_add_executor_job(
|
|
||||||
self.notify_subscribe, callbackurl, appli, comment
|
|
||||||
)
|
|
||||||
|
|
||||||
await self._do_retry(call_super)
|
|
||||||
|
|
||||||
async def async_notify_revoke(
|
|
||||||
self, callbackurl: str | None = None, appli: NotifyAppli | None = None
|
|
||||||
) -> None:
|
|
||||||
"""Revoke webhook."""
|
|
||||||
|
|
||||||
async def call_super() -> None:
|
|
||||||
await self._hass.async_add_executor_job(
|
|
||||||
self.notify_revoke, callbackurl, appli
|
|
||||||
)
|
|
||||||
|
|
||||||
await self._do_retry(call_super)
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from withings_api import AbstractWithingsApi, WithingsAuth
|
from aiowithings import AUTHORIZATION_URL, TOKEN_URL
|
||||||
|
|
||||||
from homeassistant.components.application_credentials import (
|
from homeassistant.components.application_credentials import (
|
||||||
AuthImplementation,
|
AuthImplementation,
|
||||||
@ -24,8 +24,8 @@ async def async_get_auth_implementation(
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
credential,
|
credential,
|
||||||
authorization_server=AuthorizationServer(
|
authorization_server=AuthorizationServer(
|
||||||
authorize_url=f"{WithingsAuth.URL}/oauth2_user/authorize2",
|
authorize_url=AUTHORIZATION_URL,
|
||||||
token_url=f"{AbstractWithingsApi.URL}/v2/oauth2",
|
token_url=TOKEN_URL,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from collections.abc import Mapping
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from withings_api.common import AuthScope
|
from aiowithings import AuthScope
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_id
|
from homeassistant.components.webhook import async_generate_id
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -36,10 +36,10 @@ class WithingsFlowHandler(
|
|||||||
return {
|
return {
|
||||||
"scope": ",".join(
|
"scope": ",".join(
|
||||||
[
|
[
|
||||||
AuthScope.USER_INFO.value,
|
AuthScope.USER_INFO,
|
||||||
AuthScope.USER_METRICS.value,
|
AuthScope.USER_METRICS,
|
||||||
AuthScope.USER_ACTIVITY.value,
|
AuthScope.USER_ACTIVITY,
|
||||||
AuthScope.USER_SLEEP_EVENTS.value,
|
AuthScope.USER_SLEEP_EVENTS,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Constants used by the Withings component."""
|
"""Constants used by the Withings component."""
|
||||||
from enum import StrEnum
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
DEFAULT_TITLE = "Withings"
|
DEFAULT_TITLE = "Withings"
|
||||||
@ -21,45 +20,6 @@ BED_PRESENCE_COORDINATOR = "bed_presence_coordinator"
|
|||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
|
||||||
class Measurement(StrEnum):
|
|
||||||
"""Measurement supported by the withings integration."""
|
|
||||||
|
|
||||||
BODY_TEMP_C = "body_temperature_c"
|
|
||||||
BONE_MASS_KG = "bone_mass_kg"
|
|
||||||
DIASTOLIC_MMHG = "diastolic_blood_pressure_mmhg"
|
|
||||||
FAT_FREE_MASS_KG = "fat_free_mass_kg"
|
|
||||||
FAT_MASS_KG = "fat_mass_kg"
|
|
||||||
FAT_RATIO_PCT = "fat_ratio_pct"
|
|
||||||
HEART_PULSE_BPM = "heart_pulse_bpm"
|
|
||||||
HEIGHT_M = "height_m"
|
|
||||||
HYDRATION = "hydration"
|
|
||||||
IN_BED = "in_bed"
|
|
||||||
MUSCLE_MASS_KG = "muscle_mass_kg"
|
|
||||||
PWV = "pulse_wave_velocity"
|
|
||||||
SKIN_TEMP_C = "skin_temperature_c"
|
|
||||||
SLEEP_BREATHING_DISTURBANCES_INTENSITY = "sleep_breathing_disturbances_intensity"
|
|
||||||
SLEEP_DEEP_DURATION_SECONDS = "sleep_deep_duration_seconds"
|
|
||||||
SLEEP_HEART_RATE_AVERAGE = "sleep_heart_rate_average_bpm"
|
|
||||||
SLEEP_HEART_RATE_MAX = "sleep_heart_rate_max_bpm"
|
|
||||||
SLEEP_HEART_RATE_MIN = "sleep_heart_rate_min_bpm"
|
|
||||||
SLEEP_LIGHT_DURATION_SECONDS = "sleep_light_duration_seconds"
|
|
||||||
SLEEP_REM_DURATION_SECONDS = "sleep_rem_duration_seconds"
|
|
||||||
SLEEP_RESPIRATORY_RATE_AVERAGE = "sleep_respiratory_average_bpm"
|
|
||||||
SLEEP_RESPIRATORY_RATE_MAX = "sleep_respiratory_max_bpm"
|
|
||||||
SLEEP_RESPIRATORY_RATE_MIN = "sleep_respiratory_min_bpm"
|
|
||||||
SLEEP_SCORE = "sleep_score"
|
|
||||||
SLEEP_SNORING = "sleep_snoring"
|
|
||||||
SLEEP_SNORING_EPISODE_COUNT = "sleep_snoring_eposode_count"
|
|
||||||
SLEEP_TOSLEEP_DURATION_SECONDS = "sleep_tosleep_duration_seconds"
|
|
||||||
SLEEP_TOWAKEUP_DURATION_SECONDS = "sleep_towakeup_duration_seconds"
|
|
||||||
SLEEP_WAKEUP_COUNT = "sleep_wakeup_count"
|
|
||||||
SLEEP_WAKEUP_DURATION_SECONDS = "sleep_wakeup_duration_seconds"
|
|
||||||
SPO2_PCT = "spo2_pct"
|
|
||||||
SYSTOLIC_MMGH = "systolic_blood_pressure_mmhg"
|
|
||||||
TEMP_C = "temperature_c"
|
|
||||||
WEIGHT_KG = "weight_kg"
|
|
||||||
|
|
||||||
|
|
||||||
SCORE_POINTS = "points"
|
SCORE_POINTS = "points"
|
||||||
UOM_BEATS_PER_MINUTE = "bpm"
|
UOM_BEATS_PER_MINUTE = "bpm"
|
||||||
UOM_BREATHS_PER_MINUTE = "br/min"
|
UOM_BREATHS_PER_MINUTE = "br/min"
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
"""Withings coordinator."""
|
"""Withings coordinator."""
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import Callable
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from withings_api.common import (
|
from aiowithings import (
|
||||||
AuthFailedException,
|
MeasurementType,
|
||||||
GetSleepSummaryField,
|
NotificationCategory,
|
||||||
MeasureGroupAttribs,
|
SleepSummary,
|
||||||
MeasureType,
|
SleepSummaryDataFields,
|
||||||
MeasureTypes,
|
WithingsAuthenticationFailedError,
|
||||||
NotifyAppli,
|
WithingsClient,
|
||||||
UnauthorizedException,
|
WithingsUnauthorizedError,
|
||||||
query_measure_groups,
|
aggregate_measurements,
|
||||||
)
|
)
|
||||||
|
from aiowithings.helpers import aggregate_sleep_summary
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -21,51 +21,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .api import ConfigEntryWithingsApi
|
from .const import LOGGER
|
||||||
from .const import LOGGER, Measurement
|
|
||||||
|
|
||||||
WITHINGS_MEASURE_TYPE_MAP: dict[
|
|
||||||
NotifyAppli | GetSleepSummaryField | MeasureType, Measurement
|
|
||||||
] = {
|
|
||||||
MeasureType.WEIGHT: Measurement.WEIGHT_KG,
|
|
||||||
MeasureType.FAT_MASS_WEIGHT: Measurement.FAT_MASS_KG,
|
|
||||||
MeasureType.FAT_FREE_MASS: Measurement.FAT_FREE_MASS_KG,
|
|
||||||
MeasureType.MUSCLE_MASS: Measurement.MUSCLE_MASS_KG,
|
|
||||||
MeasureType.BONE_MASS: Measurement.BONE_MASS_KG,
|
|
||||||
MeasureType.HEIGHT: Measurement.HEIGHT_M,
|
|
||||||
MeasureType.TEMPERATURE: Measurement.TEMP_C,
|
|
||||||
MeasureType.BODY_TEMPERATURE: Measurement.BODY_TEMP_C,
|
|
||||||
MeasureType.SKIN_TEMPERATURE: Measurement.SKIN_TEMP_C,
|
|
||||||
MeasureType.FAT_RATIO: Measurement.FAT_RATIO_PCT,
|
|
||||||
MeasureType.DIASTOLIC_BLOOD_PRESSURE: Measurement.DIASTOLIC_MMHG,
|
|
||||||
MeasureType.SYSTOLIC_BLOOD_PRESSURE: Measurement.SYSTOLIC_MMGH,
|
|
||||||
MeasureType.HEART_RATE: Measurement.HEART_PULSE_BPM,
|
|
||||||
MeasureType.SP02: Measurement.SPO2_PCT,
|
|
||||||
MeasureType.HYDRATION: Measurement.HYDRATION,
|
|
||||||
MeasureType.PULSE_WAVE_VELOCITY: Measurement.PWV,
|
|
||||||
GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY: (
|
|
||||||
Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY
|
|
||||||
),
|
|
||||||
GetSleepSummaryField.DEEP_SLEEP_DURATION: Measurement.SLEEP_DEEP_DURATION_SECONDS,
|
|
||||||
GetSleepSummaryField.DURATION_TO_SLEEP: Measurement.SLEEP_TOSLEEP_DURATION_SECONDS,
|
|
||||||
GetSleepSummaryField.DURATION_TO_WAKEUP: (
|
|
||||||
Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS
|
|
||||||
),
|
|
||||||
GetSleepSummaryField.HR_AVERAGE: Measurement.SLEEP_HEART_RATE_AVERAGE,
|
|
||||||
GetSleepSummaryField.HR_MAX: Measurement.SLEEP_HEART_RATE_MAX,
|
|
||||||
GetSleepSummaryField.HR_MIN: Measurement.SLEEP_HEART_RATE_MIN,
|
|
||||||
GetSleepSummaryField.LIGHT_SLEEP_DURATION: Measurement.SLEEP_LIGHT_DURATION_SECONDS,
|
|
||||||
GetSleepSummaryField.REM_SLEEP_DURATION: Measurement.SLEEP_REM_DURATION_SECONDS,
|
|
||||||
GetSleepSummaryField.RR_AVERAGE: Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE,
|
|
||||||
GetSleepSummaryField.RR_MAX: Measurement.SLEEP_RESPIRATORY_RATE_MAX,
|
|
||||||
GetSleepSummaryField.RR_MIN: Measurement.SLEEP_RESPIRATORY_RATE_MIN,
|
|
||||||
GetSleepSummaryField.SLEEP_SCORE: Measurement.SLEEP_SCORE,
|
|
||||||
GetSleepSummaryField.SNORING: Measurement.SLEEP_SNORING,
|
|
||||||
GetSleepSummaryField.SNORING_EPISODE_COUNT: Measurement.SLEEP_SNORING_EPISODE_COUNT,
|
|
||||||
GetSleepSummaryField.WAKEUP_COUNT: Measurement.SLEEP_WAKEUP_COUNT,
|
|
||||||
GetSleepSummaryField.WAKEUP_DURATION: Measurement.SLEEP_WAKEUP_DURATION_SECONDS,
|
|
||||||
NotifyAppli.BED_IN: Measurement.IN_BED,
|
|
||||||
}
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
@ -78,13 +34,13 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
|
|||||||
config_entry: ConfigEntry
|
config_entry: ConfigEntry
|
||||||
_default_update_interval: timedelta | None = UPDATE_INTERVAL
|
_default_update_interval: timedelta | None = UPDATE_INTERVAL
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
|
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
|
||||||
"""Initialize the Withings data coordinator."""
|
"""Initialize the Withings data coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass, LOGGER, name="Withings", update_interval=self._default_update_interval
|
hass, LOGGER, name="Withings", update_interval=self._default_update_interval
|
||||||
)
|
)
|
||||||
self._client = client
|
self._client = client
|
||||||
self.notification_categories: set[NotifyAppli] = set()
|
self.notification_categories: set[NotificationCategory] = set()
|
||||||
|
|
||||||
def webhook_subscription_listener(self, connected: bool) -> None:
|
def webhook_subscription_listener(self, connected: bool) -> None:
|
||||||
"""Call when webhook status changed."""
|
"""Call when webhook status changed."""
|
||||||
@ -94,7 +50,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
|
|||||||
self.update_interval = self._default_update_interval
|
self.update_interval = self._default_update_interval
|
||||||
|
|
||||||
async def async_webhook_data_updated(
|
async def async_webhook_data_updated(
|
||||||
self, notification_category: NotifyAppli
|
self, notification_category: NotificationCategory
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update data when webhook is called."""
|
"""Update data when webhook is called."""
|
||||||
LOGGER.debug("Withings webhook triggered for %s", notification_category)
|
LOGGER.debug("Withings webhook triggered for %s", notification_category)
|
||||||
@ -103,7 +59,7 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
|
|||||||
async def _async_update_data(self) -> _T:
|
async def _async_update_data(self) -> _T:
|
||||||
try:
|
try:
|
||||||
return await self._internal_update_data()
|
return await self._internal_update_data()
|
||||||
except (UnauthorizedException, AuthFailedException) as exc:
|
except (WithingsUnauthorizedError, WithingsAuthenticationFailedError) as exc:
|
||||||
raise ConfigEntryAuthFailed from exc
|
raise ConfigEntryAuthFailed from exc
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -112,136 +68,71 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
|
|||||||
|
|
||||||
|
|
||||||
class WithingsMeasurementDataUpdateCoordinator(
|
class WithingsMeasurementDataUpdateCoordinator(
|
||||||
WithingsDataUpdateCoordinator[dict[Measurement, Any]]
|
WithingsDataUpdateCoordinator[dict[MeasurementType, float]]
|
||||||
):
|
):
|
||||||
"""Withings measurement coordinator."""
|
"""Withings measurement coordinator."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
|
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
|
||||||
"""Initialize the Withings data coordinator."""
|
"""Initialize the Withings data coordinator."""
|
||||||
super().__init__(hass, client)
|
super().__init__(hass, client)
|
||||||
self.notification_categories = {
|
self.notification_categories = {
|
||||||
NotifyAppli.WEIGHT,
|
NotificationCategory.WEIGHT,
|
||||||
NotifyAppli.ACTIVITY,
|
NotificationCategory.ACTIVITY,
|
||||||
NotifyAppli.CIRCULATORY,
|
NotificationCategory.PRESSURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _internal_update_data(self) -> dict[Measurement, Any]:
|
async def _internal_update_data(self) -> dict[MeasurementType, float]:
|
||||||
"""Retrieve measurement data."""
|
"""Retrieve measurement data."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
startdate = now - timedelta(days=7)
|
startdate = now - timedelta(days=7)
|
||||||
|
|
||||||
response = await self._client.async_measure_get_meas(
|
response = await self._client.get_measurement_in_period(startdate, now)
|
||||||
None, None, startdate, now, None, startdate
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort from oldest to newest.
|
return aggregate_measurements(response)
|
||||||
groups = sorted(
|
|
||||||
query_measure_groups(
|
|
||||||
response, MeasureTypes.ANY, MeasureGroupAttribs.UNAMBIGUOUS
|
|
||||||
),
|
|
||||||
key=lambda group: group.created.datetime,
|
|
||||||
reverse=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
WITHINGS_MEASURE_TYPE_MAP[measure.type]: round(
|
|
||||||
float(measure.value * pow(10, measure.unit)), 2
|
|
||||||
)
|
|
||||||
for group in groups
|
|
||||||
for measure in group.measures
|
|
||||||
if measure.type in WITHINGS_MEASURE_TYPE_MAP
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class WithingsSleepDataUpdateCoordinator(
|
class WithingsSleepDataUpdateCoordinator(
|
||||||
WithingsDataUpdateCoordinator[dict[Measurement, Any]]
|
WithingsDataUpdateCoordinator[SleepSummary | None]
|
||||||
):
|
):
|
||||||
"""Withings sleep coordinator."""
|
"""Withings sleep coordinator."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
|
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
|
||||||
"""Initialize the Withings data coordinator."""
|
"""Initialize the Withings data coordinator."""
|
||||||
super().__init__(hass, client)
|
super().__init__(hass, client)
|
||||||
self.notification_categories = {
|
self.notification_categories = {
|
||||||
NotifyAppli.SLEEP,
|
NotificationCategory.SLEEP,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _internal_update_data(self) -> dict[Measurement, Any]:
|
async def _internal_update_data(self) -> SleepSummary | None:
|
||||||
"""Retrieve sleep data."""
|
"""Retrieve sleep data."""
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
yesterday = now - timedelta(days=1)
|
yesterday = now - timedelta(days=1)
|
||||||
yesterday_noon = dt_util.start_of_local_day(yesterday) + timedelta(hours=12)
|
yesterday_noon = dt_util.start_of_local_day(yesterday) + timedelta(hours=12)
|
||||||
yesterday_noon_utc = dt_util.as_utc(yesterday_noon)
|
yesterday_noon_utc = dt_util.as_utc(yesterday_noon)
|
||||||
|
|
||||||
response = await self._client.async_sleep_get_summary(
|
response = await self._client.get_sleep_summary_since(
|
||||||
lastupdate=yesterday_noon_utc,
|
sleep_summary_since=yesterday_noon_utc,
|
||||||
data_fields=[
|
sleep_summary_data_fields=[
|
||||||
GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY,
|
SleepSummaryDataFields.BREATHING_DISTURBANCES_INTENSITY,
|
||||||
GetSleepSummaryField.DEEP_SLEEP_DURATION,
|
SleepSummaryDataFields.DEEP_SLEEP_DURATION,
|
||||||
GetSleepSummaryField.DURATION_TO_SLEEP,
|
SleepSummaryDataFields.SLEEP_LATENCY,
|
||||||
GetSleepSummaryField.DURATION_TO_WAKEUP,
|
SleepSummaryDataFields.WAKE_UP_LATENCY,
|
||||||
GetSleepSummaryField.HR_AVERAGE,
|
SleepSummaryDataFields.AVERAGE_HEART_RATE,
|
||||||
GetSleepSummaryField.HR_MAX,
|
SleepSummaryDataFields.MIN_HEART_RATE,
|
||||||
GetSleepSummaryField.HR_MIN,
|
SleepSummaryDataFields.MAX_HEART_RATE,
|
||||||
GetSleepSummaryField.LIGHT_SLEEP_DURATION,
|
SleepSummaryDataFields.LIGHT_SLEEP_DURATION,
|
||||||
GetSleepSummaryField.REM_SLEEP_DURATION,
|
SleepSummaryDataFields.REM_SLEEP_DURATION,
|
||||||
GetSleepSummaryField.RR_AVERAGE,
|
SleepSummaryDataFields.AVERAGE_RESPIRATION_RATE,
|
||||||
GetSleepSummaryField.RR_MAX,
|
SleepSummaryDataFields.MIN_RESPIRATION_RATE,
|
||||||
GetSleepSummaryField.RR_MIN,
|
SleepSummaryDataFields.MAX_RESPIRATION_RATE,
|
||||||
GetSleepSummaryField.SLEEP_SCORE,
|
SleepSummaryDataFields.SLEEP_SCORE,
|
||||||
GetSleepSummaryField.SNORING,
|
SleepSummaryDataFields.SNORING,
|
||||||
GetSleepSummaryField.SNORING_EPISODE_COUNT,
|
SleepSummaryDataFields.SNORING_COUNT,
|
||||||
GetSleepSummaryField.WAKEUP_COUNT,
|
SleepSummaryDataFields.WAKE_UP_COUNT,
|
||||||
GetSleepSummaryField.WAKEUP_DURATION,
|
SleepSummaryDataFields.TOTAL_TIME_AWAKE,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
return aggregate_sleep_summary(response)
|
||||||
# Set the default to empty lists.
|
|
||||||
raw_values: dict[GetSleepSummaryField, list[int]] = {
|
|
||||||
field: [] for field in GetSleepSummaryField
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect the raw data.
|
|
||||||
for serie in response.series:
|
|
||||||
data = serie.data
|
|
||||||
|
|
||||||
for field in GetSleepSummaryField:
|
|
||||||
raw_values[field].append(dict(data)[field.value])
|
|
||||||
|
|
||||||
values: dict[GetSleepSummaryField, float] = {}
|
|
||||||
|
|
||||||
def average(data: list[int]) -> float:
|
|
||||||
return sum(data) / len(data)
|
|
||||||
|
|
||||||
def set_value(field: GetSleepSummaryField, func: Callable) -> None:
|
|
||||||
non_nones = [
|
|
||||||
value for value in raw_values.get(field, []) if value is not None
|
|
||||||
]
|
|
||||||
values[field] = func(non_nones) if non_nones else None
|
|
||||||
|
|
||||||
set_value(GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, average)
|
|
||||||
set_value(GetSleepSummaryField.DEEP_SLEEP_DURATION, sum)
|
|
||||||
set_value(GetSleepSummaryField.DURATION_TO_SLEEP, average)
|
|
||||||
set_value(GetSleepSummaryField.DURATION_TO_WAKEUP, average)
|
|
||||||
set_value(GetSleepSummaryField.HR_AVERAGE, average)
|
|
||||||
set_value(GetSleepSummaryField.HR_MAX, average)
|
|
||||||
set_value(GetSleepSummaryField.HR_MIN, average)
|
|
||||||
set_value(GetSleepSummaryField.LIGHT_SLEEP_DURATION, sum)
|
|
||||||
set_value(GetSleepSummaryField.REM_SLEEP_DURATION, sum)
|
|
||||||
set_value(GetSleepSummaryField.RR_AVERAGE, average)
|
|
||||||
set_value(GetSleepSummaryField.RR_MAX, average)
|
|
||||||
set_value(GetSleepSummaryField.RR_MIN, average)
|
|
||||||
set_value(GetSleepSummaryField.SLEEP_SCORE, max)
|
|
||||||
set_value(GetSleepSummaryField.SNORING, average)
|
|
||||||
set_value(GetSleepSummaryField.SNORING_EPISODE_COUNT, sum)
|
|
||||||
set_value(GetSleepSummaryField.WAKEUP_COUNT, sum)
|
|
||||||
set_value(GetSleepSummaryField.WAKEUP_DURATION, average)
|
|
||||||
|
|
||||||
return {
|
|
||||||
WITHINGS_MEASURE_TYPE_MAP[field]: round(value, 4)
|
|
||||||
if value is not None
|
|
||||||
else None
|
|
||||||
for field, value in values.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[None]):
|
class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[None]):
|
||||||
@ -250,19 +141,19 @@ class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[Non
|
|||||||
in_bed: bool | None = None
|
in_bed: bool | None = None
|
||||||
_default_update_interval = None
|
_default_update_interval = None
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
|
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
|
||||||
"""Initialize the Withings data coordinator."""
|
"""Initialize the Withings data coordinator."""
|
||||||
super().__init__(hass, client)
|
super().__init__(hass, client)
|
||||||
self.notification_categories = {
|
self.notification_categories = {
|
||||||
NotifyAppli.BED_IN,
|
NotificationCategory.IN_BED,
|
||||||
NotifyAppli.BED_OUT,
|
NotificationCategory.OUT_BED,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_webhook_data_updated(
|
async def async_webhook_data_updated(
|
||||||
self, notification_category: NotifyAppli
|
self, notification_category: NotificationCategory
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Only set new in bed value instead of refresh."""
|
"""Only set new in bed value instead of refresh."""
|
||||||
self.in_bed = notification_category == NotifyAppli.BED_IN
|
self.in_bed = notification_category == NotificationCategory.IN_BED
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
async def _internal_update_data(self) -> None:
|
async def _internal_update_data(self) -> None:
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/withings",
|
"documentation": "https://www.home-assistant.io/integrations/withings",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["withings_api"],
|
"loggers": ["withings_api"],
|
||||||
"requirements": ["withings-api==2.4.0"]
|
"requirements": ["aiowithings==0.4.4"]
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
"""Sensors flow for Withings."""
|
"""Sensors flow for Withings."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from withings_api.common import GetSleepSummaryField, MeasureType
|
from aiowithings import MeasurementType, SleepSummary
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -22,6 +23,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -32,7 +34,6 @@ from .const import (
|
|||||||
UOM_BREATHS_PER_MINUTE,
|
UOM_BREATHS_PER_MINUTE,
|
||||||
UOM_FREQUENCY,
|
UOM_FREQUENCY,
|
||||||
UOM_MMHG,
|
UOM_MMHG,
|
||||||
Measurement,
|
|
||||||
)
|
)
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
WithingsDataUpdateCoordinator,
|
WithingsDataUpdateCoordinator,
|
||||||
@ -43,146 +44,130 @@ from .entity import WithingsEntity
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WithingsEntityDescriptionMixin:
|
class WithingsMeasurementSensorEntityDescriptionMixin:
|
||||||
"""Mixin for describing withings data."""
|
"""Mixin for describing withings data."""
|
||||||
|
|
||||||
measurement: Measurement
|
measurement_type: MeasurementType
|
||||||
measure_type: GetSleepSummaryField | MeasureType
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WithingsSensorEntityDescription(
|
class WithingsMeasurementSensorEntityDescription(
|
||||||
SensorEntityDescription, WithingsEntityDescriptionMixin
|
SensorEntityDescription, WithingsMeasurementSensorEntityDescriptionMixin
|
||||||
):
|
):
|
||||||
"""Immutable class for describing withings data."""
|
"""Immutable class for describing withings data."""
|
||||||
|
|
||||||
|
|
||||||
MEASUREMENT_SENSORS = [
|
MEASUREMENT_SENSORS = [
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.WEIGHT_KG.value,
|
key="weight_kg",
|
||||||
measurement=Measurement.WEIGHT_KG,
|
measurement_type=MeasurementType.WEIGHT,
|
||||||
measure_type=MeasureType.WEIGHT,
|
|
||||||
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
||||||
device_class=SensorDeviceClass.WEIGHT,
|
device_class=SensorDeviceClass.WEIGHT,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.FAT_MASS_KG.value,
|
key="fat_mass_kg",
|
||||||
measurement=Measurement.FAT_MASS_KG,
|
measurement_type=MeasurementType.FAT_MASS_WEIGHT,
|
||||||
measure_type=MeasureType.FAT_MASS_WEIGHT,
|
|
||||||
translation_key="fat_mass",
|
translation_key="fat_mass",
|
||||||
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
||||||
device_class=SensorDeviceClass.WEIGHT,
|
device_class=SensorDeviceClass.WEIGHT,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.FAT_FREE_MASS_KG.value,
|
key="fat_free_mass_kg",
|
||||||
measurement=Measurement.FAT_FREE_MASS_KG,
|
measurement_type=MeasurementType.FAT_FREE_MASS,
|
||||||
measure_type=MeasureType.FAT_FREE_MASS,
|
|
||||||
translation_key="fat_free_mass",
|
translation_key="fat_free_mass",
|
||||||
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
||||||
device_class=SensorDeviceClass.WEIGHT,
|
device_class=SensorDeviceClass.WEIGHT,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.MUSCLE_MASS_KG.value,
|
key="muscle_mass_kg",
|
||||||
measurement=Measurement.MUSCLE_MASS_KG,
|
measurement_type=MeasurementType.MUSCLE_MASS,
|
||||||
measure_type=MeasureType.MUSCLE_MASS,
|
|
||||||
translation_key="muscle_mass",
|
translation_key="muscle_mass",
|
||||||
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
||||||
device_class=SensorDeviceClass.WEIGHT,
|
device_class=SensorDeviceClass.WEIGHT,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.BONE_MASS_KG.value,
|
key="bone_mass_kg",
|
||||||
measurement=Measurement.BONE_MASS_KG,
|
measurement_type=MeasurementType.BONE_MASS,
|
||||||
measure_type=MeasureType.BONE_MASS,
|
|
||||||
translation_key="bone_mass",
|
translation_key="bone_mass",
|
||||||
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
||||||
device_class=SensorDeviceClass.WEIGHT,
|
device_class=SensorDeviceClass.WEIGHT,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.HEIGHT_M.value,
|
key="height_m",
|
||||||
measurement=Measurement.HEIGHT_M,
|
measurement_type=MeasurementType.HEIGHT,
|
||||||
measure_type=MeasureType.HEIGHT,
|
|
||||||
translation_key="height",
|
translation_key="height",
|
||||||
native_unit_of_measurement=UnitOfLength.METERS,
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
device_class=SensorDeviceClass.DISTANCE,
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.TEMP_C.value,
|
key="temperature_c",
|
||||||
measurement=Measurement.TEMP_C,
|
measurement_type=MeasurementType.TEMPERATURE,
|
||||||
measure_type=MeasureType.TEMPERATURE,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.BODY_TEMP_C.value,
|
key="body_temperature_c",
|
||||||
measurement=Measurement.BODY_TEMP_C,
|
measurement_type=MeasurementType.BODY_TEMPERATURE,
|
||||||
measure_type=MeasureType.BODY_TEMPERATURE,
|
|
||||||
translation_key="body_temperature",
|
translation_key="body_temperature",
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.SKIN_TEMP_C.value,
|
key="skin_temperature_c",
|
||||||
measurement=Measurement.SKIN_TEMP_C,
|
measurement_type=MeasurementType.SKIN_TEMPERATURE,
|
||||||
measure_type=MeasureType.SKIN_TEMPERATURE,
|
|
||||||
translation_key="skin_temperature",
|
translation_key="skin_temperature",
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.FAT_RATIO_PCT.value,
|
key="fat_ratio_pct",
|
||||||
measurement=Measurement.FAT_RATIO_PCT,
|
measurement_type=MeasurementType.FAT_RATIO,
|
||||||
measure_type=MeasureType.FAT_RATIO,
|
|
||||||
translation_key="fat_ratio",
|
translation_key="fat_ratio",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.DIASTOLIC_MMHG.value,
|
key="diastolic_blood_pressure_mmhg",
|
||||||
measurement=Measurement.DIASTOLIC_MMHG,
|
measurement_type=MeasurementType.DIASTOLIC_BLOOD_PRESSURE,
|
||||||
measure_type=MeasureType.DIASTOLIC_BLOOD_PRESSURE,
|
|
||||||
translation_key="diastolic_blood_pressure",
|
translation_key="diastolic_blood_pressure",
|
||||||
native_unit_of_measurement=UOM_MMHG,
|
native_unit_of_measurement=UOM_MMHG,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.SYSTOLIC_MMGH.value,
|
key="systolic_blood_pressure_mmhg",
|
||||||
measurement=Measurement.SYSTOLIC_MMGH,
|
measurement_type=MeasurementType.SYSTOLIC_BLOOD_PRESSURE,
|
||||||
measure_type=MeasureType.SYSTOLIC_BLOOD_PRESSURE,
|
|
||||||
translation_key="systolic_blood_pressure",
|
translation_key="systolic_blood_pressure",
|
||||||
native_unit_of_measurement=UOM_MMHG,
|
native_unit_of_measurement=UOM_MMHG,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.HEART_PULSE_BPM.value,
|
key="heart_pulse_bpm",
|
||||||
measurement=Measurement.HEART_PULSE_BPM,
|
measurement_type=MeasurementType.HEART_RATE,
|
||||||
measure_type=MeasureType.HEART_RATE,
|
|
||||||
translation_key="heart_pulse",
|
translation_key="heart_pulse",
|
||||||
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
||||||
icon="mdi:heart-pulse",
|
icon="mdi:heart-pulse",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.SPO2_PCT.value,
|
key="spo2_pct",
|
||||||
measurement=Measurement.SPO2_PCT,
|
measurement_type=MeasurementType.SP02,
|
||||||
measure_type=MeasureType.SP02,
|
|
||||||
translation_key="spo2",
|
translation_key="spo2",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.HYDRATION.value,
|
key="hydration",
|
||||||
measurement=Measurement.HYDRATION,
|
measurement_type=MeasurementType.HYDRATION,
|
||||||
measure_type=MeasureType.HYDRATION,
|
|
||||||
translation_key="hydration",
|
translation_key="hydration",
|
||||||
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
|
||||||
device_class=SensorDeviceClass.WEIGHT,
|
device_class=SensorDeviceClass.WEIGHT,
|
||||||
@ -190,29 +175,42 @@ MEASUREMENT_SENSORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsMeasurementSensorEntityDescription(
|
||||||
key=Measurement.PWV.value,
|
key="pulse_wave_velocity",
|
||||||
measurement=Measurement.PWV,
|
measurement_type=MeasurementType.PULSE_WAVE_VELOCITY,
|
||||||
measure_type=MeasureType.PULSE_WAVE_VELOCITY,
|
|
||||||
translation_key="pulse_wave_velocity",
|
translation_key="pulse_wave_velocity",
|
||||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||||
device_class=SensorDeviceClass.SPEED,
|
device_class=SensorDeviceClass.SPEED,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WithingsSleepSensorEntityDescriptionMixin:
|
||||||
|
"""Mixin for describing withings data."""
|
||||||
|
|
||||||
|
value_fn: Callable[[SleepSummary], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WithingsSleepSensorEntityDescription(
|
||||||
|
SensorEntityDescription, WithingsSleepSensorEntityDescriptionMixin
|
||||||
|
):
|
||||||
|
"""Immutable class for describing withings data."""
|
||||||
|
|
||||||
|
|
||||||
SLEEP_SENSORS = [
|
SLEEP_SENSORS = [
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY.value,
|
key="sleep_breathing_disturbances_intensity",
|
||||||
measurement=Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY,
|
value_fn=lambda sleep_summary: sleep_summary.breathing_disturbances_intensity,
|
||||||
measure_type=GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY,
|
|
||||||
translation_key="breathing_disturbances_intensity",
|
translation_key="breathing_disturbances_intensity",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_DEEP_DURATION_SECONDS.value,
|
key="sleep_deep_duration_seconds",
|
||||||
measurement=Measurement.SLEEP_DEEP_DURATION_SECONDS,
|
value_fn=lambda sleep_summary: sleep_summary.deep_sleep_duration,
|
||||||
measure_type=GetSleepSummaryField.DEEP_SLEEP_DURATION,
|
|
||||||
translation_key="deep_sleep",
|
translation_key="deep_sleep",
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
icon="mdi:sleep",
|
icon="mdi:sleep",
|
||||||
@ -220,10 +218,9 @@ SLEEP_SENSORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_TOSLEEP_DURATION_SECONDS.value,
|
key="sleep_tosleep_duration_seconds",
|
||||||
measurement=Measurement.SLEEP_TOSLEEP_DURATION_SECONDS,
|
value_fn=lambda sleep_summary: sleep_summary.sleep_latency,
|
||||||
measure_type=GetSleepSummaryField.DURATION_TO_SLEEP,
|
|
||||||
translation_key="time_to_sleep",
|
translation_key="time_to_sleep",
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
icon="mdi:sleep",
|
icon="mdi:sleep",
|
||||||
@ -231,10 +228,9 @@ SLEEP_SENSORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS.value,
|
key="sleep_towakeup_duration_seconds",
|
||||||
measurement=Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS,
|
value_fn=lambda sleep_summary: sleep_summary.wake_up_latency,
|
||||||
measure_type=GetSleepSummaryField.DURATION_TO_WAKEUP,
|
|
||||||
translation_key="time_to_wakeup",
|
translation_key="time_to_wakeup",
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
icon="mdi:sleep-off",
|
icon="mdi:sleep-off",
|
||||||
@ -242,40 +238,36 @@ SLEEP_SENSORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_HEART_RATE_AVERAGE.value,
|
key="sleep_heart_rate_average_bpm",
|
||||||
measurement=Measurement.SLEEP_HEART_RATE_AVERAGE,
|
value_fn=lambda sleep_summary: sleep_summary.average_heart_rate,
|
||||||
measure_type=GetSleepSummaryField.HR_AVERAGE,
|
|
||||||
translation_key="average_heart_rate",
|
translation_key="average_heart_rate",
|
||||||
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
||||||
icon="mdi:heart-pulse",
|
icon="mdi:heart-pulse",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_HEART_RATE_MAX.value,
|
key="sleep_heart_rate_max_bpm",
|
||||||
measurement=Measurement.SLEEP_HEART_RATE_MAX,
|
value_fn=lambda sleep_summary: sleep_summary.max_heart_rate,
|
||||||
measure_type=GetSleepSummaryField.HR_MAX,
|
|
||||||
translation_key="maximum_heart_rate",
|
translation_key="maximum_heart_rate",
|
||||||
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
||||||
icon="mdi:heart-pulse",
|
icon="mdi:heart-pulse",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_HEART_RATE_MIN.value,
|
key="sleep_heart_rate_min_bpm",
|
||||||
measurement=Measurement.SLEEP_HEART_RATE_MIN,
|
value_fn=lambda sleep_summary: sleep_summary.min_heart_rate,
|
||||||
measure_type=GetSleepSummaryField.HR_MIN,
|
|
||||||
translation_key="minimum_heart_rate",
|
translation_key="minimum_heart_rate",
|
||||||
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
|
||||||
icon="mdi:heart-pulse",
|
icon="mdi:heart-pulse",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_LIGHT_DURATION_SECONDS.value,
|
key="sleep_light_duration_seconds",
|
||||||
measurement=Measurement.SLEEP_LIGHT_DURATION_SECONDS,
|
value_fn=lambda sleep_summary: sleep_summary.light_sleep_duration,
|
||||||
measure_type=GetSleepSummaryField.LIGHT_SLEEP_DURATION,
|
|
||||||
translation_key="light_sleep",
|
translation_key="light_sleep",
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
icon="mdi:sleep",
|
icon="mdi:sleep",
|
||||||
@ -283,10 +275,9 @@ SLEEP_SENSORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_REM_DURATION_SECONDS.value,
|
key="sleep_rem_duration_seconds",
|
||||||
measurement=Measurement.SLEEP_REM_DURATION_SECONDS,
|
value_fn=lambda sleep_summary: sleep_summary.rem_sleep_duration,
|
||||||
measure_type=GetSleepSummaryField.REM_SLEEP_DURATION,
|
|
||||||
translation_key="rem_sleep",
|
translation_key="rem_sleep",
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
icon="mdi:sleep",
|
icon="mdi:sleep",
|
||||||
@ -294,73 +285,65 @@ SLEEP_SENSORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE.value,
|
key="sleep_respiratory_average_bpm",
|
||||||
measurement=Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE,
|
value_fn=lambda sleep_summary: sleep_summary.average_respiration_rate,
|
||||||
measure_type=GetSleepSummaryField.RR_AVERAGE,
|
|
||||||
translation_key="average_respiratory_rate",
|
translation_key="average_respiratory_rate",
|
||||||
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_RESPIRATORY_RATE_MAX.value,
|
key="sleep_respiratory_max_bpm",
|
||||||
measurement=Measurement.SLEEP_RESPIRATORY_RATE_MAX,
|
value_fn=lambda sleep_summary: sleep_summary.max_respiration_rate,
|
||||||
measure_type=GetSleepSummaryField.RR_MAX,
|
|
||||||
translation_key="maximum_respiratory_rate",
|
translation_key="maximum_respiratory_rate",
|
||||||
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_RESPIRATORY_RATE_MIN.value,
|
key="sleep_respiratory_min_bpm",
|
||||||
measurement=Measurement.SLEEP_RESPIRATORY_RATE_MIN,
|
value_fn=lambda sleep_summary: sleep_summary.min_respiration_rate,
|
||||||
measure_type=GetSleepSummaryField.RR_MIN,
|
|
||||||
translation_key="minimum_respiratory_rate",
|
translation_key="minimum_respiratory_rate",
|
||||||
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
|
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_SCORE.value,
|
key="sleep_score",
|
||||||
measurement=Measurement.SLEEP_SCORE,
|
value_fn=lambda sleep_summary: sleep_summary.sleep_score,
|
||||||
measure_type=GetSleepSummaryField.SLEEP_SCORE,
|
|
||||||
translation_key="sleep_score",
|
translation_key="sleep_score",
|
||||||
native_unit_of_measurement=SCORE_POINTS,
|
native_unit_of_measurement=SCORE_POINTS,
|
||||||
icon="mdi:medal",
|
icon="mdi:medal",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_SNORING.value,
|
key="sleep_snoring",
|
||||||
measurement=Measurement.SLEEP_SNORING,
|
value_fn=lambda sleep_summary: sleep_summary.snoring,
|
||||||
measure_type=GetSleepSummaryField.SNORING,
|
|
||||||
translation_key="snoring",
|
translation_key="snoring",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_SNORING_EPISODE_COUNT.value,
|
key="sleep_snoring_eposode_count",
|
||||||
measurement=Measurement.SLEEP_SNORING_EPISODE_COUNT,
|
value_fn=lambda sleep_summary: sleep_summary.snoring_count,
|
||||||
measure_type=GetSleepSummaryField.SNORING_EPISODE_COUNT,
|
|
||||||
translation_key="snoring_episode_count",
|
translation_key="snoring_episode_count",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_WAKEUP_COUNT.value,
|
key="sleep_wakeup_count",
|
||||||
measurement=Measurement.SLEEP_WAKEUP_COUNT,
|
value_fn=lambda sleep_summary: sleep_summary.wake_up_count,
|
||||||
measure_type=GetSleepSummaryField.WAKEUP_COUNT,
|
|
||||||
translation_key="wakeup_count",
|
translation_key="wakeup_count",
|
||||||
native_unit_of_measurement=UOM_FREQUENCY,
|
native_unit_of_measurement=UOM_FREQUENCY,
|
||||||
icon="mdi:sleep-off",
|
icon="mdi:sleep-off",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
WithingsSensorEntityDescription(
|
WithingsSleepSensorEntityDescription(
|
||||||
key=Measurement.SLEEP_WAKEUP_DURATION_SECONDS.value,
|
key="sleep_wakeup_duration_seconds",
|
||||||
measurement=Measurement.SLEEP_WAKEUP_DURATION_SECONDS,
|
value_fn=lambda sleep_summary: sleep_summary.total_time_awake,
|
||||||
measure_type=GetSleepSummaryField.WAKEUP_DURATION,
|
|
||||||
translation_key="wakeup_time",
|
translation_key="wakeup_time",
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
icon="mdi:sleep-off",
|
icon="mdi:sleep-off",
|
||||||
@ -398,38 +381,51 @@ async def async_setup_entry(
|
|||||||
class WithingsSensor(WithingsEntity, SensorEntity):
|
class WithingsSensor(WithingsEntity, SensorEntity):
|
||||||
"""Implementation of a Withings sensor."""
|
"""Implementation of a Withings sensor."""
|
||||||
|
|
||||||
entity_description: WithingsSensorEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: WithingsDataUpdateCoordinator,
|
coordinator: WithingsDataUpdateCoordinator,
|
||||||
entity_description: WithingsSensorEntityDescription,
|
entity_description: SensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize sensor."""
|
"""Initialize sensor."""
|
||||||
super().__init__(coordinator, entity_description.key)
|
super().__init__(coordinator, entity_description.key)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> None | str | int | float:
|
|
||||||
"""Return the state of the entity."""
|
|
||||||
return self.coordinator.data[self.entity_description.measurement]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if the sensor is available."""
|
|
||||||
return (
|
|
||||||
super().available
|
|
||||||
and self.entity_description.measurement in self.coordinator.data
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WithingsMeasurementSensor(WithingsSensor):
|
class WithingsMeasurementSensor(WithingsSensor):
|
||||||
"""Implementation of a Withings measurement sensor."""
|
"""Implementation of a Withings measurement sensor."""
|
||||||
|
|
||||||
coordinator: WithingsMeasurementDataUpdateCoordinator
|
coordinator: WithingsMeasurementDataUpdateCoordinator
|
||||||
|
|
||||||
|
entity_description: WithingsMeasurementSensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
return self.coordinator.data[self.entity_description.measurement_type]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if the sensor is available."""
|
||||||
|
return (
|
||||||
|
super().available
|
||||||
|
and self.entity_description.measurement_type in self.coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WithingsSleepSensor(WithingsSensor):
|
class WithingsSleepSensor(WithingsSensor):
|
||||||
"""Implementation of a Withings sleep sensor."""
|
"""Implementation of a Withings sleep sensor."""
|
||||||
|
|
||||||
coordinator: WithingsSleepDataUpdateCoordinator
|
coordinator: WithingsSleepDataUpdateCoordinator
|
||||||
|
|
||||||
|
entity_description: WithingsSleepSensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
assert self.coordinator.data
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if the sensor is available."""
|
||||||
|
return super().available and self.coordinator.data is not None
|
||||||
|
@ -386,6 +386,9 @@ aiowatttime==0.1.1
|
|||||||
# homeassistant.components.webostv
|
# homeassistant.components.webostv
|
||||||
aiowebostv==0.3.3
|
aiowebostv==0.3.3
|
||||||
|
|
||||||
|
# homeassistant.components.withings
|
||||||
|
aiowithings==0.4.4
|
||||||
|
|
||||||
# homeassistant.components.yandex_transport
|
# homeassistant.components.yandex_transport
|
||||||
aioymaps==1.2.2
|
aioymaps==1.2.2
|
||||||
|
|
||||||
@ -2717,9 +2720,6 @@ wiffi==1.1.2
|
|||||||
# homeassistant.components.wirelesstag
|
# homeassistant.components.wirelesstag
|
||||||
wirelesstagpy==0.8.1
|
wirelesstagpy==0.8.1
|
||||||
|
|
||||||
# homeassistant.components.withings
|
|
||||||
withings-api==2.4.0
|
|
||||||
|
|
||||||
# homeassistant.components.wled
|
# homeassistant.components.wled
|
||||||
wled==0.16.0
|
wled==0.16.0
|
||||||
|
|
||||||
|
@ -361,6 +361,9 @@ aiowatttime==0.1.1
|
|||||||
# homeassistant.components.webostv
|
# homeassistant.components.webostv
|
||||||
aiowebostv==0.3.3
|
aiowebostv==0.3.3
|
||||||
|
|
||||||
|
# homeassistant.components.withings
|
||||||
|
aiowithings==0.4.4
|
||||||
|
|
||||||
# homeassistant.components.yandex_transport
|
# homeassistant.components.yandex_transport
|
||||||
aioymaps==1.2.2
|
aioymaps==1.2.2
|
||||||
|
|
||||||
@ -2020,9 +2023,6 @@ whois==0.9.27
|
|||||||
# homeassistant.components.wiffi
|
# homeassistant.components.wiffi
|
||||||
wiffi==1.1.2
|
wiffi==1.1.2
|
||||||
|
|
||||||
# homeassistant.components.withings
|
|
||||||
withings-api==2.4.0
|
|
||||||
|
|
||||||
# homeassistant.components.wled
|
# homeassistant.components.wled
|
||||||
wled==0.16.0
|
wled==0.16.0
|
||||||
|
|
||||||
|
@ -3,19 +3,14 @@ from datetime import timedelta
|
|||||||
import time
|
import time
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from aiowithings import Device, MeasurementGroup, SleepSummary, WithingsClient
|
||||||
|
from aiowithings.models import NotificationConfiguration
|
||||||
import pytest
|
import pytest
|
||||||
from withings_api import (
|
|
||||||
MeasureGetMeasResponse,
|
|
||||||
NotifyListResponse,
|
|
||||||
SleepGetSummaryResponse,
|
|
||||||
UserGetDeviceResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
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.withings.api import ConfigEntryWithingsApi
|
|
||||||
from homeassistant.components.withings.const import DOMAIN
|
from homeassistant.components.withings.const import DOMAIN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -133,22 +128,34 @@ def polling_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
|||||||
def mock_withings():
|
def mock_withings():
|
||||||
"""Mock withings."""
|
"""Mock withings."""
|
||||||
|
|
||||||
mock = AsyncMock(spec=ConfigEntryWithingsApi)
|
devices_json = load_json_object_fixture("withings/get_device.json")
|
||||||
mock.user_get_device.return_value = UserGetDeviceResponse(
|
devices = [Device.from_api(device) for device in devices_json["devices"]]
|
||||||
**load_json_object_fixture("withings/get_device.json")
|
|
||||||
)
|
meas_json = load_json_object_fixture("withings/get_meas.json")
|
||||||
mock.async_measure_get_meas.return_value = MeasureGetMeasResponse(
|
measurement_groups = [
|
||||||
**load_json_object_fixture("withings/get_meas.json")
|
MeasurementGroup.from_api(measurement)
|
||||||
)
|
for measurement in meas_json["measuregrps"]
|
||||||
mock.async_sleep_get_summary.return_value = SleepGetSummaryResponse(
|
]
|
||||||
**load_json_object_fixture("withings/get_sleep.json")
|
|
||||||
)
|
sleep_json = load_json_object_fixture("withings/get_sleep.json")
|
||||||
mock.async_notify_list.return_value = NotifyListResponse(
|
sleep_summaries = [
|
||||||
**load_json_object_fixture("withings/notify_list.json")
|
SleepSummary.from_api(sleep_summary) for sleep_summary in sleep_json["series"]
|
||||||
)
|
]
|
||||||
|
|
||||||
|
notification_json = load_json_object_fixture("withings/notify_list.json")
|
||||||
|
notifications = [
|
||||||
|
NotificationConfiguration.from_api(not_conf)
|
||||||
|
for not_conf in notification_json["profiles"]
|
||||||
|
]
|
||||||
|
|
||||||
|
mock = AsyncMock(spec=WithingsClient)
|
||||||
|
mock.get_devices.return_value = devices
|
||||||
|
mock.get_measurement_in_period.return_value = measurement_groups
|
||||||
|
mock.get_sleep_summary_since.return_value = sleep_summaries
|
||||||
|
mock.list_notification_configurations.return_value = notifications
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.withings.ConfigEntryWithingsApi",
|
"homeassistant.components.withings.WithingsClient",
|
||||||
return_value=mock,
|
return_value=mock,
|
||||||
):
|
):
|
||||||
yield mock
|
yield mock
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": []
|
|
||||||
}
|
|
@ -11,7 +11,7 @@
|
|||||||
'entity_id': 'sensor.henk_weight',
|
'entity_id': 'sensor.henk_weight',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '70.0',
|
'state': '70',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.1
|
# name: test_all_entities.1
|
||||||
@ -26,7 +26,7 @@
|
|||||||
'entity_id': 'sensor.henk_fat_mass',
|
'entity_id': 'sensor.henk_fat_mass',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '5.0',
|
'state': '5',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.10
|
# name: test_all_entities.10
|
||||||
@ -40,7 +40,7 @@
|
|||||||
'entity_id': 'sensor.henk_diastolic_blood_pressure',
|
'entity_id': 'sensor.henk_diastolic_blood_pressure',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '70.0',
|
'state': '70',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.11
|
# name: test_all_entities.11
|
||||||
@ -54,7 +54,7 @@
|
|||||||
'entity_id': 'sensor.henk_systolic_blood_pressure',
|
'entity_id': 'sensor.henk_systolic_blood_pressure',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '100.0',
|
'state': '100',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.12
|
# name: test_all_entities.12
|
||||||
@ -69,7 +69,7 @@
|
|||||||
'entity_id': 'sensor.henk_heart_pulse',
|
'entity_id': 'sensor.henk_heart_pulse',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '60.0',
|
'state': '60',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.13
|
# name: test_all_entities.13
|
||||||
@ -114,7 +114,7 @@
|
|||||||
'entity_id': 'sensor.henk_pulse_wave_velocity',
|
'entity_id': 'sensor.henk_pulse_wave_velocity',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '100.0',
|
'state': '100',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.16
|
# name: test_all_entities.16
|
||||||
@ -127,7 +127,7 @@
|
|||||||
'entity_id': 'sensor.henk_breathing_disturbances_intensity',
|
'entity_id': 'sensor.henk_breathing_disturbances_intensity',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '10.0',
|
'state': '10',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.17
|
# name: test_all_entities.17
|
||||||
@ -159,7 +159,7 @@
|
|||||||
'entity_id': 'sensor.henk_time_to_sleep',
|
'entity_id': 'sensor.henk_time_to_sleep',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '780.0',
|
'state': '780',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.19
|
# name: test_all_entities.19
|
||||||
@ -175,7 +175,7 @@
|
|||||||
'entity_id': 'sensor.henk_time_to_wakeup',
|
'entity_id': 'sensor.henk_time_to_wakeup',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '996.0',
|
'state': '996',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.2
|
# name: test_all_entities.2
|
||||||
@ -190,7 +190,7 @@
|
|||||||
'entity_id': 'sensor.henk_fat_free_mass',
|
'entity_id': 'sensor.henk_fat_free_mass',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '60.0',
|
'state': '60',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.20
|
# name: test_all_entities.20
|
||||||
@ -205,7 +205,7 @@
|
|||||||
'entity_id': 'sensor.henk_average_heart_rate',
|
'entity_id': 'sensor.henk_average_heart_rate',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '83.2',
|
'state': '83',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.21
|
# name: test_all_entities.21
|
||||||
@ -220,7 +220,7 @@
|
|||||||
'entity_id': 'sensor.henk_maximum_heart_rate',
|
'entity_id': 'sensor.henk_maximum_heart_rate',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '108.4',
|
'state': '108',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.22
|
# name: test_all_entities.22
|
||||||
@ -235,7 +235,7 @@
|
|||||||
'entity_id': 'sensor.henk_minimum_heart_rate',
|
'entity_id': 'sensor.henk_minimum_heart_rate',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '58.0',
|
'state': '58',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.23
|
# name: test_all_entities.23
|
||||||
@ -281,7 +281,7 @@
|
|||||||
'entity_id': 'sensor.henk_average_respiratory_rate',
|
'entity_id': 'sensor.henk_average_respiratory_rate',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '14.2',
|
'state': '14',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.26
|
# name: test_all_entities.26
|
||||||
@ -295,7 +295,7 @@
|
|||||||
'entity_id': 'sensor.henk_maximum_respiratory_rate',
|
'entity_id': 'sensor.henk_maximum_respiratory_rate',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '20.0',
|
'state': '20',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.27
|
# name: test_all_entities.27
|
||||||
@ -309,7 +309,7 @@
|
|||||||
'entity_id': 'sensor.henk_minimum_respiratory_rate',
|
'entity_id': 'sensor.henk_minimum_respiratory_rate',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '10.0',
|
'state': '10',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.28
|
# name: test_all_entities.28
|
||||||
@ -337,7 +337,7 @@
|
|||||||
'entity_id': 'sensor.henk_snoring',
|
'entity_id': 'sensor.henk_snoring',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '1044.0',
|
'state': '1044',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.3
|
# name: test_all_entities.3
|
||||||
@ -352,7 +352,7 @@
|
|||||||
'entity_id': 'sensor.henk_muscle_mass',
|
'entity_id': 'sensor.henk_muscle_mass',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '50.0',
|
'state': '50',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.30
|
# name: test_all_entities.30
|
||||||
@ -396,7 +396,7 @@
|
|||||||
'entity_id': 'sensor.henk_wakeup_time',
|
'entity_id': 'sensor.henk_wakeup_time',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '3468.0',
|
'state': '3468',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.33
|
# name: test_all_entities.33
|
||||||
@ -568,7 +568,7 @@
|
|||||||
'entity_id': 'sensor.henk_bone_mass',
|
'entity_id': 'sensor.henk_bone_mass',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '10.0',
|
'state': '10',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.40
|
# name: test_all_entities.40
|
||||||
@ -820,7 +820,7 @@
|
|||||||
'entity_id': 'sensor.henk_height',
|
'entity_id': 'sensor.henk_height',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '2.0',
|
'state': '2',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.50
|
# name: test_all_entities.50
|
||||||
@ -1065,7 +1065,7 @@
|
|||||||
'entity_id': 'sensor.henk_temperature',
|
'entity_id': 'sensor.henk_temperature',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '40.0',
|
'state': '40',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.60
|
# name: test_all_entities.60
|
||||||
@ -1220,7 +1220,7 @@
|
|||||||
'entity_id': 'sensor.henk_body_temperature',
|
'entity_id': 'sensor.henk_body_temperature',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '40.0',
|
'state': '40',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.8
|
# name: test_all_entities.8
|
||||||
@ -1235,7 +1235,7 @@
|
|||||||
'entity_id': 'sensor.henk_skin_temperature',
|
'entity_id': 'sensor.henk_skin_temperature',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '20.0',
|
'state': '20',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities.9
|
# name: test_all_entities.9
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientResponseError
|
from aiohttp.client_exceptions import ClientResponseError
|
||||||
|
from aiowithings import NotificationCategory
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
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
|
||||||
@ -36,7 +36,7 @@ async def test_binary_sensor(
|
|||||||
resp = await call_webhook(
|
resp = await call_webhook(
|
||||||
hass,
|
hass,
|
||||||
WEBHOOK_ID,
|
WEBHOOK_ID,
|
||||||
{"userid": USER_ID, "appli": NotifyAppli.BED_IN},
|
{"userid": USER_ID, "appli": NotificationCategory.IN_BED},
|
||||||
client,
|
client,
|
||||||
)
|
)
|
||||||
assert resp.message_code == 0
|
assert resp.message_code == 0
|
||||||
@ -46,7 +46,7 @@ async def test_binary_sensor(
|
|||||||
resp = await call_webhook(
|
resp = await call_webhook(
|
||||||
hass,
|
hass,
|
||||||
WEBHOOK_ID,
|
WEBHOOK_ID,
|
||||||
{"userid": USER_ID, "appli": NotifyAppli.BED_OUT},
|
{"userid": USER_ID, "appli": NotificationCategory.OUT_BED},
|
||||||
client,
|
client,
|
||||||
)
|
)
|
||||||
assert resp.message_code == 0
|
assert resp.message_code == 0
|
||||||
@ -73,6 +73,6 @@ async def test_polling_binary_sensor(
|
|||||||
await call_webhook(
|
await call_webhook(
|
||||||
hass,
|
hass,
|
||||||
WEBHOOK_ID,
|
WEBHOOK_ID,
|
||||||
{"userid": USER_ID, "appli": NotifyAppli.BED_IN},
|
{"userid": USER_ID, "appli": NotificationCategory.IN_BED},
|
||||||
client,
|
client,
|
||||||
)
|
)
|
||||||
|
@ -4,11 +4,14 @@ from typing import Any
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from aiowithings import (
|
||||||
|
NotificationCategory,
|
||||||
|
WithingsAuthenticationFailedError,
|
||||||
|
WithingsUnauthorizedError,
|
||||||
|
)
|
||||||
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 homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.cloud import CloudNotAvailable
|
from homeassistant.components.cloud import CloudNotAvailable
|
||||||
@ -26,7 +29,6 @@ from tests.common import (
|
|||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
async_fire_time_changed,
|
async_fire_time_changed,
|
||||||
async_mock_cloud_connection_status,
|
async_mock_cloud_connection_status,
|
||||||
load_json_object_fixture,
|
|
||||||
)
|
)
|
||||||
from tests.components.cloud import mock_cloud
|
from tests.components.cloud import mock_cloud
|
||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
@ -126,19 +128,29 @@ async def test_data_manager_webhook_subscription(
|
|||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert withings.async_notify_subscribe.call_count == 6
|
assert withings.subscribe_notification.call_count == 6
|
||||||
|
|
||||||
webhook_url = "https://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.subscribe_notification.assert_any_call(
|
||||||
withings.async_notify_subscribe.assert_any_call(
|
webhook_url, NotificationCategory.WEIGHT
|
||||||
webhook_url, NotifyAppli.CIRCULATORY
|
)
|
||||||
|
withings.subscribe_notification.assert_any_call(
|
||||||
|
webhook_url, NotificationCategory.PRESSURE
|
||||||
|
)
|
||||||
|
withings.subscribe_notification.assert_any_call(
|
||||||
|
webhook_url, NotificationCategory.ACTIVITY
|
||||||
|
)
|
||||||
|
withings.subscribe_notification.assert_any_call(
|
||||||
|
webhook_url, NotificationCategory.SLEEP
|
||||||
)
|
)
|
||||||
withings.async_notify_subscribe.assert_any_call(webhook_url, NotifyAppli.ACTIVITY)
|
|
||||||
withings.async_notify_subscribe.assert_any_call(webhook_url, NotifyAppli.SLEEP)
|
|
||||||
|
|
||||||
withings.async_notify_revoke.assert_any_call(webhook_url, NotifyAppli.BED_IN)
|
withings.revoke_notification_configurations.assert_any_call(
|
||||||
withings.async_notify_revoke.assert_any_call(webhook_url, NotifyAppli.BED_OUT)
|
webhook_url, NotificationCategory.IN_BED
|
||||||
|
)
|
||||||
|
withings.revoke_notification_configurations.assert_any_call(
|
||||||
|
webhook_url, NotificationCategory.OUT_BED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_subscription_polling_config(
|
async def test_webhook_subscription_polling_config(
|
||||||
@ -149,16 +161,16 @@ async def test_webhook_subscription_polling_config(
|
|||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test webhook subscriptions not run when polling."""
|
"""Test webhook subscriptions not run when polling."""
|
||||||
await setup_integration(hass, polling_config_entry)
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
await hass_client_no_auth()
|
await hass_client_no_auth()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
freezer.tick(timedelta(seconds=1))
|
freezer.tick(timedelta(seconds=1))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert withings.notify_revoke.call_count == 0
|
assert withings.revoke_notification_configurations.call_count == 0
|
||||||
assert withings.notify_subscribe.call_count == 0
|
assert withings.subscribe_notification.call_count == 0
|
||||||
assert withings.notify_list.call_count == 0
|
assert withings.list_notification_configurations.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -200,22 +212,22 @@ async def test_webhooks_request_data(
|
|||||||
|
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
|
|
||||||
assert withings.async_measure_get_meas.call_count == 1
|
assert withings.get_measurement_in_period.call_count == 1
|
||||||
|
|
||||||
await call_webhook(
|
await call_webhook(
|
||||||
hass,
|
hass,
|
||||||
WEBHOOK_ID,
|
WEBHOOK_ID,
|
||||||
{"userid": USER_ID, "appli": NotifyAppli.WEIGHT},
|
{"userid": USER_ID, "appli": NotificationCategory.WEIGHT},
|
||||||
client,
|
client,
|
||||||
)
|
)
|
||||||
assert withings.async_measure_get_meas.call_count == 2
|
assert withings.get_measurement_in_period.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error",
|
"error",
|
||||||
[
|
[
|
||||||
UnauthorizedException(401),
|
WithingsUnauthorizedError(401),
|
||||||
AuthFailedException(500),
|
WithingsAuthenticationFailedError(500),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_triggering_reauth(
|
async def test_triggering_reauth(
|
||||||
@ -228,7 +240,7 @@ async def test_triggering_reauth(
|
|||||||
"""Test triggering reauth."""
|
"""Test triggering reauth."""
|
||||||
await setup_integration(hass, polling_config_entry, False)
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
withings.async_measure_get_meas.side_effect = error
|
withings.get_measurement_in_period.side_effect = error
|
||||||
freezer.tick(timedelta(minutes=10))
|
freezer.tick(timedelta(minutes=10))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -384,7 +396,7 @@ async def test_setup_with_cloud(
|
|||||||
"homeassistant.components.cloud.async_create_cloudhook",
|
"homeassistant.components.cloud.async_create_cloudhook",
|
||||||
return_value="https://hooks.nabu.casa/ABCD",
|
return_value="https://hooks.nabu.casa/ABCD",
|
||||||
) as fake_create_cloudhook, patch(
|
) as fake_create_cloudhook, patch(
|
||||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
"homeassistant.components.withings.async_get_config_entry_implementation",
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.cloud.async_delete_cloudhook"
|
"homeassistant.components.cloud.async_delete_cloudhook"
|
||||||
) as fake_delete_cloudhook, patch(
|
) as fake_delete_cloudhook, patch(
|
||||||
@ -462,7 +474,7 @@ async def test_cloud_disconnect(
|
|||||||
"homeassistant.components.cloud.async_create_cloudhook",
|
"homeassistant.components.cloud.async_create_cloudhook",
|
||||||
return_value="https://hooks.nabu.casa/ABCD",
|
return_value="https://hooks.nabu.casa/ABCD",
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
"homeassistant.components.withings.async_get_config_entry_implementation",
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.cloud.async_delete_cloudhook"
|
"homeassistant.components.cloud.async_delete_cloudhook"
|
||||||
), patch(
|
), patch(
|
||||||
@ -475,34 +487,31 @@ async def test_cloud_disconnect(
|
|||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
withings.async_notify_list.return_value = NotifyListResponse(
|
withings.list_notification_configurations.return_value = []
|
||||||
**load_json_object_fixture("withings/empty_notify_list.json")
|
|
||||||
)
|
|
||||||
|
|
||||||
assert withings.async_notify_subscribe.call_count == 6
|
assert withings.subscribe_notification.call_count == 6
|
||||||
|
|
||||||
async_mock_cloud_connection_status(hass, False)
|
async_mock_cloud_connection_status(hass, False)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert withings.async_notify_revoke.call_count == 3
|
assert withings.revoke_notification_configurations.call_count == 3
|
||||||
|
|
||||||
async_mock_cloud_connection_status(hass, True)
|
async_mock_cloud_connection_status(hass, True)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert withings.async_notify_subscribe.call_count == 12
|
assert withings.subscribe_notification.call_count == 12
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("body", "expected_code"),
|
("body", "expected_code"),
|
||||||
[
|
[
|
||||||
[{"userid": 0, "appli": NotifyAppli.WEIGHT.value}, 0], # Success
|
[{"userid": 0, "appli": NotificationCategory.WEIGHT.value}, 0], # Success
|
||||||
[{"userid": None, "appli": 1}, 0], # Success, we ignore the user_id.
|
[{"userid": None, "appli": 1}, 0], # Success, we ignore the user_id.
|
||||||
[{}, 12], # No request body.
|
[{}, 12], # No request body.
|
||||||
[{"userid": "GG"}, 20], # appli not provided.
|
[{"userid": "GG"}, 20], # appli not provided.
|
||||||
[{"userid": 0}, 20], # appli not provided.
|
[{"userid": 0}, 20], # appli not provided.
|
||||||
[{"userid": 0, "appli": 99}, 21], # Invalid appli.
|
|
||||||
[
|
[
|
||||||
{"userid": 11, "appli": NotifyAppli.WEIGHT.value},
|
{"userid": 11, "appli": NotificationCategory.WEIGHT.value},
|
||||||
0,
|
0,
|
||||||
], # Success, we ignore the user_id
|
], # Success, we ignore the user_id
|
||||||
],
|
],
|
||||||
|
@ -57,7 +57,7 @@ async def test_update_failed(
|
|||||||
"""Test all entities."""
|
"""Test all entities."""
|
||||||
await setup_integration(hass, polling_config_entry, False)
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
withings.async_measure_get_meas.side_effect = Exception
|
withings.get_measurement_in_period.side_effect = Exception
|
||||||
freezer.tick(timedelta(minutes=10))
|
freezer.tick(timedelta(minutes=10))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user