From c86025c12d6501d9c7b328d0019e92b429d3606a Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 01:00:34 +0200 Subject: [PATCH 1/9] Add activity sensors to Withings --- homeassistant/components/withings/__init__.py | 3 + homeassistant/components/withings/const.py | 1 + .../components/withings/coordinator.py | 42 +++++++++++++- homeassistant/components/withings/sensor.py | 57 ++++++++++++++++++- 4 files changed, 100 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index ef91f3368a9740..9460648d918fde 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -51,6 +51,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( + ACTIVITY_COORDINATOR, BED_PRESENCE_COORDINATOR, CONF_PROFILES, CONF_USE_WEBHOOK, @@ -62,6 +63,7 @@ SLEEP_COORDINATOR, ) from .coordinator import ( + WithingsActivityDataUpdateCoordinator, WithingsBedPresenceDataUpdateCoordinator, WithingsDataUpdateCoordinator, WithingsGoalsDataUpdateCoordinator, @@ -163,6 +165,7 @@ async def _refresh_token() -> str: hass, client ), GOALS_COORDINATOR: WithingsGoalsDataUpdateCoordinator(hass, client), + ACTIVITY_COORDINATOR: WithingsActivityDataUpdateCoordinator(hass, client), } for coordinator in coordinators.values(): diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index f04500bb3b89d7..4e12b2fb0bfae5 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -17,6 +17,7 @@ SLEEP_COORDINATOR = "sleep_coordinator" BED_PRESENCE_COORDINATOR = "bed_presence_coordinator" GOALS_COORDINATOR = "goals_coordinator" +ACTIVITY_COORDINATOR = "activity_coordinator" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 2700b833cea918..a9f8f338336291 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -1,9 +1,10 @@ """Withings coordinator.""" from abc import abstractmethod -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from typing import TypeVar from aiowithings import ( + Activity, Goals, MeasurementType, NotificationCategory, @@ -81,7 +82,6 @@ def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: super().__init__(hass, client) self.notification_categories = { NotificationCategory.WEIGHT, - NotificationCategory.ACTIVITY, NotificationCategory.PRESSURE, } self._previous_data: dict[MeasurementType, float] = {} @@ -185,3 +185,41 @@ def webhook_subscription_listener(self, connected: bool) -> None: async def _internal_update_data(self) -> Goals: """Retrieve goals data.""" return await self._client.get_goals() + + +class WithingsActivityDataUpdateCoordinator( + WithingsDataUpdateCoordinator[Activity | None] +): + """Withings activity coordinator.""" + + _last_activity_date: date | None = None + _previous_data: Activity | None = None + + def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: + """Initialize the Withings data coordinator.""" + super().__init__(hass, client) + self.notification_categories = { + NotificationCategory.ACTIVITY, + } + + async def _internal_update_data(self) -> Activity | None: + """Retrieve latest activity.""" + if self._last_valid_update is None: + now = dt_util.utcnow() + startdate = now - timedelta(days=14) + activities = await self._client.get_activities_in_period(startdate, now) + else: + activities = await self._client.get_activities_since( + self._last_valid_update + ) + + if activities: + latest_activity = max(activities, key=lambda activity: activity.date) + if ( + self._last_activity_date is None + or latest_activity.date >= self._last_activity_date + ): + self._last_activity_date = latest_activity.date + self._previous_data = latest_activity + return latest_activity + return self._previous_data diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 54c13500e1ddbf..110d81aa2aa92f 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass -from aiowithings import Goals, MeasurementType, SleepSummary +from aiowithings import Activity, Goals, MeasurementType, SleepSummary from homeassistant.components.sensor import ( SensorDeviceClass, @@ -26,6 +26,7 @@ from homeassistant.helpers.typing import StateType from .const import ( + ACTIVITY_COORDINATOR, DOMAIN, GOALS_COORDINATOR, MEASUREMENT_COORDINATOR, @@ -37,6 +38,7 @@ UOM_MMHG, ) from .coordinator import ( + WithingsActivityDataUpdateCoordinator, WithingsDataUpdateCoordinator, WithingsGoalsDataUpdateCoordinator, WithingsMeasurementDataUpdateCoordinator, @@ -398,6 +400,31 @@ class WithingsSleepSensorEntityDescription( ] +@dataclass +class WithingsActivitySensorEntityDescriptionMixin: + """Mixin for describing withings data.""" + + value_fn: Callable[[Activity], StateType] + + +@dataclass +class WithingsActivitySensorEntityDescription( + SensorEntityDescription, WithingsActivitySensorEntityDescriptionMixin +): + """Immutable class for describing withings data.""" + + +ACTIVITY_SENSORS = [ + WithingsActivitySensorEntityDescription( + key="activity_steps", + value_fn=lambda activity: activity.steps, + translation_key="activity_steps", + native_unit_of_measurement="Steps", + state_class=SensorStateClass.MEASUREMENT, + ), +] + + STEP_GOAL = "steps" SLEEP_GOAL = "sleep" WEIGHT_GOAL = "weight" @@ -516,6 +543,15 @@ def _async_goals_listener() -> None: goals_coordinator.async_add_listener(_async_goals_listener) + activity_coordinator: WithingsActivityDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ][ACTIVITY_COORDINATOR] + + entities.extend( + WithingsActivitySensor(activity_coordinator, attribute) + for attribute in ACTIVITY_SENSORS + ) + sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ][SLEEP_COORDINATOR] @@ -591,3 +627,22 @@ def native_value(self) -> StateType: """Return the state of the entity.""" assert self.coordinator.data return self.entity_description.value_fn(self.coordinator.data) + + +class WithingsActivitySensor(WithingsSensor): + """Implementation of a Withings activity sensor.""" + + coordinator: WithingsActivityDataUpdateCoordinator + + entity_description: WithingsActivitySensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the state of the entity.""" + assert self.coordinator.data + return self.entity_description.value_fn(self.coordinator.data) + + @property + def available(self) -> bool: + """Return if the sensor is available.""" + return super().available and self.coordinator.data is not None From eac347048a320f7f8f8f34315bd2f13985a11a45 Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 16:10:25 +0200 Subject: [PATCH 2/9] Add activity sensors to Withings --- .../components/withings/coordinator.py | 23 +- homeassistant/components/withings/sensor.py | 110 ++++++- .../components/withings/strings.json | 27 ++ tests/components/withings/conftest.py | 13 +- .../withings/fixtures/activity.json | 282 ++++++++++++++++++ .../withings/snapshots/test_sensor.ambr | 134 +++++++++ tests/components/withings/test_sensor.py | 92 +++++- 7 files changed, 653 insertions(+), 28 deletions(-) create mode 100644 tests/components/withings/fixtures/activity.json diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index a9f8f338336291..c6245380a3e350 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -192,7 +192,6 @@ class WithingsActivityDataUpdateCoordinator( ): """Withings activity coordinator.""" - _last_activity_date: date | None = None _previous_data: Activity | None = None def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: @@ -207,19 +206,19 @@ async def _internal_update_data(self) -> Activity | None: if self._last_valid_update is None: now = dt_util.utcnow() startdate = now - timedelta(days=14) - activities = await self._client.get_activities_in_period(startdate, now) + activities = await self._client.get_activities_in_period( + startdate.date(), now.date() + ) else: activities = await self._client.get_activities_since( self._last_valid_update ) - if activities: - latest_activity = max(activities, key=lambda activity: activity.date) - if ( - self._last_activity_date is None - or latest_activity.date >= self._last_activity_date - ): - self._last_activity_date = latest_activity.date - self._previous_data = latest_activity - return latest_activity - return self._previous_data + for activity in activities: + if activity.date == date.today(): + self._previous_data = activity + self._last_valid_update = activity.modified + return activity + if self._previous_data and self._previous_data.date == date.today(): + return self._previous_data + return None diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 110d81aa2aa92f..2f6496c10694a2 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, + Platform, UnitOfLength, UnitOfMass, UnitOfSpeed, @@ -23,6 +24,7 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import StateType from .const import ( @@ -416,12 +418,83 @@ class WithingsActivitySensorEntityDescription( ACTIVITY_SENSORS = [ WithingsActivitySensorEntityDescription( - key="activity_steps", + key="activity_steps_today", value_fn=lambda activity: activity.steps, - translation_key="activity_steps", + translation_key="activity_steps_today", + icon="mdi:shoe-print", native_unit_of_measurement="Steps", state_class=SensorStateClass.MEASUREMENT, ), + WithingsActivitySensorEntityDescription( + key="activity_distance_today", + value_fn=lambda activity: activity.distance, + translation_key="activity_distance_today", + suggested_display_precision=0, + icon="mdi:map-marker-distance", + native_unit_of_measurement=UnitOfLength.METERS, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_floors_climbed_today", + value_fn=lambda activity: activity.floors_climbed, + translation_key="activity_floors_climbed_today", + icon="mdi:stairs-up", + native_unit_of_measurement="Floors", + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_soft_duration_today", + value_fn=lambda activity: activity.soft_activity, + translation_key="activity_soft_duration_today", + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.MINUTES, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_moderate_duration_today", + value_fn=lambda activity: activity.moderate_activity, + translation_key="activity_moderate_duration_today", + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.MINUTES, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_intense_duration_today", + value_fn=lambda activity: activity.intense_activity, + translation_key="activity_intense_duration_today", + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.MINUTES, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_active_duration_today", + value_fn=lambda activity: activity.total_time_active, + translation_key="activity_active_duration_today", + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_active_calories_burnt_today", + value_fn=lambda activity: activity.active_calories_burnt, + suggested_display_precision=1, + translation_key="activity_active_calories_burnt_today", + native_unit_of_measurement="Calories", + state_class=SensorStateClass.MEASUREMENT, + ), + WithingsActivitySensorEntityDescription( + key="activity_total_calories_burnt_today", + value_fn=lambda activity: activity.total_calories_burnt, + suggested_display_precision=1, + translation_key="activity_total_calories_burnt_today", + native_unit_of_measurement="Calories", + state_class=SensorStateClass.MEASUREMENT, + ), ] @@ -489,6 +562,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" + ent_reg = er.async_get(hass) + measurement_coordinator: WithingsMeasurementDataUpdateCoordinator = hass.data[ DOMAIN ][entry.entry_id][MEASUREMENT_COORDINATOR] @@ -547,10 +622,25 @@ def _async_goals_listener() -> None: entry.entry_id ][ACTIVITY_COORDINATOR] - entities.extend( - WithingsActivitySensor(activity_coordinator, attribute) - for attribute in ACTIVITY_SENSORS - ) + activity_callback: Callable[[], None] | None = None + + def _async_add_activity_entities() -> None: + """Add activity entities.""" + async_add_entities( + WithingsActivitySensor(activity_coordinator, attribute) + for attribute in ACTIVITY_SENSORS + ) + if activity_callback: + activity_callback() + + if activity_coordinator.data is not None or ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_activity_steps_today" + ): + _async_add_activity_entities() + else: + activity_callback = activity_coordinator.async_add_listener( + _async_add_activity_entities + ) sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id @@ -639,10 +729,6 @@ class WithingsActivitySensor(WithingsSensor): @property def native_value(self) -> StateType: """Return the state of the entity.""" - assert self.coordinator.data + if not self.coordinator.data: + return None return self.entity_description.value_fn(self.coordinator.data) - - @property - def available(self) -> bool: - """Return if the sensor is available.""" - return super().available and self.coordinator.data is not None diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index fcb94d6979abba..a6a832d83949cb 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -143,6 +143,33 @@ }, "weight_goal": { "name": "Weight goal" + }, + "activity_steps_today": { + "name": "Steps today" + }, + "activity_distance_today": { + "name": "Distance travelled today" + }, + "activity_floors_climbed_today": { + "name": "Floors climbed today" + }, + "activity_soft_duration_today": { + "name": "Soft activity today" + }, + "activity_moderate_duration_today": { + "name": "Moderate activity today" + }, + "activity_intense_duration_today": { + "name": "Intense activity today" + }, + "activity_active_duration_today": { + "name": "Active time today" + }, + "activity_active_calories_burnt_today": { + "name": "Active calories burnt today" + }, + "activity_total_calories_burnt_today": { + "name": "Total calories burnt today" } } } diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 0131feba9431e7..72664bcfe6a9e4 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from aiowithings import Device, Goals, MeasurementGroup, SleepSummary, WithingsClient -from aiowithings.models import NotificationConfiguration +from aiowithings.models import Activity, NotificationConfiguration import pytest from homeassistant.components.application_credentials import ( @@ -15,7 +15,11 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_json_object_fixture +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -148,6 +152,9 @@ def mock_withings(): for not_conf in notification_json["profiles"] ] + activity_json = load_json_array_fixture("withings/activity.json") + activities = [Activity.from_api(activity) for activity in activity_json] + goals_json = load_json_object_fixture("withings/goals.json") goals = Goals.from_api(goals_json) @@ -157,6 +164,8 @@ def mock_withings(): 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 + mock.get_activities_since.return_value = activities + mock.get_activities_in_period.return_value = activities mock.list_notification_configurations.return_value = notifications with patch( diff --git a/tests/components/withings/fixtures/activity.json b/tests/components/withings/fixtures/activity.json new file mode 100644 index 00000000000000..8ba9f526afa3e0 --- /dev/null +++ b/tests/components/withings/fixtures/activity.json @@ -0,0 +1,282 @@ +[ + { + "steps": 1892, + "distance": 1607.93, + "elevation": 0, + "soft": 4981, + "moderate": 158, + "intense": 0, + "active": 158, + "calories": 204.796, + "totalcalories": 2454.481, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-08", + "modified": 1697038118, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 2576, + "distance": 2349.617, + "elevation": 0, + "soft": 1255, + "moderate": 1211, + "intense": 0, + "active": 1211, + "calories": 134.967, + "totalcalories": 2351.652, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-09", + "modified": 1697038118, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 1827, + "distance": 1595.537, + "elevation": 0, + "soft": 2194, + "moderate": 569, + "intense": 0, + "active": 569, + "calories": 110.223, + "totalcalories": 2313.98, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-10", + "modified": 1697057517, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 3801, + "distance": 3307.985, + "elevation": 0, + "soft": 5146, + "moderate": 963, + "intense": 0, + "active": 963, + "calories": 240.89, + "totalcalories": 2385.746, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-11", + "modified": 1697842183, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 2501, + "distance": 2158.186, + "elevation": 0, + "soft": 1854, + "moderate": 998, + "intense": 0, + "active": 998, + "calories": 113.123, + "totalcalories": 2317.396, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-12", + "modified": 1697842183, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 6787, + "distance": 6008.779, + "elevation": 0, + "soft": 3773, + "moderate": 2831, + "intense": 36, + "active": 2867, + "calories": 263.371, + "totalcalories": 2380.669, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-13", + "modified": 1697842183, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 1232, + "distance": 1050.925, + "elevation": 0, + "soft": 2950, + "moderate": 196, + "intense": 0, + "active": 196, + "calories": 124.754, + "totalcalories": 2311.674, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-14", + "modified": 1697842183, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 851, + "distance": 723.139, + "elevation": 0, + "soft": 1634, + "moderate": 83, + "intense": 0, + "active": 83, + "calories": 68.121, + "totalcalories": 2294.325, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-15", + "modified": 1697842184, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 654, + "distance": 557.509, + "elevation": 0, + "soft": 1558, + "moderate": 124, + "intense": 0, + "active": 124, + "calories": 66.707, + "totalcalories": 2292.897, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-16", + "modified": 1697842184, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 566, + "distance": 482.185, + "elevation": 0, + "soft": 1085, + "moderate": 52, + "intense": 0, + "active": 52, + "calories": 45.126, + "totalcalories": 2287.08, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-17", + "modified": 1697842184, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 2204, + "distance": 1901.651, + "elevation": 0, + "soft": 1393, + "moderate": 941, + "intense": 0, + "active": 941, + "calories": 92.585, + "totalcalories": 2302.971, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-18", + "modified": 1697842185, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 95, + "distance": 80.63, + "elevation": 0, + "soft": 543, + "moderate": 0, + "intense": 0, + "active": 0, + "calories": 21.541, + "totalcalories": 2277.668, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-19", + "modified": 1697842185, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 1209, + "distance": 1028.559, + "elevation": 0, + "soft": 1864, + "moderate": 292, + "intense": 0, + "active": 292, + "calories": 85.497, + "totalcalories": 2303.788, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-20", + "modified": 1697884856, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + }, + { + "steps": 1155, + "distance": 1020.121, + "elevation": 0, + "soft": 1516, + "moderate": 1487, + "intense": 420, + "active": 1907, + "calories": 221.132, + "totalcalories": 2444.149, + "deviceid": null, + "hash_deviceid": null, + "timezone": "Europe/Amsterdam", + "date": "2023-10-21", + "modified": 1697888004, + "brand": 18, + "modelid": 1055, + "model": "GoogleFit tracker", + "is_tracker": false + } +] diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index 3546a24d2fe6e6..c4e472dd47285a 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -1,4 +1,33 @@ # serializer version: 1 +# name: test_all_entities[sensor.henk_active_calories_burnt_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Active calories burnt today', + 'state_class': , + 'unit_of_measurement': 'Calories', + }), + 'context': , + 'entity_id': 'sensor.henk_active_calories_burnt_today', + 'last_changed': , + 'last_updated': , + 'state': '221.132', + }) +# --- +# name: test_all_entities[sensor.henk_active_time_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Active time today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_active_time_today', + 'last_changed': , + 'last_updated': , + 'state': '1907', + }) +# --- # name: test_all_entities[sensor.henk_average_heart_rate] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -102,6 +131,22 @@ 'state': '70', }) # --- +# name: test_all_entities[sensor.henk_distance_travelled_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'henk Distance travelled today', + 'icon': 'mdi:map-marker-distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_distance_travelled_today', + 'last_changed': , + 'last_updated': , + 'state': '1020.121', + }) +# --- # name: test_all_entities[sensor.henk_extracellular_water] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -161,6 +206,21 @@ 'state': '0.07', }) # --- +# name: test_all_entities[sensor.henk_floors_climbed_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Floors climbed today', + 'icon': 'mdi:stairs-up', + 'state_class': , + 'unit_of_measurement': 'Floors', + }), + 'context': , + 'entity_id': 'sensor.henk_floors_climbed_today', + 'last_changed': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[sensor.henk_heart_pulse] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -207,6 +267,21 @@ 'state': '0.95', }) # --- +# name: test_all_entities[sensor.henk_intense_activity_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Intense activity today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_intense_activity_today', + 'last_changed': , + 'last_updated': , + 'state': '420', + }) +# --- # name: test_all_entities[sensor.henk_intracellular_water] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -296,6 +371,21 @@ 'state': '10', }) # --- +# name: test_all_entities[sensor.henk_moderate_activity_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Moderate activity today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_moderate_activity_today', + 'last_changed': , + 'last_updated': , + 'state': '1487', + }) +# --- # name: test_all_entities[sensor.henk_muscle_mass] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -414,6 +504,21 @@ 'state': '87', }) # --- +# name: test_all_entities[sensor.henk_soft_activity_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Soft activity today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_soft_activity_today', + 'last_changed': , + 'last_updated': , + 'state': '1516', + }) +# --- # name: test_all_entities[sensor.henk_spo2] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -443,6 +548,21 @@ 'state': '10000', }) # --- +# name: test_all_entities[sensor.henk_steps_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Steps today', + 'icon': 'mdi:shoe-print', + 'state_class': , + 'unit_of_measurement': 'Steps', + }), + 'context': , + 'entity_id': 'sensor.henk_steps_today', + 'last_changed': , + 'last_updated': , + 'state': '1155', + }) +# --- # name: test_all_entities[sensor.henk_systolic_blood_pressure] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -504,6 +624,20 @@ 'state': '996', }) # --- +# name: test_all_entities[sensor.henk_total_calories_burnt_today] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Total calories burnt today', + 'state_class': , + 'unit_of_measurement': 'Calories', + }), + 'context': , + 'entity_id': 'sensor.henk_total_calories_burnt_today', + 'last_changed': , + 'last_updated': , + 'state': '2444.149', + }) +# --- # name: test_all_entities[sensor.henk_vascular_age] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 6738d9a3eb47a9..c9756d6af3a235 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,12 +2,12 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from aiowithings import Goals, MeasurementGroup +from aiowithings import Activity, Goals, MeasurementGroup from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -21,6 +21,7 @@ ) +@pytest.mark.freeze_time("2023-10-21") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_all_entities( hass: HomeAssistant, @@ -163,3 +164,90 @@ async def test_update_new_goals_creates_new_sensor( await hass.async_block_till_done() assert hass.states.get("sensor.henk_step_goal") is not None + + +async def test_activity_sensors_unknown_next_day( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test activity sensors will return unknown the next day.""" + freezer.move_to("2023-10-21") + await setup_integration(hass, polling_config_entry, False) + + assert hass.states.get("sensor.henk_steps_today") is not None + + withings.get_activities_since.return_value = [] + + freezer.tick(timedelta(days=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_steps_today").state == STATE_UNKNOWN + + +async def test_activity_sensors_same_result_same_day( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test activity sensors will return the same result if old data is updated.""" + freezer.move_to("2023-10-21") + await setup_integration(hass, polling_config_entry, False) + + assert hass.states.get("sensor.henk_steps_today").state == "1155" + + withings.get_activities_since.return_value = [] + + freezer.tick(timedelta(hours=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_steps_today").state == "1155" + + +async def test_activity_sensors_created_when_existed( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test activity sensors will be added if they existed before.""" + freezer.move_to("2023-10-21") + await setup_integration(hass, polling_config_entry, False) + + assert hass.states.get("sensor.henk_steps_today") is not None + assert hass.states.get("sensor.henk_steps_today").state != STATE_UNKNOWN + + withings.get_activities_in_period.return_value = [] + + await hass.config_entries.async_reload(polling_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_steps_today").state == STATE_UNKNOWN + + +async def test_activity_sensors_created_when_receive_activity_data( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test activity sensors will be added if we receive activity data.""" + freezer.move_to("2023-10-21") + withings.get_activities_in_period.return_value = [] + await setup_integration(hass, polling_config_entry, False) + + assert hass.states.get("sensor.henk_steps_today") is None + + activity_json = load_json_array_fixture("withings/activity.json") + activities = [Activity.from_api(activity) for activity in activity_json] + withings.get_activities_in_period.return_value = activities + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_steps_today") is not None From 29758be17caf4d7be8dd8afc48b2a4ca11b0a8ba Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 16:30:15 +0200 Subject: [PATCH 3/9] Add activity sensors to Withings --- homeassistant/components/withings/sensor.py | 28 +++++++++++++------ .../withings/snapshots/test_sensor.ambr | 27 ++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 2f6496c10694a2..8a5068731c28a2 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -3,6 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from aiowithings import Activity, Goals, MeasurementType, SleepSummary @@ -26,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from .const import ( ACTIVITY_COORDINATOR, @@ -423,7 +425,7 @@ class WithingsActivitySensorEntityDescription( translation_key="activity_steps_today", icon="mdi:shoe-print", native_unit_of_measurement="Steps", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), WithingsActivitySensorEntityDescription( key="activity_distance_today", @@ -433,7 +435,7 @@ class WithingsActivitySensorEntityDescription( icon="mdi:map-marker-distance", native_unit_of_measurement=UnitOfLength.METERS, device_class=SensorDeviceClass.DISTANCE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), WithingsActivitySensorEntityDescription( key="activity_floors_climbed_today", @@ -441,7 +443,7 @@ class WithingsActivitySensorEntityDescription( translation_key="activity_floors_climbed_today", icon="mdi:stairs-up", native_unit_of_measurement="Floors", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), WithingsActivitySensorEntityDescription( key="activity_soft_duration_today", @@ -450,7 +452,8 @@ class WithingsActivitySensorEntityDescription( native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, ), WithingsActivitySensorEntityDescription( key="activity_moderate_duration_today", @@ -459,7 +462,8 @@ class WithingsActivitySensorEntityDescription( native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, ), WithingsActivitySensorEntityDescription( key="activity_intense_duration_today", @@ -468,7 +472,8 @@ class WithingsActivitySensorEntityDescription( native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, ), WithingsActivitySensorEntityDescription( key="activity_active_duration_today", @@ -477,7 +482,7 @@ class WithingsActivitySensorEntityDescription( native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, device_class=SensorDeviceClass.DURATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), WithingsActivitySensorEntityDescription( key="activity_active_calories_burnt_today", @@ -485,7 +490,7 @@ class WithingsActivitySensorEntityDescription( suggested_display_precision=1, translation_key="activity_active_calories_burnt_today", native_unit_of_measurement="Calories", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), WithingsActivitySensorEntityDescription( key="activity_total_calories_burnt_today", @@ -493,7 +498,7 @@ class WithingsActivitySensorEntityDescription( suggested_display_precision=1, translation_key="activity_total_calories_burnt_today", native_unit_of_measurement="Calories", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), ] @@ -732,3 +737,8 @@ def native_value(self) -> StateType: if not self.coordinator.data: return None return self.entity_description.value_fn(self.coordinator.data) + + @property + def last_reset(self) -> datetime: + """These values reset every day.""" + return dt_util.start_of_local_day() diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index c4e472dd47285a..cf8ff0a462bc0f 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -3,7 +3,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'henk Active calories burnt today', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': 'Calories', }), 'context': , @@ -18,7 +19,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'henk Active time today', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -137,7 +139,8 @@ 'device_class': 'distance', 'friendly_name': 'henk Distance travelled today', 'icon': 'mdi:map-marker-distance', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -211,7 +214,8 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'henk Floors climbed today', 'icon': 'mdi:stairs-up', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': 'Floors', }), 'context': , @@ -272,7 +276,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'henk Intense activity today', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -376,7 +381,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'henk Moderate activity today', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -509,7 +515,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'henk Soft activity today', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -553,7 +560,8 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'henk Steps today', 'icon': 'mdi:shoe-print', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': 'Steps', }), 'context': , @@ -628,7 +636,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'henk Total calories burnt today', - 'state_class': , + 'last_reset': '2023-10-20T00:00:00-07:00', + 'state_class': , 'unit_of_measurement': 'Calories', }), 'context': , From 9197d4609928dba7806f40bd1a8d1bf6853c366c Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 18:52:50 +0200 Subject: [PATCH 4/9] Don't create entities when we receive no new data --- homeassistant/components/withings/sensor.py | 21 ++++++++++++--------- tests/components/withings/test_sensor.py | 6 ++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 8a5068731c28a2..e46fcc131fa06a 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -629,18 +629,21 @@ def _async_goals_listener() -> None: activity_callback: Callable[[], None] | None = None + activity_entities_setup_before = ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_activity_steps_today" + ) + def _async_add_activity_entities() -> None: """Add activity entities.""" - async_add_entities( - WithingsActivitySensor(activity_coordinator, attribute) - for attribute in ACTIVITY_SENSORS - ) - if activity_callback: - activity_callback() + if activity_coordinator.data is not None or activity_entities_setup_before: + async_add_entities( + WithingsActivitySensor(activity_coordinator, attribute) + for attribute in ACTIVITY_SENSORS + ) + if activity_callback: + activity_callback() - if activity_coordinator.data is not None or ent_reg.async_get_entity_id( - Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_activity_steps_today" - ): + if activity_coordinator.data is not None or activity_entities_setup_before: _async_add_activity_entities() else: activity_callback = activity_coordinator.async_add_listener( diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index c9756d6af3a235..ba55e82df2ffdf 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -242,6 +242,12 @@ async def test_activity_sensors_created_when_receive_activity_data( assert hass.states.get("sensor.henk_steps_today") is None + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_steps_today") is None + activity_json = load_json_array_fixture("withings/activity.json") activities = [Activity.from_api(activity) for activity in activity_json] withings.get_activities_in_period.return_value = activities From f79718e1039cf8c62141a420203f8ab809f38d39 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 22 Oct 2023 21:51:14 +0200 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: J. Nick Koston --- homeassistant/components/withings/coordinator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index c6245380a3e350..7b6f2271baebef 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -214,11 +214,12 @@ async def _internal_update_data(self) -> Activity | None: self._last_valid_update ) + today = date.today() for activity in activities: - if activity.date == date.today(): + if activity.date == today: self._previous_data = activity self._last_valid_update = activity.modified return activity - if self._previous_data and self._previous_data.date == date.today(): + if self._previous_data and self._previous_data.date == today return self._previous_data return None From 89503e12389e95fc02502c2a190b464565193f91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Oct 2023 09:52:49 -1000 Subject: [PATCH 6/9] Update homeassistant/components/withings/coordinator.py --- homeassistant/components/withings/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 7b6f2271baebef..3b39dddb27edb3 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -220,6 +220,6 @@ async def _internal_update_data(self) -> Activity | None: self._previous_data = activity self._last_valid_update = activity.modified return activity - if self._previous_data and self._previous_data.date == today + if self._previous_data and self._previous_data.date == today: return self._previous_data return None From fd95ef2871ce7b3ed25c00b805d570388d3475da Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 22:02:52 +0200 Subject: [PATCH 7/9] Fix merge --- .../components/airzone_cloud/manifest.json | 2 +- .../components/devolo_home_network/entity.py | 1 + homeassistant/components/elgato/entity.py | 1 + .../components/esphome/manifest.json | 2 +- homeassistant/components/fibaro/manifest.json | 2 +- homeassistant/components/picnic/sensor.py | 2 - homeassistant/components/solaredge/sensor.py | 42 +-- .../components/solaredge/strings.json | 67 ++++ homeassistant/components/waqi/config_flow.py | 60 ++-- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- .../elgato/snapshots/test_button.ambr | 4 +- .../elgato/snapshots/test_light.ambr | 6 +- .../elgato/snapshots/test_sensor.ambr | 10 +- .../elgato/snapshots/test_switch.ambr | 4 +- tests/components/picnic/test_sensor.py | 188 +++++++---- tests/components/withings/__init__.py | 22 +- .../components/withings/fixtures/devices.json | 13 + .../withings/fixtures/get_device.json | 15 - .../withings/fixtures/get_meas.json | 313 ------------------ .../withings/fixtures/get_sleep.json | 201 ----------- .../withings/fixtures/measurements.json | 307 +++++++++++++++++ .../{get_meas_1.json => measurements_1.json} | 0 .../withings/fixtures/notifications.json | 20 ++ .../withings/fixtures/notify_list.json | 22 -- .../withings/fixtures/sleep_summaries.json | 197 +++++++++++ tests/components/withings/test_init.py | 4 - 27 files changed, 828 insertions(+), 689 deletions(-) create mode 100644 tests/components/withings/fixtures/devices.json delete mode 100644 tests/components/withings/fixtures/get_device.json delete mode 100644 tests/components/withings/fixtures/get_meas.json delete mode 100644 tests/components/withings/fixtures/get_sleep.json create mode 100644 tests/components/withings/fixtures/measurements.json rename tests/components/withings/fixtures/{get_meas_1.json => measurements_1.json} (100%) create mode 100644 tests/components/withings/fixtures/notifications.json delete mode 100644 tests/components/withings/fixtures/notify_list.json create mode 100644 tests/components/withings/fixtures/sleep_summaries.json diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index bc5bf1ee875e77..a3c0f5e7dc091f 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.2.8"] + "requirements": ["aioairzone-cloud==0.3.0"] } diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index 56a1043d126e27..ff0b2ba2c48473 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -53,6 +53,7 @@ def __init__( manufacturer="devolo", model=device.product, name=entry.title, + serial_number=device.serial_number, sw_version=device.firmware_version, ) self._attr_translation_key = self.entity_description.key diff --git a/homeassistant/components/elgato/entity.py b/homeassistant/components/elgato/entity.py index 4f4c2a9d8e9cee..1bbd32f5b441bf 100644 --- a/homeassistant/components/elgato/entity.py +++ b/homeassistant/components/elgato/entity.py @@ -23,6 +23,7 @@ def __init__(self, coordinator: ElgatoDataUpdateCoordinator) -> None: super().__init__(coordinator=coordinator) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.data.info.serial_number)}, + serial_number=coordinator.data.info.serial_number, manufacturer="Elgato", model=coordinator.data.info.product_name, name=coordinator.data.info.display_name, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index f59a09b3d6ba35..ae52af971eded1 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.0.9", + "aioesphomeapi==18.0.10", "bluetooth-data-tools==1.13.0", "esphome-dashboard-api==1.2.3" ], diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 36cd4c9153fc4b..68763228f82126 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.5"] + "requirements": ["pyfibaro==0.7.6"] } diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index fb4e756b1bef78..e7a69e0bf0294e 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -254,8 +254,6 @@ def __init__( super().__init__(coordinator) self.entity_description = description - self.entity_id = f"sensor.picnic_{description.key}" - self._attr_unique_id = f"{config_entry.unique_id}.{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index ca998493237238..5e298ae2a6f328 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -51,7 +51,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="lifetime_energy", json_key="lifeTimeData", - name="Lifetime energy", + translation_key="lifetime_energy", icon="mdi:solar-power", state_class=SensorStateClass.TOTAL, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -60,7 +60,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="energy_this_year", json_key="lastYearData", - name="Energy this year", + translation_key="energy_this_year", entity_registry_enabled_default=False, icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -69,7 +69,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="energy_this_month", json_key="lastMonthData", - name="Energy this month", + translation_key="energy_this_month", entity_registry_enabled_default=False, icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -78,7 +78,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="energy_today", json_key="lastDayData", - name="Energy today", + translation_key="energy_today", entity_registry_enabled_default=False, icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -87,7 +87,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="current_power", json_key="currentPower", - name="Current Power", + translation_key="current_power", icon="mdi:solar-power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, @@ -96,71 +96,71 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="site_details", json_key="status", - name="Site details", + translation_key="site_details", entity_registry_enabled_default=False, ), SolarEdgeSensorEntityDescription( key="meters", json_key="meters", - name="Meters", + translation_key="meters", entity_registry_enabled_default=False, ), SolarEdgeSensorEntityDescription( key="sensors", json_key="sensors", - name="Sensors", + translation_key="sensors", entity_registry_enabled_default=False, ), SolarEdgeSensorEntityDescription( key="gateways", json_key="gateways", - name="Gateways", + translation_key="gateways", entity_registry_enabled_default=False, ), SolarEdgeSensorEntityDescription( key="batteries", json_key="batteries", - name="Batteries", + translation_key="batteries", entity_registry_enabled_default=False, ), SolarEdgeSensorEntityDescription( key="inverters", json_key="inverters", - name="Inverters", + translation_key="inverters", entity_registry_enabled_default=False, ), SolarEdgeSensorEntityDescription( key="power_consumption", json_key="LOAD", - name="Power Consumption", + translation_key="power_consumption", entity_registry_enabled_default=False, icon="mdi:flash", ), SolarEdgeSensorEntityDescription( key="solar_power", json_key="PV", - name="Solar Power", + translation_key="solar_power", entity_registry_enabled_default=False, icon="mdi:solar-power", ), SolarEdgeSensorEntityDescription( key="grid_power", json_key="GRID", - name="Grid Power", + translation_key="grid_power", entity_registry_enabled_default=False, icon="mdi:power-plug", ), SolarEdgeSensorEntityDescription( key="storage_power", json_key="STORAGE", - name="Storage Power", + translation_key="storage_power", entity_registry_enabled_default=False, icon="mdi:car-battery", ), SolarEdgeSensorEntityDescription( key="purchased_energy", json_key="Purchased", - name="Imported Energy", + translation_key="purchased_energy", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -169,7 +169,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="production_energy", json_key="Production", - name="Production Energy", + translation_key="production_energy", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -178,7 +178,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="consumption_energy", json_key="Consumption", - name="Consumption Energy", + translation_key="consumption_energy", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -187,7 +187,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="selfconsumption_energy", json_key="SelfConsumption", - name="SelfConsumption Energy", + translation_key="selfconsumption_energy", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -196,7 +196,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="feedin_energy", json_key="FeedIn", - name="Exported Energy", + translation_key="feedin_energy", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -205,7 +205,7 @@ class SolarEdgeSensorEntityDescription( SolarEdgeSensorEntityDescription( key="storage_level", json_key="STORAGE", - name="Storage Level", + translation_key="storage_level", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json index b6f258b0dc884c..2b626987546572 100644 --- a/homeassistant/components/solaredge/strings.json +++ b/homeassistant/components/solaredge/strings.json @@ -19,5 +19,72 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "lifetime_energy": { + "name": "Lifetime energy" + }, + "energy_this_year": { + "name": "Energy this year" + }, + "energy_this_month": { + "name": "Energy this month" + }, + "energy_today": { + "name": "Energy today" + }, + "current_power": { + "name": "Current power" + }, + "site_details": { + "name": "Site details" + }, + "meters": { + "name": "Meters" + }, + "sensors": { + "name": "Sensors" + }, + "gateways": { + "name": "Gateways" + }, + "batteries": { + "name": "Batteries" + }, + "inverters": { + "name": "Inverters" + }, + "power_consumption": { + "name": "Power consumption" + }, + "solar_power": { + "name": "Solar power" + }, + "grid_power": { + "name": "Grid power" + }, + "storage_power": { + "name": "Stored power" + }, + "purchased_energy": { + "name": "Imported energy" + }, + "production_energy": { + "name": "Produced energy" + }, + "consumption_energy": { + "name": "Consumed energy" + }, + "selfconsumption_energy": { + "name": "Self-consumed energy" + }, + "feedin_energy": { + "name": "Exported energy" + }, + "storage_level": { + "name": "Storage level" + } + } } } diff --git a/homeassistant/components/waqi/config_flow.py b/homeassistant/components/waqi/config_flow.py index 8404b4256789ca..d23afdf33eebc8 100644 --- a/homeassistant/components/waqi/config_flow.py +++ b/homeassistant/components/waqi/config_flow.py @@ -1,7 +1,6 @@ """Config flow for World Air Quality Index (WAQI) integration.""" from __future__ import annotations -from collections.abc import Awaitable, Callable import logging from typing import Any @@ -90,13 +89,10 @@ async def async_step_user( errors=errors, ) - async def _async_base_step( - self, - step_id: str, - method: Callable[[WAQIClient, dict[str, Any]], Awaitable[WAQIAirQuality]], - data_schema: vol.Schema, - user_input: dict[str, Any] | None = None, + async def async_step_map( + self, user_input: dict[str, Any] | None = None ) -> FlowResult: + """Add measuring station via map.""" errors: dict[str, str] = {} if user_input is not None: async with WAQIClient( @@ -104,7 +100,10 @@ async def _async_base_step( ) as waqi_client: waqi_client.authenticate(self.data[CONF_API_KEY]) try: - measuring_station = await method(waqi_client, user_input) + measuring_station = await waqi_client.get_by_coordinates( + user_input[CONF_LOCATION][CONF_LATITUDE], + user_input[CONF_LOCATION][CONF_LONGITUDE], + ) except WAQIConnectionError: errors["base"] = "cannot_connect" except Exception as exc: # pylint: disable=broad-except @@ -113,19 +112,8 @@ async def _async_base_step( else: return await self._async_create_entry(measuring_station) return self.async_show_form( - step_id=step_id, data_schema=data_schema, errors=errors - ) - - async def async_step_map( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Add measuring station via map.""" - return await self._async_base_step( - CONF_MAP, - lambda waqi_client, data: waqi_client.get_by_coordinates( - data[CONF_LOCATION][CONF_LATITUDE], data[CONF_LOCATION][CONF_LONGITUDE] - ), - self.add_suggested_values_to_schema( + step_id=CONF_MAP, + data_schema=self.add_suggested_values_to_schema( vol.Schema( { vol.Required( @@ -140,26 +128,40 @@ async def async_step_map( } }, ), - user_input, + errors=errors, ) async def async_step_station_number( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Add measuring station via station number.""" - return await self._async_base_step( - CONF_STATION_NUMBER, - lambda waqi_client, data: waqi_client.get_by_station_number( - data[CONF_STATION_NUMBER] - ), - vol.Schema( + errors: dict[str, str] = {} + if user_input is not None: + async with WAQIClient( + session=async_get_clientsession(self.hass) + ) as waqi_client: + waqi_client.authenticate(self.data[CONF_API_KEY]) + try: + measuring_station = await waqi_client.get_by_station_number( + user_input[CONF_STATION_NUMBER] + ) + except WAQIConnectionError: + errors["base"] = "cannot_connect" + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception(exc) + errors["base"] = "unknown" + else: + return await self._async_create_entry(measuring_station) + return self.async_show_form( + step_id=CONF_STATION_NUMBER, + data_schema=vol.Schema( { vol.Required( CONF_STATION_NUMBER, ): int, } ), - user_input, + errors=errors, ) async def _async_create_entry( diff --git a/requirements_all.txt b/requirements_all.txt index d627e5ad004021..c784e9dad7c6a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.2.8 +aioairzone-cloud==0.3.0 # homeassistant.components.airzone aioairzone==0.6.9 @@ -237,7 +237,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.9 +aioesphomeapi==18.0.10 # homeassistant.components.flo aioflo==2021.11.0 @@ -1709,7 +1709,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.5 +pyfibaro==0.7.6 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27cc3d1a0e6223..3c9cc360ecdb61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.2.8 +aioairzone-cloud==0.3.0 # homeassistant.components.airzone aioairzone==0.6.9 @@ -218,7 +218,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.9 +aioesphomeapi==18.0.10 # homeassistant.components.flo aioflo==2021.11.0 @@ -1285,7 +1285,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.5 +pyfibaro==0.7.6 # homeassistant.components.fido pyfido==2.1.2 diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 134e213db6f789..e145c0b82bc3c2 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -69,7 +69,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, @@ -145,7 +145,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index f730015856de17..727170128d17a6 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -101,7 +101,7 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, @@ -211,7 +211,7 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, @@ -321,7 +321,7 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 3afcbc2e106ec9..0322993ef99a28 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -76,7 +76,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, @@ -162,7 +162,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, @@ -248,7 +248,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, @@ -331,7 +331,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, @@ -417,7 +417,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index ca34f8d0081417..d6b8896d5a272c 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -69,7 +69,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, @@ -145,7 +145,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 6d5a56499b96d2..cae10320fb9be4 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.components.picnic import const -from homeassistant.components.picnic.const import CONF_COUNTRY_CODE +from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, DOMAIN from homeassistant.components.picnic.sensor import SENSOR_TYPES from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( @@ -17,6 +17,7 @@ CURRENCY_EURO, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util @@ -168,8 +169,11 @@ async def _enable_all_sensors(self): """Enable all sensors of the Picnic integration.""" # Enable the sensors for sensor_type in SENSOR_KEYS: + entry = self.entity_registry.async_get_or_create( + Platform.SENSOR, DOMAIN, f"{self.config_entry.unique_id}.{sensor_type}" + ) updated_entry = self.entity_registry.async_update_entity( - f"sensor.picnic_{sensor_type}", disabled_by=None + entry.entity_id, disabled_by=None ) assert updated_entry.disabled is False await self.hass.async_block_till_done() @@ -197,76 +201,86 @@ async def test_sensor_setup_platform_not_available(self): # Assert that sensors are not set up assert ( - self.hass.states.get("sensor.picnic_selected_slot_max_order_time") is None + self.hass.states.get("sensor.mock_title_max_order_time_of_selected_slot") + is None + ) + assert self.hass.states.get("sensor.mock_title_status_of_last_order") is None + assert ( + self.hass.states.get("sensor.mock_title_total_price_of_last_order") is None ) - assert self.hass.states.get("sensor.picnic_last_order_status") is None - assert self.hass.states.get("sensor.picnic_last_order_total_price") is None async def test_sensors_setup(self): """Test the default sensor setup behaviour.""" await self._setup_platform(use_default_responses=True) - self._assert_sensor("sensor.picnic_cart_items_count", "10") + self._assert_sensor("sensor.mock_title_cart_items_count", "10") self._assert_sensor( - "sensor.picnic_cart_total_price", "25.35", unit=CURRENCY_EURO + "sensor.mock_title_cart_total_price", + "25.35", + unit=CURRENCY_EURO, ) self._assert_sensor( - "sensor.picnic_selected_slot_start", + "sensor.mock_title_start_of_selected_slot", "2021-03-03T13:45:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_selected_slot_end", + "sensor.mock_title_end_of_selected_slot", "2021-03-03T14:45:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_selected_slot_max_order_time", + "sensor.mock_title_max_order_time_of_selected_slot", "2021-03-02T21:00:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) - self._assert_sensor("sensor.picnic_selected_slot_min_order_value", "35.0") self._assert_sensor( - "sensor.picnic_last_order_slot_start", + "sensor.mock_title_minimum_order_value_for_selected_slot", + "35.0", + ) + self._assert_sensor( + "sensor.mock_title_start_of_last_order_s_slot", "2021-02-26T19:15:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_slot_end", + "sensor.mock_title_end_of_last_order_s_slot", "2021-02-26T20:15:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) - self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED") + self._assert_sensor("sensor.mock_title_status_of_last_order", "COMPLETED") self._assert_sensor( - "sensor.picnic_last_order_max_order_time", + "sensor.mock_title_max_order_time_of_last_order", "2021-02-25T21:00:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_delivery_time", + "sensor.mock_title_last_order_delivery_time", "2021-02-26T19:54:05+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_total_price", "41.33", unit=CURRENCY_EURO + "sensor.mock_title_total_price_of_last_order", + "41.33", + unit=CURRENCY_EURO, ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_start", + "sensor.mock_title_expected_start_of_next_delivery", "unknown", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_end", + "sensor.mock_title_expected_end_of_next_delivery", "unknown", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_next_delivery_slot_start", + "sensor.mock_title_start_of_next_delivery_s_slot", "unknown", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_next_delivery_slot_end", + "sensor.mock_title_end_of_next_delivery_s_slot", "unknown", cls=SensorDeviceClass.TIMESTAMP, ) @@ -275,13 +289,22 @@ async def test_sensors_setup_disabled_by_default(self): """Test that some sensors are disabled by default.""" await self._setup_platform(use_default_responses=True, enable_all_sensors=False) - self._assert_sensor("sensor.picnic_cart_items_count", disabled=True) - self._assert_sensor("sensor.picnic_last_order_slot_start", disabled=True) - self._assert_sensor("sensor.picnic_last_order_slot_end", disabled=True) - self._assert_sensor("sensor.picnic_last_order_status", disabled=True) - self._assert_sensor("sensor.picnic_last_order_total_price", disabled=True) - self._assert_sensor("sensor.picnic_next_delivery_slot_start", disabled=True) - self._assert_sensor("sensor.picnic_next_delivery_slot_end", disabled=True) + self._assert_sensor("sensor.mock_title_cart_items_count", disabled=True) + self._assert_sensor( + "sensor.mock_title_start_of_last_order_s_slot", disabled=True + ) + self._assert_sensor("sensor.mock_title_end_of_last_order_s_slot", disabled=True) + self._assert_sensor("sensor.mock_title_status_of_last_order", disabled=True) + self._assert_sensor( + "sensor.mock_title_total_price_of_last_order", disabled=True + ) + self._assert_sensor( + "sensor.mock_title_start_of_next_delivery_s_slot", + disabled=True, + ) + self._assert_sensor( + "sensor.mock_title_end_of_next_delivery_s_slot", disabled=True + ) async def test_sensors_no_selected_time_slot(self): """Test sensor states with no explicit selected time slot.""" @@ -299,11 +322,15 @@ async def test_sensors_no_selected_time_slot(self): await self._setup_platform() # Assert sensors are unknown - self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_selected_slot_max_order_time", STATE_UNKNOWN) + self._assert_sensor("sensor.mock_title_start_of_selected_slot", STATE_UNKNOWN) + self._assert_sensor("sensor.mock_title_end_of_selected_slot", STATE_UNKNOWN) self._assert_sensor( - "sensor.picnic_selected_slot_min_order_value", STATE_UNKNOWN + "sensor.mock_title_max_order_time_of_selected_slot", + STATE_UNKNOWN, + ) + self._assert_sensor( + "sensor.mock_title_minimum_order_value_for_selected_slot", + STATE_UNKNOWN, ) async def test_next_delivery_sensors(self): @@ -321,18 +348,22 @@ async def test_next_delivery_sensors(self): await self._setup_platform() # Assert delivery time is not available, but eta is - self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) + self._assert_sensor("sensor.mock_title_last_order_delivery_time", STATE_UNKNOWN) self._assert_sensor( - "sensor.picnic_next_delivery_eta_start", "2021-02-26T19:54:00+00:00" + "sensor.mock_title_expected_start_of_next_delivery", + "2021-02-26T19:54:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_end", "2021-02-26T20:14:00+00:00" + "sensor.mock_title_expected_end_of_next_delivery", + "2021-02-26T20:14:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_slot_start", "2021-02-26T19:15:00+00:00" + "sensor.mock_title_start_of_next_delivery_s_slot", + "2021-02-26T19:15:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_slot_end", "2021-02-26T20:15:00+00:00" + "sensor.mock_title_end_of_next_delivery_s_slot", + "2021-02-26T20:15:00+00:00", ) async def test_sensors_eta_date_malformed(self): @@ -352,8 +383,14 @@ async def test_sensors_eta_date_malformed(self): await self._coordinator.async_refresh() # Assert eta times are not available due to malformed date strings - self._assert_sensor("sensor.picnic_next_delivery_eta_start", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_next_delivery_eta_end", STATE_UNKNOWN) + self._assert_sensor( + "sensor.mock_title_expected_start_of_next_delivery", + STATE_UNKNOWN, + ) + self._assert_sensor( + "sensor.mock_title_expected_end_of_next_delivery", + STATE_UNKNOWN, + ) async def test_sensors_use_detailed_eta_if_available(self): """Test sensor states when last order is not yet delivered.""" @@ -378,10 +415,12 @@ async def test_sensors_use_detailed_eta_if_available(self): delivery_response["delivery_id"] ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_start", "2021-03-05T10:19:20+00:00" + "sensor.mock_title_expected_start_of_next_delivery", + "2021-03-05T10:19:20+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_end", "2021-03-05T10:39:20+00:00" + "sensor.mock_title_expected_end_of_next_delivery", + "2021-03-05T10:39:20+00:00", ) async def test_sensors_no_data(self): @@ -398,21 +437,35 @@ async def test_sensors_no_data(self): # Assert all default-enabled sensors have STATE_UNAVAILABLE because the last update failed assert self._coordinator.last_update_success is False - self._assert_sensor("sensor.picnic_cart_total_price", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE) + self._assert_sensor("sensor.mock_title_cart_total_price", STATE_UNAVAILABLE) + self._assert_sensor( + "sensor.mock_title_start_of_selected_slot", STATE_UNAVAILABLE + ) + self._assert_sensor("sensor.mock_title_end_of_selected_slot", STATE_UNAVAILABLE) + self._assert_sensor( + "sensor.mock_title_max_order_time_of_selected_slot", + STATE_UNAVAILABLE, + ) + self._assert_sensor( + "sensor.mock_title_minimum_order_value_for_selected_slot", + STATE_UNAVAILABLE, + ) self._assert_sensor( - "sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE + "sensor.mock_title_max_order_time_of_last_order", + STATE_UNAVAILABLE, ) self._assert_sensor( - "sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE + "sensor.mock_title_last_order_delivery_time", + STATE_UNAVAILABLE, ) self._assert_sensor( - "sensor.picnic_last_order_max_order_time", STATE_UNAVAILABLE + "sensor.mock_title_expected_start_of_next_delivery", + STATE_UNAVAILABLE, + ) + self._assert_sensor( + "sensor.mock_title_expected_end_of_next_delivery", + STATE_UNAVAILABLE, ) - self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_next_delivery_eta_start", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_next_delivery_eta_end", STATE_UNAVAILABLE) async def test_sensors_malformed_delivery_data(self): """Test sensor states when the delivery api returns not a list.""" @@ -425,10 +478,19 @@ async def test_sensors_malformed_delivery_data(self): # Assert all last-order sensors have STATE_UNAVAILABLE because the delivery info fetch failed assert self._coordinator.last_update_success is True - self._assert_sensor("sensor.picnic_last_order_max_order_time", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_next_delivery_eta_start", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_next_delivery_eta_end", STATE_UNKNOWN) + self._assert_sensor( + "sensor.mock_title_max_order_time_of_last_order", + STATE_UNKNOWN, + ) + self._assert_sensor("sensor.mock_title_last_order_delivery_time", STATE_UNKNOWN) + self._assert_sensor( + "sensor.mock_title_expected_start_of_next_delivery", + STATE_UNKNOWN, + ) + self._assert_sensor( + "sensor.mock_title_expected_end_of_next_delivery", + STATE_UNKNOWN, + ) async def test_sensors_malformed_response(self): """Test coordinator update fails when API yields ValueError.""" @@ -474,22 +536,28 @@ async def test_multiple_active_orders(self): await self._setup_platform() self._assert_sensor( - "sensor.picnic_last_order_slot_start", "2022-03-08T12:15:00+00:00" + "sensor.mock_title_start_of_last_order_s_slot", + "2022-03-08T12:15:00+00:00", ) self._assert_sensor( - "sensor.picnic_last_order_slot_end", "2022-03-08T13:15:00+00:00" + "sensor.mock_title_end_of_last_order_s_slot", + "2022-03-08T13:15:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_slot_start", "2022-03-01T08:15:00+00:00" + "sensor.mock_title_start_of_next_delivery_s_slot", + "2022-03-01T08:15:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_slot_end", "2022-03-01T09:15:00+00:00" + "sensor.mock_title_end_of_next_delivery_s_slot", + "2022-03-01T09:15:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_start", "2022-03-01T08:30:00+00:00" + "sensor.mock_title_expected_start_of_next_delivery", + "2022-03-01T08:30:00+00:00", ) self._assert_sensor( - "sensor.picnic_next_delivery_eta_end", "2022-03-01T08:45:00+00:00" + "sensor.mock_title_expected_end_of_next_delivery", + "2022-03-01T08:45:00+00:00", ) async def test_device_registry_entry(self): diff --git a/tests/components/withings/__init__.py b/tests/components/withings/__init__.py index 9693d21f162070..2425a5bd600ffc 100644 --- a/tests/components/withings/__init__.py +++ b/tests/components/withings/__init__.py @@ -5,13 +5,19 @@ from urllib.parse import urlparse from aiohttp.test_utils import TestClient +from aiowithings import Goals, MeasurementGroup from freezegun.api import FrozenDateTimeFactory from homeassistant.components.webhook import async_generate_url from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_array_fixture, + load_json_object_fixture, +) @dataclass @@ -64,3 +70,17 @@ async def prepare_webhook_setup( freezer.tick(timedelta(seconds=1)) async_fire_time_changed(hass) await hass.async_block_till_done() + + +def load_goals_fixture(fixture: str = "withings/goals.json") -> Goals: + """Return goals from fixture.""" + goals_json = load_json_object_fixture(fixture) + return Goals.from_api(goals_json) + + +def load_measurements_fixture( + fixture: str = "withings/measurements.json", +) -> list[MeasurementGroup]: + """Return measurement from fixture.""" + meas_json = load_json_array_fixture(fixture) + return [MeasurementGroup.from_api(measurement) for measurement in meas_json] diff --git a/tests/components/withings/fixtures/devices.json b/tests/components/withings/fixtures/devices.json new file mode 100644 index 00000000000000..9a2b7b81cf4d77 --- /dev/null +++ b/tests/components/withings/fixtures/devices.json @@ -0,0 +1,13 @@ +[ + { + "type": "Scale", + "battery": "high", + "model": "Body+", + "model_id": 5, + "timezone": "Europe/Amsterdam", + "first_session_date": null, + "last_session_date": 1693867179, + "deviceid": "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d", + "hash_deviceid": "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d" + } +] diff --git a/tests/components/withings/fixtures/get_device.json b/tests/components/withings/fixtures/get_device.json deleted file mode 100644 index 64bac3d4a190d5..00000000000000 --- a/tests/components/withings/fixtures/get_device.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "devices": [ - { - "type": "Scale", - "battery": "high", - "model": "Body+", - "model_id": 5, - "timezone": "Europe/Amsterdam", - "first_session_date": null, - "last_session_date": 1693867179, - "deviceid": "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d", - "hash_deviceid": "f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d" - } - ] -} diff --git a/tests/components/withings/fixtures/get_meas.json b/tests/components/withings/fixtures/get_meas.json deleted file mode 100644 index d473b61c274278..00000000000000 --- a/tests/components/withings/fixtures/get_meas.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "more": false, - "timezone": "UTC", - "updatetime": 1564617600, - "offset": 0, - "measuregrps": [ - { - "grpid": 1, - "attrib": 0, - "date": 1564660800, - "created": 1564660800, - "modified": 1564660800, - "category": 1, - "deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", - "hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", - "measures": [ - { - "type": 1, - "unit": 0, - "value": 70 - }, - { - "type": 8, - "unit": 0, - "value": 5 - }, - { - "type": 5, - "unit": 0, - "value": 60 - }, - { - "type": 76, - "unit": 0, - "value": 50 - }, - { - "type": 88, - "unit": 0, - "value": 10 - }, - { - "type": 4, - "unit": 0, - "value": 2 - }, - { - "type": 12, - "unit": 0, - "value": 40 - }, - { - "type": 71, - "unit": 0, - "value": 40 - }, - { - "type": 73, - "unit": 0, - "value": 20 - }, - { - "type": 6, - "unit": -3, - "value": 70 - }, - { - "type": 9, - "unit": 0, - "value": 70 - }, - { - "type": 10, - "unit": 0, - "value": 100 - }, - { - "type": 11, - "unit": 0, - "value": 60 - }, - { - "type": 54, - "unit": -2, - "value": 95 - }, - { - "type": 77, - "unit": -2, - "value": 95 - }, - { - "type": 91, - "unit": 0, - "value": 100 - }, - { - "type": 123, - "unit": 0, - "value": 100 - }, - { - "type": 155, - "unit": 0, - "value": 100 - }, - { - "type": 168, - "unit": 0, - "value": 100 - }, - { - "type": 169, - "unit": 0, - "value": 100 - } - ], - "modelid": 45, - "model": "BPM Connect", - "comment": null - }, - { - "grpid": 1, - "attrib": 0, - "date": 1564657200, - "created": 1564657200, - "modified": 1564657200, - "category": 1, - "deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", - "hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", - "measures": [ - { - "type": 1, - "unit": 0, - "value": 71 - }, - { - "type": 8, - "unit": 0, - "value": 51 - }, - { - "type": 5, - "unit": 0, - "value": 61 - }, - { - "type": 76, - "unit": 0, - "value": 51 - }, - { - "type": 88, - "unit": 0, - "value": 11 - }, - { - "type": 4, - "unit": 0, - "value": 21 - }, - { - "type": 12, - "unit": 0, - "value": 41 - }, - { - "type": 71, - "unit": 0, - "value": 41 - }, - { - "type": 73, - "unit": 0, - "value": 21 - }, - { - "type": 6, - "unit": -3, - "value": 71 - }, - { - "type": 9, - "unit": 0, - "value": 71 - }, - { - "type": 10, - "unit": 0, - "value": 101 - }, - { - "type": 11, - "unit": 0, - "value": 61 - }, - { - "type": 54, - "unit": -2, - "value": 96 - }, - { - "type": 77, - "unit": -2, - "value": 96 - }, - { - "type": 91, - "unit": 0, - "value": 101 - } - ], - "modelid": 45, - "model": "BPM Connect", - "comment": null - }, - { - "grpid": 1, - "attrib": 1, - "date": 1564664400, - "created": 1564664400, - "modified": 1564664400, - "category": 1, - "deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", - "hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", - "measures": [ - { - "type": 1, - "unit": 0, - "value": 71 - }, - { - "type": 8, - "unit": 0, - "value": 4 - }, - { - "type": 5, - "unit": 0, - "value": 40 - }, - { - "type": 76, - "unit": 0, - "value": 51 - }, - { - "type": 88, - "unit": 0, - "value": 11 - }, - { - "type": 4, - "unit": 0, - "value": 201 - }, - { - "type": 12, - "unit": 0, - "value": 41 - }, - { - "type": 71, - "unit": 0, - "value": 34 - }, - { - "type": 73, - "unit": 0, - "value": 21 - }, - { - "type": 6, - "unit": -3, - "value": 71 - }, - { - "type": 9, - "unit": 0, - "value": 71 - }, - { - "type": 10, - "unit": 0, - "value": 101 - }, - { - "type": 11, - "unit": 0, - "value": 61 - }, - { - "type": 54, - "unit": -2, - "value": 98 - }, - { - "type": 77, - "unit": -2, - "value": 96 - }, - { - "type": 91, - "unit": 0, - "value": 102 - } - ], - "modelid": 45, - "model": "BPM Connect", - "comment": null - } - ] -} diff --git a/tests/components/withings/fixtures/get_sleep.json b/tests/components/withings/fixtures/get_sleep.json deleted file mode 100644 index 29ed3df3fd3bc0..00000000000000 --- a/tests/components/withings/fixtures/get_sleep.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "more": false, - "offset": 0, - "series": [ - { - "id": 2081804182, - "timezone": "Europe/Paris", - "model": 32, - "model_id": 63, - "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", - "startdate": 1618691453, - "enddate": 1618713173, - "date": "2021-04-18", - "data": { - "wakeupduration": 3060, - "wakeupcount": 1, - "durationtosleep": 540, - "remsleepduration": 2400, - "durationtowakeup": 1140, - "total_sleep_time": 18660, - "sleep_efficiency": 0.86, - "sleep_latency": 540, - "wakeup_latency": 1140, - "waso": 1380, - "nb_rem_episodes": 1, - "out_of_bed_count": 0, - "lightsleepduration": 10440, - "deepsleepduration": 5820, - "hr_average": 103, - "hr_min": 70, - "hr_max": 120, - "rr_average": 14, - "rr_min": 10, - "rr_max": 20, - "breathing_disturbances_intensity": 9, - "snoring": 1080, - "snoringepisodecount": 18, - "sleep_score": 37, - "apnea_hypopnea_index": 9 - }, - "created": 1620237476, - "modified": 1620237476 - }, - { - "id": 2081804265, - "timezone": "Europe/Paris", - "model": 32, - "model_id": 63, - "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", - "startdate": 1618605055, - "enddate": 1618636975, - "date": "2021-04-17", - "data": { - "wakeupduration": 2520, - "wakeupcount": 3, - "durationtosleep": 900, - "remsleepduration": 6840, - "durationtowakeup": 420, - "total_sleep_time": 26880, - "sleep_efficiency": 0.91, - "sleep_latency": 900, - "wakeup_latency": 420, - "waso": 1200, - "nb_rem_episodes": 2, - "out_of_bed_count": 0, - "lightsleepduration": 12840, - "deepsleepduration": 7200, - "hr_average": 85, - "hr_min": 50, - "hr_max": 120, - "rr_average": 16, - "rr_min": 10, - "rr_max": 20, - "breathing_disturbances_intensity": 14, - "snoring": 1140, - "snoringepisodecount": 19, - "sleep_score": 90, - "apnea_hypopnea_index": 14 - }, - "created": 1620237480, - "modified": 1620237479 - }, - { - "id": 2081804358, - "timezone": "Europe/Paris", - "model": 32, - "model_id": 63, - "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", - "startdate": 1618518658, - "enddate": 1618548058, - "date": "2021-04-16", - "data": { - "wakeupduration": 4080, - "wakeupcount": 1, - "durationtosleep": 840, - "remsleepduration": 2040, - "durationtowakeup": 1560, - "total_sleep_time": 16860, - "sleep_efficiency": 0.81, - "sleep_latency": 840, - "wakeup_latency": 1560, - "waso": 1680, - "nb_rem_episodes": 2, - "out_of_bed_count": 0, - "lightsleepduration": 11100, - "deepsleepduration": 3720, - "hr_average": 65, - "hr_min": 50, - "hr_max": 91, - "rr_average": 14, - "rr_min": 10, - "rr_max": 20, - "breathing_disturbances_intensity": -1, - "snoring": 1020, - "snoringepisodecount": 17, - "sleep_score": 20, - "apnea_hypopnea_index": -1 - }, - "created": 1620237484, - "modified": 1620237484 - }, - { - "id": 2081804405, - "timezone": "Europe/Paris", - "model": 32, - "model_id": 63, - "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", - "startdate": 1618432203, - "enddate": 1618453143, - "date": "2021-04-15", - "data": { - "wakeupduration": 4080, - "wakeupcount": 1, - "durationtosleep": 840, - "remsleepduration": 2040, - "durationtowakeup": 1560, - "total_sleep_time": 16860, - "sleep_efficiency": 0.81, - "sleep_latency": 840, - "wakeup_latency": 1560, - "waso": 1680, - "nb_rem_episodes": 2, - "out_of_bed_count": 0, - "lightsleepduration": 11100, - "deepsleepduration": 3720, - "hr_average": 65, - "hr_min": 50, - "hr_max": 91, - "rr_average": 14, - "rr_min": 10, - "rr_max": 20, - "breathing_disturbances_intensity": -1, - "snoring": 1020, - "snoringepisodecount": 17, - "sleep_score": 20, - "apnea_hypopnea_index": -1 - }, - "created": 1620237486, - "modified": 1620237486 - }, - { - "id": 2081804490, - "timezone": "Europe/Paris", - "model": 32, - "model_id": 63, - "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", - "startdate": 1618345805, - "enddate": 1618373504, - "date": "2021-04-14", - "data": { - "wakeupduration": 3600, - "wakeupcount": 2, - "durationtosleep": 780, - "remsleepduration": 3960, - "durationtowakeup": 300, - "total_sleep_time": 22680, - "sleep_efficiency": 0.86, - "sleep_latency": 780, - "wakeup_latency": 300, - "waso": 3939, - "nb_rem_episodes": 4, - "out_of_bed_count": 3, - "lightsleepduration": 12960, - "deepsleepduration": 5760, - "hr_average": 98, - "hr_min": 70, - "hr_max": 120, - "rr_average": 13, - "rr_min": 10, - "rr_max": 20, - "breathing_disturbances_intensity": 29, - "snoring": 960, - "snoringepisodecount": 16, - "sleep_score": 62, - "apnea_hypopnea_index": 29 - }, - "created": 1620237490, - "modified": 1620237489 - } - ] -} diff --git a/tests/components/withings/fixtures/measurements.json b/tests/components/withings/fixtures/measurements.json new file mode 100644 index 00000000000000..3ed59a7c3f4e74 --- /dev/null +++ b/tests/components/withings/fixtures/measurements.json @@ -0,0 +1,307 @@ +[ + { + "grpid": 1, + "attrib": 0, + "date": 1564660800, + "created": 1564660800, + "modified": 1564660800, + "category": 1, + "deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", + "hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", + "measures": [ + { + "type": 1, + "unit": 0, + "value": 70 + }, + { + "type": 8, + "unit": 0, + "value": 5 + }, + { + "type": 5, + "unit": 0, + "value": 60 + }, + { + "type": 76, + "unit": 0, + "value": 50 + }, + { + "type": 88, + "unit": 0, + "value": 10 + }, + { + "type": 4, + "unit": 0, + "value": 2 + }, + { + "type": 12, + "unit": 0, + "value": 40 + }, + { + "type": 71, + "unit": 0, + "value": 40 + }, + { + "type": 73, + "unit": 0, + "value": 20 + }, + { + "type": 6, + "unit": -3, + "value": 70 + }, + { + "type": 9, + "unit": 0, + "value": 70 + }, + { + "type": 10, + "unit": 0, + "value": 100 + }, + { + "type": 11, + "unit": 0, + "value": 60 + }, + { + "type": 54, + "unit": -2, + "value": 95 + }, + { + "type": 77, + "unit": -2, + "value": 95 + }, + { + "type": 91, + "unit": 0, + "value": 100 + }, + { + "type": 123, + "unit": 0, + "value": 100 + }, + { + "type": 155, + "unit": 0, + "value": 100 + }, + { + "type": 168, + "unit": 0, + "value": 100 + }, + { + "type": 169, + "unit": 0, + "value": 100 + } + ], + "modelid": 45, + "model": "BPM Connect", + "comment": null + }, + { + "grpid": 1, + "attrib": 0, + "date": 1564657200, + "created": 1564657200, + "modified": 1564657200, + "category": 1, + "deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", + "hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", + "measures": [ + { + "type": 1, + "unit": 0, + "value": 71 + }, + { + "type": 8, + "unit": 0, + "value": 51 + }, + { + "type": 5, + "unit": 0, + "value": 61 + }, + { + "type": 76, + "unit": 0, + "value": 51 + }, + { + "type": 88, + "unit": 0, + "value": 11 + }, + { + "type": 4, + "unit": 0, + "value": 21 + }, + { + "type": 12, + "unit": 0, + "value": 41 + }, + { + "type": 71, + "unit": 0, + "value": 41 + }, + { + "type": 73, + "unit": 0, + "value": 21 + }, + { + "type": 6, + "unit": -3, + "value": 71 + }, + { + "type": 9, + "unit": 0, + "value": 71 + }, + { + "type": 10, + "unit": 0, + "value": 101 + }, + { + "type": 11, + "unit": 0, + "value": 61 + }, + { + "type": 54, + "unit": -2, + "value": 96 + }, + { + "type": 77, + "unit": -2, + "value": 96 + }, + { + "type": 91, + "unit": 0, + "value": 101 + } + ], + "modelid": 45, + "model": "BPM Connect", + "comment": null + }, + { + "grpid": 1, + "attrib": 1, + "date": 1564664400, + "created": 1564664400, + "modified": 1564664400, + "category": 1, + "deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", + "hash_deviceid": "91a7e556c2022ef54dca6e07a853c3193734d148", + "measures": [ + { + "type": 1, + "unit": 0, + "value": 71 + }, + { + "type": 8, + "unit": 0, + "value": 4 + }, + { + "type": 5, + "unit": 0, + "value": 40 + }, + { + "type": 76, + "unit": 0, + "value": 51 + }, + { + "type": 88, + "unit": 0, + "value": 11 + }, + { + "type": 4, + "unit": 0, + "value": 201 + }, + { + "type": 12, + "unit": 0, + "value": 41 + }, + { + "type": 71, + "unit": 0, + "value": 34 + }, + { + "type": 73, + "unit": 0, + "value": 21 + }, + { + "type": 6, + "unit": -3, + "value": 71 + }, + { + "type": 9, + "unit": 0, + "value": 71 + }, + { + "type": 10, + "unit": 0, + "value": 101 + }, + { + "type": 11, + "unit": 0, + "value": 61 + }, + { + "type": 54, + "unit": -2, + "value": 98 + }, + { + "type": 77, + "unit": -2, + "value": 96 + }, + { + "type": 91, + "unit": 0, + "value": 102 + } + ], + "modelid": 45, + "model": "BPM Connect", + "comment": null + } +] diff --git a/tests/components/withings/fixtures/get_meas_1.json b/tests/components/withings/fixtures/measurements_1.json similarity index 100% rename from tests/components/withings/fixtures/get_meas_1.json rename to tests/components/withings/fixtures/measurements_1.json diff --git a/tests/components/withings/fixtures/notifications.json b/tests/components/withings/fixtures/notifications.json new file mode 100644 index 00000000000000..8f4d49fde498c5 --- /dev/null +++ b/tests/components/withings/fixtures/notifications.json @@ -0,0 +1,20 @@ +[ + { + "appli": 50, + "callbackurl": "https://not.my.callback/url", + "expires": 2147483647, + "comment": null + }, + { + "appli": 50, + "callbackurl": "https://example.com/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e", + "expires": 2147483647, + "comment": null + }, + { + "appli": 51, + "callbackurl": "https://example.com/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e", + "expires": 2147483647, + "comment": null + } +] diff --git a/tests/components/withings/fixtures/notify_list.json b/tests/components/withings/fixtures/notify_list.json deleted file mode 100644 index ef7a99857e4d11..00000000000000 --- a/tests/components/withings/fixtures/notify_list.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "profiles": [ - { - "appli": 50, - "callbackurl": "https://not.my.callback/url", - "expires": 2147483647, - "comment": null - }, - { - "appli": 50, - "callbackurl": "https://example.com/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e", - "expires": 2147483647, - "comment": null - }, - { - "appli": 51, - "callbackurl": "https://example.com/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e", - "expires": 2147483647, - "comment": null - } - ] -} diff --git a/tests/components/withings/fixtures/sleep_summaries.json b/tests/components/withings/fixtures/sleep_summaries.json new file mode 100644 index 00000000000000..1bcfcfcc1d2e69 --- /dev/null +++ b/tests/components/withings/fixtures/sleep_summaries.json @@ -0,0 +1,197 @@ +[ + { + "id": 2081804182, + "timezone": "Europe/Paris", + "model": 32, + "model_id": 63, + "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", + "startdate": 1618691453, + "enddate": 1618713173, + "date": "2021-04-18", + "data": { + "wakeupduration": 3060, + "wakeupcount": 1, + "durationtosleep": 540, + "remsleepduration": 2400, + "durationtowakeup": 1140, + "total_sleep_time": 18660, + "sleep_efficiency": 0.86, + "sleep_latency": 540, + "wakeup_latency": 1140, + "waso": 1380, + "nb_rem_episodes": 1, + "out_of_bed_count": 0, + "lightsleepduration": 10440, + "deepsleepduration": 5820, + "hr_average": 103, + "hr_min": 70, + "hr_max": 120, + "rr_average": 14, + "rr_min": 10, + "rr_max": 20, + "breathing_disturbances_intensity": 9, + "snoring": 1080, + "snoringepisodecount": 18, + "sleep_score": 37, + "apnea_hypopnea_index": 9 + }, + "created": 1620237476, + "modified": 1620237476 + }, + { + "id": 2081804265, + "timezone": "Europe/Paris", + "model": 32, + "model_id": 63, + "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", + "startdate": 1618605055, + "enddate": 1618636975, + "date": "2021-04-17", + "data": { + "wakeupduration": 2520, + "wakeupcount": 3, + "durationtosleep": 900, + "remsleepduration": 6840, + "durationtowakeup": 420, + "total_sleep_time": 26880, + "sleep_efficiency": 0.91, + "sleep_latency": 900, + "wakeup_latency": 420, + "waso": 1200, + "nb_rem_episodes": 2, + "out_of_bed_count": 0, + "lightsleepduration": 12840, + "deepsleepduration": 7200, + "hr_average": 85, + "hr_min": 50, + "hr_max": 120, + "rr_average": 16, + "rr_min": 10, + "rr_max": 20, + "breathing_disturbances_intensity": 14, + "snoring": 1140, + "snoringepisodecount": 19, + "sleep_score": 90, + "apnea_hypopnea_index": 14 + }, + "created": 1620237480, + "modified": 1620237479 + }, + { + "id": 2081804358, + "timezone": "Europe/Paris", + "model": 32, + "model_id": 63, + "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", + "startdate": 1618518658, + "enddate": 1618548058, + "date": "2021-04-16", + "data": { + "wakeupduration": 4080, + "wakeupcount": 1, + "durationtosleep": 840, + "remsleepduration": 2040, + "durationtowakeup": 1560, + "total_sleep_time": 16860, + "sleep_efficiency": 0.81, + "sleep_latency": 840, + "wakeup_latency": 1560, + "waso": 1680, + "nb_rem_episodes": 2, + "out_of_bed_count": 0, + "lightsleepduration": 11100, + "deepsleepduration": 3720, + "hr_average": 65, + "hr_min": 50, + "hr_max": 91, + "rr_average": 14, + "rr_min": 10, + "rr_max": 20, + "breathing_disturbances_intensity": -1, + "snoring": 1020, + "snoringepisodecount": 17, + "sleep_score": 20, + "apnea_hypopnea_index": -1 + }, + "created": 1620237484, + "modified": 1620237484 + }, + { + "id": 2081804405, + "timezone": "Europe/Paris", + "model": 32, + "model_id": 63, + "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", + "startdate": 1618432203, + "enddate": 1618453143, + "date": "2021-04-15", + "data": { + "wakeupduration": 4080, + "wakeupcount": 1, + "durationtosleep": 840, + "remsleepduration": 2040, + "durationtowakeup": 1560, + "total_sleep_time": 16860, + "sleep_efficiency": 0.81, + "sleep_latency": 840, + "wakeup_latency": 1560, + "waso": 1680, + "nb_rem_episodes": 2, + "out_of_bed_count": 0, + "lightsleepduration": 11100, + "deepsleepduration": 3720, + "hr_average": 65, + "hr_min": 50, + "hr_max": 91, + "rr_average": 14, + "rr_min": 10, + "rr_max": 20, + "breathing_disturbances_intensity": -1, + "snoring": 1020, + "snoringepisodecount": 17, + "sleep_score": 20, + "apnea_hypopnea_index": -1 + }, + "created": 1620237486, + "modified": 1620237486 + }, + { + "id": 2081804490, + "timezone": "Europe/Paris", + "model": 32, + "model_id": 63, + "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", + "startdate": 1618345805, + "enddate": 1618373504, + "date": "2021-04-14", + "data": { + "wakeupduration": 3600, + "wakeupcount": 2, + "durationtosleep": 780, + "remsleepduration": 3960, + "durationtowakeup": 300, + "total_sleep_time": 22680, + "sleep_efficiency": 0.86, + "sleep_latency": 780, + "wakeup_latency": 300, + "waso": 3939, + "nb_rem_episodes": 4, + "out_of_bed_count": 3, + "lightsleepduration": 12960, + "deepsleepduration": 5760, + "hr_average": 98, + "hr_min": 70, + "hr_max": 120, + "rr_average": 13, + "rr_min": 10, + "rr_max": 20, + "breathing_disturbances_intensity": 29, + "snoring": 960, + "snoringepisodecount": 16, + "sleep_score": 62, + "apnea_hypopnea_index": 29 + }, + "created": 1620237490, + "modified": 1620237489 + } +] diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index baec7e92ea07f4..3f20791ac4d828 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -120,11 +120,9 @@ async def test_data_manager_webhook_subscription( hass: HomeAssistant, withings: AsyncMock, webhook_config_entry: MockConfigEntry, - hass_client_no_auth: ClientSessionGenerator, ) -> None: """Test data manager webhook subscriptions.""" await setup_integration(hass, webhook_config_entry) - await hass_client_no_auth() await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() @@ -158,12 +156,10 @@ async def test_webhook_subscription_polling_config( hass: HomeAssistant, withings: AsyncMock, polling_config_entry: MockConfigEntry, - hass_client_no_auth: ClientSessionGenerator, freezer: FrozenDateTimeFactory, ) -> None: """Test webhook subscriptions not run when polling.""" await setup_integration(hass, polling_config_entry, False) - await hass_client_no_auth() await hass.async_block_till_done() freezer.tick(timedelta(seconds=1)) async_fire_time_changed(hass) From f014dd9b32a6c32f7b0f3d032257ea591f0778b6 Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 22:06:31 +0200 Subject: [PATCH 8/9] Fix merge --- tests/components/withings/__init__.py | 10 +++++++++- tests/components/withings/conftest.py | 15 +++++++++------ tests/components/withings/test_sensor.py | 24 ++++++++++-------------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/tests/components/withings/__init__.py b/tests/components/withings/__init__.py index 2425a5bd600ffc..56bee0c30db2a8 100644 --- a/tests/components/withings/__init__.py +++ b/tests/components/withings/__init__.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse from aiohttp.test_utils import TestClient -from aiowithings import Goals, MeasurementGroup +from aiowithings import Activity, Goals, MeasurementGroup from freezegun.api import FrozenDateTimeFactory from homeassistant.components.webhook import async_generate_url @@ -84,3 +84,11 @@ def load_measurements_fixture( """Return measurement from fixture.""" meas_json = load_json_array_fixture(fixture) return [MeasurementGroup.from_api(measurement) for measurement in meas_json] + + +def load_activity_fixture( + fixture: str = "withings/activity.json", +) -> list[Activity]: + """Return measurement from fixture.""" + activity_json = load_json_array_fixture(fixture) + return [Activity.from_api(activity) for activity in activity_json] diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 3898bce87dcd65..066a9eed031fa1 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from aiowithings import Device, SleepSummary, WithingsClient -from aiowithings.models import Activity, NotificationConfiguration +from aiowithings.models import NotificationConfiguration import pytest from homeassistant.components.application_credentials import ( @@ -16,7 +16,11 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_json_array_fixture -from tests.components.withings import load_goals_fixture, load_measurements_fixture +from tests.components.withings import ( + load_activity_fixture, + load_goals_fixture, + load_measurements_fixture, +) CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -132,7 +136,7 @@ def mock_withings(): devices_json = load_json_array_fixture("withings/devices.json") devices = [Device.from_api(device) for device in devices_json] - measurement_groups = load_measurements_fixture("withings/measurements.json") + measurement_groups = load_measurements_fixture() sleep_json = load_json_array_fixture("withings/sleep_summaries.json") sleep_summaries = [ @@ -144,12 +148,11 @@ def mock_withings(): NotificationConfiguration.from_api(not_conf) for not_conf in notification_json ] - activity_json = load_json_array_fixture("withings/activity.json") - activities = [Activity.from_api(activity) for activity in activity_json] + activities = load_activity_fixture() mock = AsyncMock(spec=WithingsClient) mock.get_devices.return_value = devices - mock.get_goals.return_value = load_goals_fixture("withings/goals.json") + mock.get_goals.return_value = load_goals_fixture() 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 diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 3a26faa5d95631..1acfc324d8127a 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from aiowithings import Activity from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -11,14 +10,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import load_goals_fixture, load_measurements_fixture, setup_integration - -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - load_json_array_fixture, +from . import ( + load_activity_fixture, + load_goals_fixture, + load_measurements_fixture, + setup_integration, ) +from tests.common import MockConfigEntry, async_fire_time_changed + @pytest.mark.freeze_time("2023-10-21") @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -121,9 +121,7 @@ async def test_update_new_measurement_creates_new_sensor( assert hass.states.get("sensor.henk_fat_mass") is None - withings.get_measurement_in_period.return_value = load_measurements_fixture( - "withings/measurements.json" - ) + withings.get_measurement_in_period.return_value = load_measurements_fixture() freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) @@ -147,7 +145,7 @@ async def test_update_new_goals_creates_new_sensor( assert hass.states.get("sensor.henk_step_goal") is None assert hass.states.get("sensor.henk_weight_goal") is not None - withings.get_goals.return_value = load_goals_fixture("withings/goals.json") + withings.get_goals.return_value = load_goals_fixture() freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) @@ -238,9 +236,7 @@ async def test_activity_sensors_created_when_receive_activity_data( assert hass.states.get("sensor.henk_steps_today") is None - activity_json = load_json_array_fixture("withings/activity.json") - activities = [Activity.from_api(activity) for activity in activity_json] - withings.get_activities_in_period.return_value = activities + withings.get_activities_in_period.return_value = load_activity_fixture() freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) From 0dfe525c81117e55e03c94bf38b3d6f959f8f4c2 Mon Sep 17 00:00:00 2001 From: joostlek Date: Sun, 22 Oct 2023 22:46:30 +0200 Subject: [PATCH 9/9] Fix merge --- .coveragerc | 1 - .github/workflows/wheels.yml | 2 + homeassistant/components/fibaro/__init__.py | 4 +- .../components/nibe_heatpump/number.py | 2 + .../components/opensky/manifest.json | 2 +- .../components/withings/binary_sensor.py | 6 +- .../components/withings/diagnostics.py | 21 +-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- tests/components/nibe_heatpump/__init__.py | 17 +++ tests/components/nibe_heatpump/conftest.py | 13 +- .../nibe_heatpump/snapshots/test_number.ambr | 135 ++++++++++++++++++ tests/components/nibe_heatpump/test_button.py | 14 +- tests/components/nibe_heatpump/test_number.py | 109 ++++++++++++++ tests/components/opensky/__init__.py | 7 +- tests/components/opensky/conftest.py | 9 +- tests/components/opensky/test_sensor.py | 12 +- 19 files changed, 305 insertions(+), 57 deletions(-) create mode 100644 tests/components/nibe_heatpump/snapshots/test_number.ambr create mode 100644 tests/components/nibe_heatpump/test_number.py diff --git a/.coveragerc b/.coveragerc index b994e61a1226b2..9634ca2edb8404 100644 --- a/.coveragerc +++ b/.coveragerc @@ -827,7 +827,6 @@ omit = homeassistant/components/nibe_heatpump/__init__.py homeassistant/components/nibe_heatpump/climate.py homeassistant/components/nibe_heatpump/binary_sensor.py - homeassistant/components/nibe_heatpump/number.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py homeassistant/components/nibe_heatpump/switch.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a51502cd888cde..3b23f1b5b0570c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -176,9 +176,11 @@ jobs: # and don't yet use isolated build environments. # Build these first. # grpcio: https://github.com/grpc/grpc/issues/33918 + # pydantic: https://github.com/pydantic/pydantic/issues/7689 touch requirements_old-cython.txt cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt + cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt - name: Adjust build env run: | diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 55b41372faaff9..cdfa7f6a8649b4 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -101,6 +101,7 @@ def __init__(self, config: Mapping[str, Any]) -> None: self._event_callbacks: dict[int, list[Callable[[FibaroEvent], None]]] = {} self.hub_serial: str # Unique serial number of the hub self.hub_name: str # The friendly name of the hub + self.hub_model: str self.hub_software_version: str self.hub_api_url: str = config[CONF_URL] # Device infos by fibaro device id @@ -113,6 +114,7 @@ def connect(self) -> bool: info = self._client.read_info() self.hub_serial = info.serial_number self.hub_name = info.hc_name + self.hub_model = info.platform self.hub_software_version = info.current_version if connected is False: @@ -409,7 +411,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: serial_number=controller.hub_serial, manufacturer="Fibaro", name=controller.hub_name, - model=controller.hub_serial, + model=controller.hub_model, sw_version=controller.hub_software_version, configuration_url=controller.hub_api_url.removesuffix("/api/"), ) diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 1b3bc928985505..8231cc65450242 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -50,6 +50,8 @@ def __init__(self, coordinator: Coordinator, coil: Coil) -> None: self._attr_native_min_value, self._attr_native_max_value, ) = _get_numeric_limits(coil.size) + self._attr_native_min_value /= coil.factor + self._attr_native_max_value /= coil.factor else: self._attr_native_min_value = float(coil.min) self._attr_native_max_value = float(coil.max) diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 4d1047222ff81d..d33dfec6adfea0 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", - "requirements": ["python-opensky==0.2.0"] + "requirements": ["python-opensky==0.2.1"] } diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index 24698f90809f64..69af68e988b7ea 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import BED_PRESENCE_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import WithingsBedPresenceDataUpdateCoordinator from .entity import WithingsEntity @@ -20,9 +20,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" - coordinator: WithingsBedPresenceDataUpdateCoordinator = hass.data[DOMAIN][ - entry.entry_id - ][BED_PRESENCE_COORDINATOR] + coordinator = hass.data[DOMAIN][entry.entry_id].bed_presence_coordinator entities = [WithingsBinarySensor(coordinator)] diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index efa0421f205b72..7ed9f6ce2c9ce2 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -10,12 +10,8 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant -from . import ( - CONF_CLOUDHOOK_URL, - WithingsMeasurementDataUpdateCoordinator, - WithingsSleepDataUpdateCoordinator, -) -from .const import DOMAIN, MEASUREMENT_COORDINATOR, SLEEP_COORDINATOR +from . import CONF_CLOUDHOOK_URL, WithingsData +from .const import DOMAIN async def async_get_config_entry_diagnostics( @@ -29,17 +25,12 @@ async def async_get_config_entry_diagnostics( has_cloudhooks = CONF_CLOUDHOOK_URL in entry.data - measurement_coordinator: WithingsMeasurementDataUpdateCoordinator = hass.data[ - DOMAIN - ][entry.entry_id][MEASUREMENT_COORDINATOR] - sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][ - entry.entry_id - ][SLEEP_COORDINATOR] + withings_data: WithingsData = hass.data[DOMAIN][entry.entry_id] return { "has_valid_external_webhook_url": has_valid_external_webhook_url, "has_cloudhooks": has_cloudhooks, - "webhooks_connected": measurement_coordinator.webhooks_connected, - "received_measurements": list(measurement_coordinator.data), - "received_sleep_data": sleep_coordinator.data is not None, + "webhooks_connected": withings_data.measurement_coordinator.webhooks_connected, + "received_measurements": list(withings_data.measurement_coordinator.data), + "received_sleep_data": withings_data.sleep_coordinator.data is not None, } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc6be3705e1234..ac3245c2ff1ad7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -103,7 +103,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.7.1 +anyio==4.0.0 h11==0.14.0 httpcore==0.18.0 diff --git a/requirements_all.txt b/requirements_all.txt index c784e9dad7c6a1..5ee9d20b50847c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2163,7 +2163,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.3.0 # homeassistant.components.opensky -python-opensky==0.2.0 +python-opensky==0.2.1 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c9cc360ecdb61..3aa18738c4f9dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1610,7 +1610,7 @@ python-myq==3.1.13 python-mystrom==2.2.0 # homeassistant.components.opensky -python-opensky==0.2.0 +python-opensky==0.2.1 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 78879424098b48..2668affee96dfd 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -104,7 +104,7 @@ # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.7.1 +anyio==4.0.0 h11==0.14.0 httpcore==0.18.0 diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 5446e289656184..d2852ec42f51d3 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -2,12 +2,24 @@ from typing import Any +from nibe.heatpump import Model + from homeassistant.components.nibe_heatpump import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +MOCK_ENTRY_DATA = { + "model": None, + "ip_address": "127.0.0.1", + "listening_port": 9999, + "remote_read_port": 10000, + "remote_write_port": 10001, + "word_swap": True, + "connection_type": "nibegw", +} + async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> None: """Add entry and get the coordinator.""" @@ -17,3 +29,8 @@ async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED + + +async def async_add_model(hass: HomeAssistant, model: Model): + """Add entry of specific model.""" + await async_add_entry(hass, {**MOCK_ENTRY_DATA, "model": model.name}) diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py index 2a4e2f80ff504b..d7343eac69c811 100644 --- a/tests/components/nibe_heatpump/conftest.py +++ b/tests/components/nibe_heatpump/conftest.py @@ -62,4 +62,15 @@ async def read_coils( mock_connection.read_coil = read_coil mock_connection.read_coils = read_coils - return coils + + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.nibe_heatpump import HeatPump + + get_coils_original = HeatPump.get_coils + + def get_coils(x): + coils_data = get_coils_original(x) + return [coil for coil in coils_data if coil.address in coils] + + with patch.object(HeatPump, "get_coils", new=get_coils): + yield coils diff --git a/tests/components/nibe_heatpump/snapshots/test_number.ambr b/tests/components/nibe_heatpump/snapshots/test_number.ambr new file mode 100644 index 00000000000000..d174c0cc059d36 --- /dev/null +++ b/tests/components/nibe_heatpump/snapshots/test_number.ambr @@ -0,0 +1,135 @@ +# serializer version: 1 +# name: test_update[Model.F1155-47011-number.heat_offset_s1_47011--10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1155 Heat Offset S1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heat_offset_s1_47011', + 'last_changed': , + 'last_updated': , + 'state': '-10.0', + }) +# --- +# name: test_update[Model.F1155-47011-number.heat_offset_s1_47011-10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1155 Heat Offset S1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heat_offset_s1_47011', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_update[Model.F1155-47062-number.heat_offset_s1_47011-None] + None +# --- +# name: test_update[Model.F750-47062-number.hw_charge_offset_47062--10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F750 HW charge offset', + 'max': 12.7, + 'min': -12.8, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'number.hw_charge_offset_47062', + 'last_changed': , + 'last_updated': , + 'state': '-10.0', + }) +# --- +# name: test_update[Model.F750-47062-number.hw_charge_offset_47062-10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F750 HW charge offset', + 'max': 12.7, + 'min': -12.8, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'number.hw_charge_offset_47062', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_update[Model.F750-47062-number.hw_charge_offset_47062-None] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F750 HW charge offset', + 'max': 12.7, + 'min': -12.8, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'number.hw_charge_offset_47062', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_update[Model.S320-40031-number.heating_offset_climate_system_1_40031--10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'S320 Heating offset climate system 1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heating_offset_climate_system_1_40031', + 'last_changed': , + 'last_updated': , + 'state': '-10.0', + }) +# --- +# name: test_update[Model.S320-40031-number.heating_offset_climate_system_1_40031-10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'S320 Heating offset climate system 1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heating_offset_climate_system_1_40031', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_update[Model.S320-40031-number.heating_offset_climate_system_1_40031-None] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'S320 Heating offset climate system 1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heating_offset_climate_system_1_40031', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py index e4f90a59f67d05..755827fa1282d2 100644 --- a/tests/components/nibe_heatpump/test_button.py +++ b/tests/components/nibe_heatpump/test_button.py @@ -17,20 +17,10 @@ ) from homeassistant.core import HomeAssistant -from . import async_add_entry +from . import async_add_model from tests.common import async_fire_time_changed -MOCK_ENTRY_DATA = { - "model": None, - "ip_address": "127.0.0.1", - "listening_port": 9999, - "remote_read_port": 10000, - "remote_write_port": 10001, - "word_swap": True, - "connection_type": "nibegw", -} - @pytest.fixture(autouse=True) async def fixture_single_platform(): @@ -62,7 +52,7 @@ async def test_reset_button( coils[unit.alarm_reset] = 0 coils[unit.alarm] = 0 - await async_add_entry(hass, {**MOCK_ENTRY_DATA, "model": model.name}) + await async_add_model(hass, model) state = hass.states.get(entity_id) assert state diff --git a/tests/components/nibe_heatpump/test_number.py b/tests/components/nibe_heatpump/test_number.py new file mode 100644 index 00000000000000..5c4d7f4341b40e --- /dev/null +++ b/tests/components/nibe_heatpump/test_number.py @@ -0,0 +1,109 @@ +"""Test the Nibe Heat Pump config flow.""" +from typing import Any +from unittest.mock import AsyncMock, patch + +from nibe.coil import CoilData +from nibe.heatpump import Model +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant + +from . import async_add_model + + +@pytest.fixture(autouse=True) +async def fixture_single_platform(): + """Only allow this platform to load.""" + with patch("homeassistant.components.nibe_heatpump.PLATFORMS", [Platform.NUMBER]): + yield + + +@pytest.mark.parametrize( + ("model", "address", "entity_id", "value"), + [ + # Tests for S series coils with min/max + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", 10), + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", -10), + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", None), + # Tests for F series coils with min/max + (Model.F1155, 47011, "number.heat_offset_s1_47011", 10), + (Model.F1155, 47011, "number.heat_offset_s1_47011", -10), + (Model.F1155, 47062, "number.heat_offset_s1_47011", None), + # Tests for F series coils without min/max + (Model.F750, 47062, "number.hw_charge_offset_47062", 10), + (Model.F750, 47062, "number.hw_charge_offset_47062", -10), + (Model.F750, 47062, "number.hw_charge_offset_47062", None), + ], +) +async def test_update( + hass: HomeAssistant, + model: Model, + entity_id: str, + address: int, + value: Any, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, + snapshot: SnapshotAssertion, +) -> None: + """Test setting of value.""" + coils[address] = value + + await async_add_model(hass, model) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state == snapshot + + +@pytest.mark.parametrize( + ("model", "address", "entity_id", "value"), + [ + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", 10), + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", -10), + (Model.F1155, 47011, "number.heat_offset_s1_47011", 10), + (Model.F1155, 47011, "number.heat_offset_s1_47011", -10), + (Model.F750, 47062, "number.hw_charge_offset_47062", 10), + ], +) +async def test_set_value( + hass: HomeAssistant, + mock_connection: AsyncMock, + model: Model, + entity_id: str, + address: int, + value: Any, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, +) -> None: + """Test setting of value.""" + coils[address] = 0 + + await async_add_model(hass, model) + + await hass.async_block_till_done() + assert hass.states.get(entity_id) + + # Write value + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + await hass.async_block_till_done() + + # Verify written + args = mock_connection.write_coil.call_args + assert args + coil = args.args[0] + assert isinstance(coil, CoilData) + assert coil.coil.address == address + assert coil.value == value diff --git a/tests/components/opensky/__init__.py b/tests/components/opensky/__init__.py index e746521c72cdbb..0f24f8931afe95 100644 --- a/tests/components/opensky/__init__.py +++ b/tests/components/opensky/__init__.py @@ -1,10 +1,9 @@ """Opensky tests.""" -import json from unittest.mock import patch from python_opensky import StatesResponse -from tests.common import load_fixture +from tests.common import load_json_object_fixture def patch_setup_entry() -> bool: @@ -16,5 +15,5 @@ def patch_setup_entry() -> bool: def get_states_response_fixture(fixture: str) -> StatesResponse: """Return the states response from json.""" - json_fixture = load_fixture(fixture) - return StatesResponse.parse_obj(json.loads(json_fixture)) + states_json = load_json_object_fixture(fixture) + return StatesResponse.from_api(states_json) diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index f74c18773f5aba..90e0b7251bfb23 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,10 +1,8 @@ """Configure tests for the OpenSky integration.""" from collections.abc import Awaitable, Callable -import json from unittest.mock import patch import pytest -from python_opensky import StatesResponse from homeassistant.components.opensky.const import ( CONF_ALTITUDE, @@ -21,7 +19,9 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_fixture +from . import get_states_response_fixture + +from tests.common import MockConfigEntry ComponentSetup = Callable[[MockConfigEntry], Awaitable[None]] @@ -88,10 +88,9 @@ async def mock_setup_integration( async def func(mock_config_entry: MockConfigEntry) -> None: mock_config_entry.add_to_hass(hass) - json_fixture = load_fixture("opensky/states.json") with patch( "python_opensky.OpenSky.get_states", - return_value=StatesResponse.parse_obj(json.loads(json_fixture)), + return_value=get_states_response_fixture("opensky/states.json"), ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index b637a0d0356abe..3429d5eec7e196 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -1,10 +1,8 @@ """OpenSky sensor tests.""" from datetime import timedelta -import json from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory -from python_opensky import StatesResponse from syrupy import SnapshotAssertion from homeassistant.components.opensky.const import ( @@ -18,19 +16,19 @@ from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component +from . import get_states_response_fixture from .conftest import ComponentSetup -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.common import MockConfigEntry, async_fire_time_changed LEGACY_CONFIG = {Platform.SENSOR: [{CONF_PLATFORM: DOMAIN, CONF_RADIUS: 10.0}]} async def test_legacy_migration(hass: HomeAssistant) -> None: """Test migration from yaml to config flow.""" - json_fixture = load_fixture("opensky/states.json") with patch( "python_opensky.OpenSky.get_states", - return_value=StatesResponse.parse_obj(json.loads(json_fixture)), + return_value=get_states_response_fixture("opensky/states.json"), ): assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) await hass.async_block_till_done() @@ -85,10 +83,6 @@ async def test_sensor_updating( """Test updating sensor.""" await setup_integration(config_entry) - def get_states_response_fixture(fixture: str) -> StatesResponse: - json_fixture = load_fixture(fixture) - return StatesResponse.parse_obj(json.loads(json_fixture)) - events = [] async def event_listener(event: Event) -> None: