mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add devices to Withings (#126853)
This commit is contained in:
parent
05288dad51
commit
10805805fe
@ -48,6 +48,7 @@ from .coordinator import (
|
|||||||
WithingsActivityDataUpdateCoordinator,
|
WithingsActivityDataUpdateCoordinator,
|
||||||
WithingsBedPresenceDataUpdateCoordinator,
|
WithingsBedPresenceDataUpdateCoordinator,
|
||||||
WithingsDataUpdateCoordinator,
|
WithingsDataUpdateCoordinator,
|
||||||
|
WithingsDeviceDataUpdateCoordinator,
|
||||||
WithingsGoalsDataUpdateCoordinator,
|
WithingsGoalsDataUpdateCoordinator,
|
||||||
WithingsMeasurementDataUpdateCoordinator,
|
WithingsMeasurementDataUpdateCoordinator,
|
||||||
WithingsSleepDataUpdateCoordinator,
|
WithingsSleepDataUpdateCoordinator,
|
||||||
@ -73,6 +74,7 @@ class WithingsData:
|
|||||||
goals_coordinator: WithingsGoalsDataUpdateCoordinator
|
goals_coordinator: WithingsGoalsDataUpdateCoordinator
|
||||||
activity_coordinator: WithingsActivityDataUpdateCoordinator
|
activity_coordinator: WithingsActivityDataUpdateCoordinator
|
||||||
workout_coordinator: WithingsWorkoutDataUpdateCoordinator
|
workout_coordinator: WithingsWorkoutDataUpdateCoordinator
|
||||||
|
device_coordinator: WithingsDeviceDataUpdateCoordinator
|
||||||
coordinators: set[WithingsDataUpdateCoordinator] = field(default_factory=set)
|
coordinators: set[WithingsDataUpdateCoordinator] = field(default_factory=set)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
@ -84,6 +86,7 @@ class WithingsData:
|
|||||||
self.goals_coordinator,
|
self.goals_coordinator,
|
||||||
self.activity_coordinator,
|
self.activity_coordinator,
|
||||||
self.workout_coordinator,
|
self.workout_coordinator,
|
||||||
|
self.device_coordinator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -122,6 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WithingsConfigEntry) ->
|
|||||||
goals_coordinator=WithingsGoalsDataUpdateCoordinator(hass, client),
|
goals_coordinator=WithingsGoalsDataUpdateCoordinator(hass, client),
|
||||||
activity_coordinator=WithingsActivityDataUpdateCoordinator(hass, client),
|
activity_coordinator=WithingsActivityDataUpdateCoordinator(hass, client),
|
||||||
workout_coordinator=WithingsWorkoutDataUpdateCoordinator(hass, client),
|
workout_coordinator=WithingsWorkoutDataUpdateCoordinator(hass, client),
|
||||||
|
device_coordinator=WithingsDeviceDataUpdateCoordinator(hass, client),
|
||||||
)
|
)
|
||||||
|
|
||||||
for coordinator in withings_data.coordinators:
|
for coordinator in withings_data.coordinators:
|
||||||
|
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from aiowithings import (
|
from aiowithings import (
|
||||||
Activity,
|
Activity,
|
||||||
|
Device,
|
||||||
Goals,
|
Goals,
|
||||||
MeasurementPosition,
|
MeasurementPosition,
|
||||||
MeasurementType,
|
MeasurementType,
|
||||||
@ -291,3 +292,17 @@ class WithingsWorkoutDataUpdateCoordinator(
|
|||||||
self._previous_data = latest_workout
|
self._previous_data = latest_workout
|
||||||
self._last_valid_update = latest_workout.end_date
|
self._last_valid_update = latest_workout.end_date
|
||||||
return self._previous_data
|
return self._previous_data
|
||||||
|
|
||||||
|
|
||||||
|
class WithingsDeviceDataUpdateCoordinator(
|
||||||
|
WithingsDataUpdateCoordinator[dict[str, Device]]
|
||||||
|
):
|
||||||
|
"""Withings device coordinator."""
|
||||||
|
|
||||||
|
coordinator_name: str = "device"
|
||||||
|
_default_update_interval = timedelta(hours=1)
|
||||||
|
|
||||||
|
async def _internal_update_data(self) -> dict[str, Device]:
|
||||||
|
"""Update coordinator data."""
|
||||||
|
devices = await self._client.get_devices()
|
||||||
|
return {device.device_id: device for device in devices}
|
||||||
|
@ -4,11 +4,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from aiowithings import Device
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import WithingsDataUpdateCoordinator
|
from .coordinator import (
|
||||||
|
WithingsDataUpdateCoordinator,
|
||||||
|
WithingsDeviceDataUpdateCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WithingsEntity[_T: WithingsDataUpdateCoordinator[Any]](CoordinatorEntity[_T]):
|
class WithingsEntity[_T: WithingsDataUpdateCoordinator[Any]](CoordinatorEntity[_T]):
|
||||||
@ -28,3 +33,35 @@ class WithingsEntity[_T: WithingsDataUpdateCoordinator[Any]](CoordinatorEntity[_
|
|||||||
identifiers={(DOMAIN, str(coordinator.config_entry.unique_id))},
|
identifiers={(DOMAIN, str(coordinator.config_entry.unique_id))},
|
||||||
manufacturer="Withings",
|
manufacturer="Withings",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WithingsDeviceEntity(WithingsEntity[WithingsDeviceDataUpdateCoordinator]):
|
||||||
|
"""Base class for withings device entities."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: WithingsDeviceDataUpdateCoordinator,
|
||||||
|
device_id: str,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Withings entity."""
|
||||||
|
super().__init__(coordinator, key)
|
||||||
|
self._attr_unique_id = f"{device_id}_{key}"
|
||||||
|
self.device_id = device_id
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device_id)},
|
||||||
|
manufacturer="Withings",
|
||||||
|
name=self.device.raw_model,
|
||||||
|
model=self.device.raw_model,
|
||||||
|
via_device=(DOMAIN, str(coordinator.config_entry.unique_id)),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return super().available and self.device_id in self.coordinator.data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self) -> Device:
|
||||||
|
"""Return the Withings device."""
|
||||||
|
return self.coordinator.data[self.device_id]
|
||||||
|
@ -136,6 +136,14 @@
|
|||||||
},
|
},
|
||||||
"workout_duration": {
|
"workout_duration": {
|
||||||
"default": "mdi:timer"
|
"default": "mdi:timer"
|
||||||
|
},
|
||||||
|
"battery": {
|
||||||
|
"default": "mdi:battery-off",
|
||||||
|
"state": {
|
||||||
|
"low": "mdi:battery-20",
|
||||||
|
"medium": "mdi:battery-50",
|
||||||
|
"high": "mdi:battery"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aiowithings import (
|
from aiowithings import (
|
||||||
Activity,
|
Activity,
|
||||||
|
Device,
|
||||||
Goals,
|
Goals,
|
||||||
MeasurementPosition,
|
MeasurementPosition,
|
||||||
MeasurementType,
|
MeasurementType,
|
||||||
@ -23,6 +24,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
Platform,
|
Platform,
|
||||||
@ -33,8 +35,8 @@ from homeassistant.const import (
|
|||||||
UnitOfTime,
|
UnitOfTime,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
import homeassistant.helpers.entity_registry as er
|
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
@ -51,12 +53,13 @@ from .const import (
|
|||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
WithingsActivityDataUpdateCoordinator,
|
WithingsActivityDataUpdateCoordinator,
|
||||||
WithingsDataUpdateCoordinator,
|
WithingsDataUpdateCoordinator,
|
||||||
|
WithingsDeviceDataUpdateCoordinator,
|
||||||
WithingsGoalsDataUpdateCoordinator,
|
WithingsGoalsDataUpdateCoordinator,
|
||||||
WithingsMeasurementDataUpdateCoordinator,
|
WithingsMeasurementDataUpdateCoordinator,
|
||||||
WithingsSleepDataUpdateCoordinator,
|
WithingsSleepDataUpdateCoordinator,
|
||||||
WithingsWorkoutDataUpdateCoordinator,
|
WithingsWorkoutDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from .entity import WithingsEntity
|
from .entity import WithingsDeviceEntity, WithingsEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -650,6 +653,24 @@ WORKOUT_SENSORS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class WithingsDeviceSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Immutable class for describing withings data."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Device], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
DEVICE_SENSORS = [
|
||||||
|
WithingsDeviceSensorEntityDescription(
|
||||||
|
key="battery",
|
||||||
|
translation_key="battery",
|
||||||
|
options=["low", "medium", "high"],
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda device: device.battery,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_current_goals(goals: Goals) -> set[str]:
|
def get_current_goals(goals: Goals) -> set[str]:
|
||||||
"""Return a list of present goals."""
|
"""Return a list of present goals."""
|
||||||
result = set()
|
result = set()
|
||||||
@ -800,6 +821,48 @@ async def async_setup_entry(
|
|||||||
_async_add_workout_entities
|
_async_add_workout_entities
|
||||||
)
|
)
|
||||||
|
|
||||||
|
device_coordinator = withings_data.device_coordinator
|
||||||
|
|
||||||
|
current_devices: set[str] = set()
|
||||||
|
|
||||||
|
def _async_device_listener() -> None:
|
||||||
|
"""Add device entities."""
|
||||||
|
received_devices = set(device_coordinator.data)
|
||||||
|
new_devices = received_devices - current_devices
|
||||||
|
old_devices = current_devices - received_devices
|
||||||
|
if new_devices:
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
for device_id in new_devices:
|
||||||
|
if device := device_registry.async_get_device({(DOMAIN, device_id)}):
|
||||||
|
if any(
|
||||||
|
(
|
||||||
|
config_entry := hass.config_entries.async_get_entry(
|
||||||
|
config_entry_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
and config_entry.state == ConfigEntryState.LOADED
|
||||||
|
for config_entry_id in device.config_entries
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
async_add_entities(
|
||||||
|
WithingsDeviceSensor(device_coordinator, description, device_id)
|
||||||
|
for description in DEVICE_SENSORS
|
||||||
|
)
|
||||||
|
current_devices.add(device_id)
|
||||||
|
|
||||||
|
if old_devices:
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
for device_id in old_devices:
|
||||||
|
if device := device_registry.async_get_device({(DOMAIN, device_id)}):
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device.id, remove_config_entry_id=entry.entry_id
|
||||||
|
)
|
||||||
|
current_devices.remove(device_id)
|
||||||
|
|
||||||
|
device_coordinator.async_add_listener(_async_device_listener)
|
||||||
|
|
||||||
|
_async_device_listener()
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"No data found for Withings entry %s, sensors will be added when new data is available"
|
"No data found for Withings entry %s, sensors will be added when new data is available"
|
||||||
@ -923,3 +986,24 @@ class WithingsWorkoutSensor(
|
|||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
|
||||||
|
class WithingsDeviceSensor(WithingsDeviceEntity, SensorEntity):
|
||||||
|
"""Implementation of a Withings workout sensor."""
|
||||||
|
|
||||||
|
entity_description: WithingsDeviceSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: WithingsDeviceDataUpdateCoordinator,
|
||||||
|
entity_description: WithingsDeviceSensorEntityDescription,
|
||||||
|
device_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize sensor."""
|
||||||
|
super().__init__(coordinator, device_id, entity_description.key)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
return self.entity_description.value_fn(self.device)
|
||||||
|
@ -307,6 +307,14 @@
|
|||||||
},
|
},
|
||||||
"workout_duration": {
|
"workout_duration": {
|
||||||
"name": "Last workout duration"
|
"name": "Last workout duration"
|
||||||
|
},
|
||||||
|
"battery": {
|
||||||
|
"name": "[%key:component::sensor::entity_component::battery::name%]",
|
||||||
|
"state": {
|
||||||
|
"low": "Low",
|
||||||
|
"medium": "Medium",
|
||||||
|
"high": "High"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ from typing import Any
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
from aiowithings import Activity, Goals, MeasurementGroup, SleepSummary, Workout
|
from aiowithings import Activity, Device, Goals, MeasurementGroup, SleepSummary, Workout
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_url
|
from homeassistant.components.webhook import async_generate_url
|
||||||
@ -109,3 +109,11 @@ def load_sleep_fixture(
|
|||||||
"""Return sleep summaries from fixture."""
|
"""Return sleep summaries from fixture."""
|
||||||
sleep_json = load_json_array_fixture("withings/sleep_summaries.json")
|
sleep_json = load_json_array_fixture("withings/sleep_summaries.json")
|
||||||
return [SleepSummary.from_api(sleep_summary) for sleep_summary in sleep_json]
|
return [SleepSummary.from_api(sleep_summary) for sleep_summary in sleep_json]
|
||||||
|
|
||||||
|
|
||||||
|
def load_device_fixture(
|
||||||
|
fixture: str = "withings/devices.json",
|
||||||
|
) -> list[Device]:
|
||||||
|
"""Return sleep summaries from fixture."""
|
||||||
|
devices_json = load_json_array_fixture(fixture)
|
||||||
|
return [Device.from_api(device) for device in devices_json]
|
||||||
|
@ -133,6 +133,29 @@ def polling_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def second_polling_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
||||||
|
"""Create Withings entry in Home Assistant."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Not Henk",
|
||||||
|
unique_id="54321",
|
||||||
|
data={
|
||||||
|
"auth_implementation": DOMAIN,
|
||||||
|
"token": {
|
||||||
|
"status": 0,
|
||||||
|
"userid": "54321",
|
||||||
|
"access_token": "mock-access-token",
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"scope": ",".join(scopes),
|
||||||
|
},
|
||||||
|
"profile": TITLE,
|
||||||
|
"webhook_id": WEBHOOK_ID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="withings")
|
@pytest.fixture(name="withings")
|
||||||
def mock_withings():
|
def mock_withings():
|
||||||
"""Mock withings."""
|
"""Mock withings."""
|
||||||
|
65
tests/components/withings/snapshots/test_init.ambr
Normal file
65
tests/components/withings/snapshots/test_init.ambr
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_devices[12345]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'withings',
|
||||||
|
'12345',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Withings',
|
||||||
|
'model': None,
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'henk',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': None,
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_devices[f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'withings',
|
||||||
|
'f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Withings',
|
||||||
|
'model': 'Body+',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Body+',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': None,
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': <ANY>,
|
||||||
|
})
|
||||||
|
# ---
|
@ -1,4 +1,62 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
|
# name: test_all_entities[sensor.body_battery-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.body_battery',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Battery',
|
||||||
|
'platform': 'withings',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'battery',
|
||||||
|
'unique_id': 'f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d_battery',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.body_battery-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'enum',
|
||||||
|
'friendly_name': 'Body+ Battery',
|
||||||
|
'options': list([
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.body_battery',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'high',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_all_entities[sensor.henk_active_calories_burnt_today-entry]
|
# name: test_all_entities[sensor.henk_active_calories_burnt_today-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
@ -14,6 +14,7 @@ from aiowithings import (
|
|||||||
)
|
)
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import cloud
|
from homeassistant.components import cloud
|
||||||
@ -22,6 +23,7 @@ from homeassistant.components.webhook import async_generate_url
|
|||||||
from homeassistant.components.withings.const import DOMAIN
|
from homeassistant.components.withings.const import DOMAIN
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
from homeassistant.const import CONF_WEBHOOK_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import call_webhook, prepare_webhook_setup, setup_integration
|
from . import call_webhook, prepare_webhook_setup, setup_integration
|
||||||
@ -569,3 +571,21 @@ async def test_webhook_post(
|
|||||||
resp.close()
|
resp.close()
|
||||||
|
|
||||||
assert data["code"] == expected_code
|
assert data["code"] == expected_code
|
||||||
|
|
||||||
|
|
||||||
|
async def test_devices(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
webhook_config_entry: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test devices."""
|
||||||
|
await setup_integration(hass, webhook_config_entry)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for device_id in ("12345", "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d"):
|
||||||
|
device = device_registry.async_get_device({(DOMAIN, device_id)})
|
||||||
|
assert device is not None
|
||||||
|
assert device == snapshot(name=device_id)
|
||||||
|
@ -8,12 +8,14 @@ from freezegun.api import FrozenDateTimeFactory
|
|||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.withings import DOMAIN
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
load_activity_fixture,
|
load_activity_fixture,
|
||||||
|
load_device_fixture,
|
||||||
load_goals_fixture,
|
load_goals_fixture,
|
||||||
load_measurements_fixture,
|
load_measurements_fixture,
|
||||||
load_sleep_fixture,
|
load_sleep_fixture,
|
||||||
@ -351,3 +353,83 @@ async def test_warning_if_no_entities_created(
|
|||||||
await setup_integration(hass, polling_config_entry, False)
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
assert "No data found for Withings entry" in caplog.text
|
assert "No data found for Withings entry" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_sensors_created_when_device_data_received(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test device sensors will be added if we receive device data."""
|
||||||
|
withings.get_devices.return_value = []
|
||||||
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery") is None
|
||||||
|
|
||||||
|
freezer.tick(timedelta(hours=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery") is None
|
||||||
|
|
||||||
|
withings.get_devices.return_value = load_device_fixture()
|
||||||
|
|
||||||
|
freezer.tick(timedelta(hours=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery")
|
||||||
|
assert device_registry.async_get_device(
|
||||||
|
{(DOMAIN, "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d")}
|
||||||
|
)
|
||||||
|
|
||||||
|
withings.get_devices.return_value = []
|
||||||
|
|
||||||
|
freezer.tick(timedelta(hours=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery") is None
|
||||||
|
assert not device_registry.async_get_device(
|
||||||
|
{(DOMAIN, "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d")}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_two_config_entries(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
second_polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device sensors will be added for one config entry only at a time."""
|
||||||
|
await setup_integration(hass, polling_config_entry, False)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery") is not None
|
||||||
|
|
||||||
|
second_polling_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(second_polling_config_entry.entry_id)
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.not_henk_temperature") is not None
|
||||||
|
|
||||||
|
assert "Platform withings does not generate unique IDs" not in caplog.text
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(polling_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery").state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
freezer.tick(timedelta(hours=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("sensor.body_battery").state != STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(polling_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Platform withings does not generate unique IDs" not in caplog.text
|
||||||
|
Loading…
x
Reference in New Issue
Block a user