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

View File

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

View File

@ -4,6 +4,7 @@ from datetime import datetime, timedelta
from typing import TypeVar from typing import TypeVar
from aiowithings import ( from aiowithings import (
Goals,
MeasurementType, MeasurementType,
NotificationCategory, NotificationCategory,
SleepSummary, SleepSummary,
@ -170,3 +171,17 @@ class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[Non
async def _internal_update_data(self) -> None: async def _internal_update_data(self) -> None:
"""Update coordinator data.""" """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 collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from aiowithings import MeasurementType, SleepSummary from aiowithings import Goals, MeasurementType, SleepSummary
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -27,6 +27,7 @@ from homeassistant.helpers.typing import StateType
from .const import ( from .const import (
DOMAIN, DOMAIN,
GOALS_COORDINATOR,
MEASUREMENT_COORDINATOR, MEASUREMENT_COORDINATOR,
SCORE_POINTS, SCORE_POINTS,
SLEEP_COORDINATOR, SLEEP_COORDINATOR,
@ -37,6 +38,7 @@ from .const import (
) )
from .coordinator import ( from .coordinator import (
WithingsDataUpdateCoordinator, WithingsDataUpdateCoordinator,
WithingsGoalsDataUpdateCoordinator,
WithingsMeasurementDataUpdateCoordinator, WithingsMeasurementDataUpdateCoordinator,
WithingsSleepDataUpdateCoordinator, 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
@ -406,8 +466,6 @@ async def async_setup_entry(
DOMAIN DOMAIN
][entry.entry_id][MEASUREMENT_COORDINATOR] ][entry.entry_id][MEASUREMENT_COORDINATOR]
current_measurement_types = set(measurement_coordinator.data)
entities: list[SensorEntity] = [] entities: list[SensorEntity] = []
entities.extend( entities.extend(
WithingsMeasurementSensor( WithingsMeasurementSensor(
@ -417,6 +475,8 @@ async def async_setup_entry(
if measurement_type in MEASUREMENT_SENSORS if measurement_type in MEASUREMENT_SENSORS
) )
current_measurement_types = set(measurement_coordinator.data)
def _async_measurement_listener() -> None: def _async_measurement_listener() -> None:
"""Listen for new measurements and add sensors if they did not exist.""" """Listen for new measurements and add sensors if they did not exist."""
received_measurement_types = set(measurement_coordinator.data) received_measurement_types = set(measurement_coordinator.data)
@ -431,6 +491,31 @@ async def async_setup_entry(
) )
measurement_coordinator.async_add_listener(_async_measurement_listener) 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][ sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id entry.entry_id
][SLEEP_COORDINATOR] ][SLEEP_COORDINATOR]
@ -492,3 +577,17 @@ class WithingsSleepSensor(WithingsSensor):
def available(self) -> bool: def available(self) -> bool:
"""Return if the sensor is available.""" """Return if the sensor is available."""
return super().available and self.coordinator.data is not None 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": { "wakeup_time": {
"name": "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 import time
from unittest.mock import AsyncMock, patch 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 from aiowithings.models import NotificationConfiguration
import pytest import pytest
@ -148,8 +148,12 @@ def mock_withings():
for not_conf in notification_json["profiles"] 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 = AsyncMock(spec=WithingsClient)
mock.get_devices.return_value = devices mock.get_devices.return_value = devices
mock.get_goals.return_value = goals
mock.get_measurement_in_period.return_value = measurement_groups mock.get_measurement_in_period.return_value = measurement_groups
mock.get_measurement_since.return_value = measurement_groups mock.get_measurement_since.return_value = measurement_groups
mock.get_sleep_summary_since.return_value = sleep_summaries 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 # serializer version: 1
# name: test_all_entities # name: test_all_entities[sensor.henk_average_heart_rate]
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
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'henk Average heart rate', 'friendly_name': 'henk Average heart rate',
@ -264,69 +14,7 @@
'state': '83', 'state': '83',
}) })
# --- # ---
# name: test_all_entities.25 # name: test_all_entities[sensor.henk_average_respiratory_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.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
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'henk Average respiratory rate', 'friendly_name': 'henk Average respiratory rate',
@ -340,122 +28,22 @@
'state': '14', 'state': '14',
}) })
# --- # ---
# name: test_all_entities.3 # name: test_all_entities[sensor.henk_body_temperature]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'weight', 'device_class': 'temperature',
'friendly_name': 'henk Muscle mass', 'friendly_name': 'henk Body temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>, 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.henk_muscle_mass', 'entity_id': 'sensor.henk_body_temperature',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '50', 'state': '40',
}) })
# --- # ---
# name: test_all_entities.30 # name: test_all_entities[sensor.henk_bone_mass]
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
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'weight', 'device_class': 'weight',
@ -471,7 +59,124 @@
'state': '10', '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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'distance', 'device_class': 'distance',
@ -486,37 +191,158 @@
'state': '2', 'state': '2',
}) })
# --- # ---
# name: test_all_entities.6 # name: test_all_entities[sensor.henk_hydration]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'weight',
'friendly_name': 'henk Temperature', 'friendly_name': 'henk Hydration',
'icon': 'mdi:water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.henk_temperature', 'entity_id': 'sensor.henk_hydration',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '40', 'state': '0.95',
}) })
# --- # ---
# name: test_all_entities.7 # name: test_all_entities[sensor.henk_intracellular_water]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'weight',
'friendly_name': 'henk Body temperature', 'friendly_name': 'henk Intracellular water',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfMass.KILOGRAMS: 'kg'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.henk_body_temperature', 'entity_id': 'sensor.henk_intracellular_water',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'temperature',
@ -531,17 +357,237 @@
'state': '20', 'state': '20',
}) })
# --- # ---
# name: test_all_entities.9 # name: test_all_entities[sensor.henk_sleep_goal]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ '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'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.henk_fat_ratio', 'entity_id': 'sensor.henk_spo2',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <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 datetime import timedelta
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiowithings import MeasurementGroup from aiowithings import Goals, MeasurementGroup
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -38,7 +38,9 @@ async def test_all_entities(
assert entity_entries assert entity_entries
for entity_entry in 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( async def test_update_failed(
@ -135,3 +137,29 @@ async def test_update_new_measurement_creates_new_sensor(
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("sensor.henk_fat_mass") is not None 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