Add sensors for Withings Goals (#102468)

pull/102058/head
Joost Lekkerkerker 2023-10-21 21:51:37 +02:00 committed by GitHub
parent f0f3a43b09
commit 235a3486ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 664 additions and 445 deletions

View File

@ -56,6 +56,7 @@ from .const import (
CONF_USE_WEBHOOK,
DEFAULT_TITLE,
DOMAIN,
GOALS_COORDINATOR,
LOGGER,
MEASUREMENT_COORDINATOR,
SLEEP_COORDINATOR,
@ -63,6 +64,7 @@ from .const import (
from .coordinator import (
WithingsBedPresenceDataUpdateCoordinator,
WithingsDataUpdateCoordinator,
WithingsGoalsDataUpdateCoordinator,
WithingsMeasurementDataUpdateCoordinator,
WithingsSleepDataUpdateCoordinator,
)
@ -160,6 +162,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
BED_PRESENCE_COORDINATOR: WithingsBedPresenceDataUpdateCoordinator(
hass, client
),
GOALS_COORDINATOR: WithingsGoalsDataUpdateCoordinator(hass, client),
}
for coordinator in coordinators.values():

View File

@ -16,6 +16,7 @@ PUSH_HANDLER = "push_handler"
MEASUREMENT_COORDINATOR = "measurement_coordinator"
SLEEP_COORDINATOR = "sleep_coordinator"
BED_PRESENCE_COORDINATOR = "bed_presence_coordinator"
GOALS_COORDINATOR = "goals_coordinator"
LOGGER = logging.getLogger(__package__)

View File

@ -4,6 +4,7 @@ from datetime import datetime, timedelta
from typing import TypeVar
from aiowithings import (
Goals,
MeasurementType,
NotificationCategory,
SleepSummary,
@ -170,3 +171,17 @@ class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[Non
async def _internal_update_data(self) -> None:
"""Update coordinator data."""
class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]):
"""Withings goals coordinator."""
_default_update_interval = timedelta(hours=1)
def webhook_subscription_listener(self, connected: bool) -> None:
"""Call when webhook status changed."""
# Webhooks aren't available for this datapoint, so we keep polling
async def _internal_update_data(self) -> Goals:
"""Retrieve goals data."""
return await self._client.get_goals()

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from aiowithings import MeasurementType, SleepSummary
from aiowithings import Goals, MeasurementType, SleepSummary
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -27,6 +27,7 @@ from homeassistant.helpers.typing import StateType
from .const import (
DOMAIN,
GOALS_COORDINATOR,
MEASUREMENT_COORDINATOR,
SCORE_POINTS,
SLEEP_COORDINATOR,
@ -37,6 +38,7 @@ from .const import (
)
from .coordinator import (
WithingsDataUpdateCoordinator,
WithingsGoalsDataUpdateCoordinator,
WithingsMeasurementDataUpdateCoordinator,
WithingsSleepDataUpdateCoordinator,
)
@ -396,6 +398,64 @@ SLEEP_SENSORS = [
]
STEP_GOAL = "steps"
SLEEP_GOAL = "sleep"
WEIGHT_GOAL = "weight"
@dataclass
class WithingsGoalsSensorEntityDescriptionMixin:
"""Mixin for describing withings data."""
value_fn: Callable[[Goals], StateType]
@dataclass
class WithingsGoalsSensorEntityDescription(
SensorEntityDescription, WithingsGoalsSensorEntityDescriptionMixin
):
"""Immutable class for describing withings data."""
GOALS_SENSORS: dict[str, WithingsGoalsSensorEntityDescription] = {
STEP_GOAL: WithingsGoalsSensorEntityDescription(
key="step_goal",
value_fn=lambda goals: goals.steps,
icon="mdi:shoe-print",
translation_key="step_goal",
native_unit_of_measurement="Steps",
state_class=SensorStateClass.MEASUREMENT,
),
SLEEP_GOAL: WithingsGoalsSensorEntityDescription(
key="sleep_goal",
value_fn=lambda goals: goals.sleep,
icon="mdi:bed-clock",
translation_key="sleep_goal",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
WEIGHT_GOAL: WithingsGoalsSensorEntityDescription(
key="weight_goal",
value_fn=lambda goals: goals.weight,
translation_key="weight_goal",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
}
def get_current_goals(goals: Goals) -> set[str]:
"""Return a list of present goals."""
result = set()
for goal in (STEP_GOAL, SLEEP_GOAL, WEIGHT_GOAL):
if getattr(goals, goal):
result.add(goal)
return result
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@ -406,8 +466,6 @@ async def async_setup_entry(
DOMAIN
][entry.entry_id][MEASUREMENT_COORDINATOR]
current_measurement_types = set(measurement_coordinator.data)
entities: list[SensorEntity] = []
entities.extend(
WithingsMeasurementSensor(
@ -417,6 +475,8 @@ async def async_setup_entry(
if measurement_type in MEASUREMENT_SENSORS
)
current_measurement_types = set(measurement_coordinator.data)
def _async_measurement_listener() -> None:
"""Listen for new measurements and add sensors if they did not exist."""
received_measurement_types = set(measurement_coordinator.data)
@ -431,6 +491,31 @@ async def async_setup_entry(
)
measurement_coordinator.async_add_listener(_async_measurement_listener)
goals_coordinator: WithingsGoalsDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
][GOALS_COORDINATOR]
current_goals = get_current_goals(goals_coordinator.data)
entities.extend(
WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal])
for goal in current_goals
)
def _async_goals_listener() -> None:
"""Listen for new goals and add sensors if they did not exist."""
received_goals = get_current_goals(goals_coordinator.data)
new_goals = received_goals - current_goals
if new_goals:
current_goals.update(new_goals)
async_add_entities(
WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal])
for goal in new_goals
)
goals_coordinator.async_add_listener(_async_goals_listener)
sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
][SLEEP_COORDINATOR]
@ -492,3 +577,17 @@ class WithingsSleepSensor(WithingsSensor):
def available(self) -> bool:
"""Return if the sensor is available."""
return super().available and self.coordinator.data is not None
class WithingsGoalsSensor(WithingsSensor):
"""Implementation of a Withings goals sensor."""
coordinator: WithingsGoalsDataUpdateCoordinator
entity_description: WithingsGoalsSensorEntityDescription
@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)

View File

@ -134,6 +134,15 @@
},
"wakeup_time": {
"name": "Wakeup time"
},
"step_goal": {
"name": "Step goal"
},
"sleep_goal": {
"name": "Sleep goal"
},
"weight_goal": {
"name": "Weight goal"
}
}
}

View File

@ -3,7 +3,7 @@ from datetime import timedelta
import time
from unittest.mock import AsyncMock, patch
from aiowithings import Device, MeasurementGroup, SleepSummary, WithingsClient
from aiowithings import Device, Goals, MeasurementGroup, SleepSummary, WithingsClient
from aiowithings.models import NotificationConfiguration
import pytest
@ -148,8 +148,12 @@ def mock_withings():
for not_conf in notification_json["profiles"]
]
goals_json = load_json_object_fixture("withings/goals.json")
goals = Goals.from_api(goals_json)
mock = AsyncMock(spec=WithingsClient)
mock.get_devices.return_value = devices
mock.get_goals.return_value = goals
mock.get_measurement_in_period.return_value = measurement_groups
mock.get_measurement_since.return_value = measurement_groups
mock.get_sleep_summary_since.return_value = sleep_summaries

View File

@ -0,0 +1,8 @@
{
"steps": 10000,
"sleep": 28800,
"weight": {
"value": 70500,
"unit": -3
}
}

View File

@ -0,0 +1,6 @@
{
"weight": {
"value": 70500,
"unit": -3
}
}

View File

@ -1,255 +1,5 @@
# serializer version: 1
# name: test_all_entities
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Weight',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_weight',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '70',
})
# ---
# name: test_all_entities.1
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Fat mass',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_mass',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '5',
})
# ---
# name: test_all_entities.10
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Diastolic blood pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'mmhg',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_diastolic_blood_pressure',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '70',
})
# ---
# name: test_all_entities.11
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Systolic blood pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'mmhg',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_systolic_blood_pressure',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities.12
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Heart pulse',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_heart_pulse',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_all_entities.13
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk SpO2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_spo2',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '0.95',
})
# ---
# name: test_all_entities.14
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Hydration',
'icon': 'mdi:water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_hydration',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '0.95',
})
# ---
# name: test_all_entities.15
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speed',
'friendly_name': 'henk Pulse wave velocity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.METERS_PER_SECOND: 'm/s'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_pulse_wave_velocity',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities.16
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk VO2 max',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ml/min/kg',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_vo2_max',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities.17
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Vascular age',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_vascular_age',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities.18
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Extracellular water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_extracellular_water',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities.19
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Intracellular water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_intracellular_water',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities.2
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Fat free mass',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_free_mass',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_all_entities.20
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Breathing disturbances intensity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_breathing_disturbances_intensity',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_all_entities.21
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Deep sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_deep_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '26220',
})
# ---
# name: test_all_entities.22
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Time to sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_time_to_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '780',
})
# ---
# name: test_all_entities.23
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Time to wakeup',
'icon': 'mdi:sleep-off',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_time_to_wakeup',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '996',
})
# ---
# name: test_all_entities.24
# name: test_all_entities[sensor.henk_average_heart_rate]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Average heart rate',
@ -264,69 +14,7 @@
'state': '83',
})
# ---
# name: test_all_entities.25
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Maximum heart rate',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_maximum_heart_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '108',
})
# ---
# name: test_all_entities.26
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Minimum heart rate',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_minimum_heart_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '58',
})
# ---
# name: test_all_entities.27
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Light sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_light_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '58440',
})
# ---
# name: test_all_entities.28
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk REM sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_rem_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '17280',
})
# ---
# name: test_all_entities.29
# name: test_all_entities[sensor.henk_average_respiratory_rate]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Average respiratory rate',
@ -340,122 +28,22 @@
'state': '14',
})
# ---
# name: test_all_entities.3
# name: test_all_entities[sensor.henk_body_temperature]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Muscle mass',
'device_class': 'temperature',
'friendly_name': 'henk Body temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_muscle_mass',
'entity_id': 'sensor.henk_body_temperature',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '50',
'state': '40',
})
# ---
# name: test_all_entities.30
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Maximum respiratory rate',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'br/min',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_maximum_respiratory_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '20',
})
# ---
# name: test_all_entities.31
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Minimum respiratory rate',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'br/min',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_minimum_respiratory_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_all_entities.32
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Sleep score',
'icon': 'mdi:medal',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'points',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_sleep_score',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '90',
})
# ---
# name: test_all_entities.33
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Snoring',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_snoring',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '1044',
})
# ---
# name: test_all_entities.34
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Snoring episode count',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_snoring_episode_count',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '87',
})
# ---
# name: test_all_entities.35
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Wakeup count',
'icon': 'mdi:sleep-off',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'times',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_wakeup_count',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '8',
})
# ---
# name: test_all_entities.36
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Wakeup time',
'icon': 'mdi:sleep-off',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_wakeup_time',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '3468',
})
# ---
# name: test_all_entities.4
# name: test_all_entities[sensor.henk_bone_mass]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
@ -471,7 +59,124 @@
'state': '10',
})
# ---
# name: test_all_entities.5
# name: test_all_entities[sensor.henk_breathing_disturbances_intensity]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Breathing disturbances intensity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_breathing_disturbances_intensity',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_all_entities[sensor.henk_deep_sleep]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Deep sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_deep_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '26220',
})
# ---
# name: test_all_entities[sensor.henk_diastolic_blood_pressure]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Diastolic blood pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'mmhg',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_diastolic_blood_pressure',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '70',
})
# ---
# name: test_all_entities[sensor.henk_extracellular_water]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Extracellular water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_extracellular_water',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[sensor.henk_fat_free_mass]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Fat free mass',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_free_mass',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_all_entities[sensor.henk_fat_mass]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Fat mass',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_mass',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '5',
})
# ---
# name: test_all_entities[sensor.henk_fat_ratio]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Fat ratio',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_ratio',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '0.07',
})
# ---
# name: test_all_entities[sensor.henk_heart_pulse]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Heart pulse',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_heart_pulse',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_all_entities[sensor.henk_height]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'distance',
@ -486,37 +191,158 @@
'state': '2',
})
# ---
# name: test_all_entities.6
# name: test_all_entities[sensor.henk_hydration]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'henk Temperature',
'device_class': 'weight',
'friendly_name': 'henk Hydration',
'icon': 'mdi:water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_temperature',
'entity_id': 'sensor.henk_hydration',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '40',
'state': '0.95',
})
# ---
# name: test_all_entities.7
# name: test_all_entities[sensor.henk_intracellular_water]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'henk Body temperature',
'device_class': 'weight',
'friendly_name': 'henk Intracellular water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_body_temperature',
'entity_id': 'sensor.henk_intracellular_water',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '40',
'state': '100',
})
# ---
# name: test_all_entities.8
# name: test_all_entities[sensor.henk_light_sleep]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Light sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_light_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '58440',
})
# ---
# name: test_all_entities[sensor.henk_maximum_heart_rate]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Maximum heart rate',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_maximum_heart_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '108',
})
# ---
# name: test_all_entities[sensor.henk_maximum_respiratory_rate]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Maximum respiratory rate',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'br/min',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_maximum_respiratory_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '20',
})
# ---
# name: test_all_entities[sensor.henk_minimum_heart_rate]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Minimum heart rate',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_minimum_heart_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '58',
})
# ---
# name: test_all_entities[sensor.henk_minimum_respiratory_rate]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Minimum respiratory rate',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'br/min',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_minimum_respiratory_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_all_entities[sensor.henk_muscle_mass]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Muscle mass',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_muscle_mass',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '50',
})
# ---
# name: test_all_entities[sensor.henk_pulse_wave_velocity]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speed',
'friendly_name': 'henk Pulse wave velocity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.METERS_PER_SECOND: 'm/s'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_pulse_wave_velocity',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[sensor.henk_rem_sleep]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk REM sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_rem_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '17280',
})
# ---
# name: test_all_entities[sensor.henk_skin_temperature]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
@ -531,17 +357,237 @@
'state': '20',
})
# ---
# name: test_all_entities.9
# name: test_all_entities[sensor.henk_sleep_goal]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Fat ratio',
'device_class': 'duration',
'friendly_name': 'henk Sleep goal',
'icon': 'mdi:bed-clock',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_sleep_goal',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '28800',
})
# ---
# name: test_all_entities[sensor.henk_sleep_score]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Sleep score',
'icon': 'mdi:medal',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'points',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_sleep_score',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '90',
})
# ---
# name: test_all_entities[sensor.henk_snoring]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Snoring',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_snoring',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '1044',
})
# ---
# name: test_all_entities[sensor.henk_snoring_episode_count]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Snoring episode count',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_snoring_episode_count',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '87',
})
# ---
# name: test_all_entities[sensor.henk_spo2]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk SpO2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_ratio',
'entity_id': 'sensor.henk_spo2',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '0.07',
'state': '0.95',
})
# ---
# name: test_all_entities[sensor.henk_step_goal]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Step goal',
'icon': 'mdi:shoe-print',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'Steps',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_step_goal',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10000',
})
# ---
# name: test_all_entities[sensor.henk_systolic_blood_pressure]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Systolic blood pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'mmhg',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_systolic_blood_pressure',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[sensor.henk_temperature]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'henk Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_temperature',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '40',
})
# ---
# name: test_all_entities[sensor.henk_time_to_sleep]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Time to sleep',
'icon': 'mdi:sleep',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_time_to_sleep',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '780',
})
# ---
# name: test_all_entities[sensor.henk_time_to_wakeup]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Time to wakeup',
'icon': 'mdi:sleep-off',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_time_to_wakeup',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '996',
})
# ---
# name: test_all_entities[sensor.henk_vascular_age]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Vascular age',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_vascular_age',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[sensor.henk_vo2_max]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk VO2 max',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ml/min/kg',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_vo2_max',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[sensor.henk_wakeup_count]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Wakeup count',
'icon': 'mdi:sleep-off',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'times',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_wakeup_count',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '8',
})
# ---
# name: test_all_entities[sensor.henk_wakeup_time]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Wakeup time',
'icon': 'mdi:sleep-off',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_wakeup_time',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '3468',
})
# ---
# name: test_all_entities[sensor.henk_weight]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Weight',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_weight',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '70',
})
# ---
# name: test_all_entities[sensor.henk_weight_goal]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'henk Weight goal',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_weight_goal',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '70.5',
})
# ---

View File

@ -2,7 +2,7 @@
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from aiowithings import MeasurementGroup
from aiowithings import Goals, MeasurementGroup
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
@ -38,7 +38,9 @@ async def test_all_entities(
assert entity_entries
for entity_entry in entity_entries:
assert hass.states.get(entity_entry.entity_id) == snapshot
assert hass.states.get(entity_entry.entity_id) == snapshot(
name=entity_entry.entity_id
)
async def test_update_failed(
@ -135,3 +137,29 @@ async def test_update_new_measurement_creates_new_sensor(
await hass.async_block_till_done()
assert hass.states.get("sensor.henk_fat_mass") is not None
async def test_update_new_goals_creates_new_sensor(
hass: HomeAssistant,
withings: AsyncMock,
polling_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test fetching new goals will add a new sensor."""
goals_json = load_json_object_fixture("withings/goals_1.json")
goals = Goals.from_api(goals_json)
withings.get_goals.return_value = goals
await setup_integration(hass, polling_config_entry, False)
assert hass.states.get("sensor.henk_step_goal") is None
assert hass.states.get("sensor.henk_weight_goal") is not None
goals_json = load_json_object_fixture("withings/goals.json")
goals = Goals.from_api(goals_json)
withings.get_goals.return_value = goals
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.henk_step_goal") is not None