"""Tests for the Withings component.""" from unittest.mock import MagicMock, patch import asynctest from withings_api import ( WithingsApi, WithingsMeasures, WithingsSleep, WithingsSleepSummary, ) import pytest from homeassistant.components.withings import DOMAIN from homeassistant.components.withings.common import NotAuthenticatedError import homeassistant.components.withings.const as const from homeassistant.components.withings.sensor import async_setup_entry from homeassistant.config_entries import ConfigEntry, SOURCE_USER from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig def get_entity_id(measure, profile): """Get an entity id for a measure and profile.""" return "sensor.{}_{}_{}".format(DOMAIN, measure, slugify(profile)) def assert_state_equals(hass: HomeAssistantType, profile: str, measure: str, expected): """Assert the state of a withings sensor.""" entity_id = get_entity_id(measure, profile) state_obj = hass.states.get(entity_id) assert state_obj, "Expected entity {} to exist but it did not".format(entity_id) assert state_obj.state == str( expected ), "Expected {} but was {} for measure {}".format( expected, state_obj.state, measure ) async def test_health_sensor_properties(withings_factory: WithingsFactory): """Test method.""" data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M])) await data.configure_all(WithingsFactoryConfig.PROFILE_1, "authorization_code") state = data.hass.states.get("sensor.withings_height_m_person_1") state_dict = state.as_dict() assert state_dict.get("state") == "2" assert state_dict.get("attributes") == { "measurement": "height_m", "measure_type": 4, "friendly_name": "Withings height_m person_1", "unit_of_measurement": "m", "icon": "mdi:ruler", } SENSOR_TEST_DATA = [ (const.MEAS_WEIGHT_KG, 70), (const.MEAS_FAT_MASS_KG, 5), (const.MEAS_FAT_FREE_MASS_KG, 60), (const.MEAS_MUSCLE_MASS_KG, 50), (const.MEAS_BONE_MASS_KG, 10), (const.MEAS_HEIGHT_M, 2), (const.MEAS_FAT_RATIO_PCT, 0.07), (const.MEAS_DIASTOLIC_MMHG, 70), (const.MEAS_SYSTOLIC_MMGH, 100), (const.MEAS_HEART_PULSE_BPM, 60), (const.MEAS_SPO2_PCT, 0.95), (const.MEAS_HYDRATION, 0.95), (const.MEAS_PWV, 100), (const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, 320), (const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, 520), (const.MEAS_SLEEP_DEEP_DURATION_SECONDS, 720), (const.MEAS_SLEEP_REM_DURATION_SECONDS, 920), (const.MEAS_SLEEP_WAKEUP_COUNT, 1120), (const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, 1320), (const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, 1520), (const.MEAS_SLEEP_HEART_RATE_AVERAGE, 1720), (const.MEAS_SLEEP_HEART_RATE_MIN, 1920), (const.MEAS_SLEEP_HEART_RATE_MAX, 2120), (const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, 2320), (const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, 2520), (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, 2720), ] @pytest.mark.parametrize("measure,expected", SENSOR_TEST_DATA) async def test_health_sensor_throttled( withings_factory: WithingsFactory, measure, expected ): """Test method.""" data = await withings_factory(WithingsFactoryConfig(measures=measure)) profile = WithingsFactoryConfig.PROFILE_1 await data.configure_all(profile, "authorization_code") # Checking initial data. assert_state_equals(data.hass, profile, measure, expected) # Encountering a throttled data. await async_update_entity(data.hass, get_entity_id(measure, profile)) assert_state_equals(data.hass, profile, measure, expected) NONE_SENSOR_TEST_DATA = [ (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), ] @pytest.mark.parametrize("measure,expected", NONE_SENSOR_TEST_DATA) async def test_health_sensor_state_none( withings_factory: WithingsFactory, measure, expected ): """Test method.""" data = await withings_factory( WithingsFactoryConfig( measures=measure, withings_measures_response=None, withings_sleep_response=None, withings_sleep_summary_response=None, ) ) profile = WithingsFactoryConfig.PROFILE_1 await data.configure_all(profile, "authorization_code") # Checking initial data. assert_state_equals(data.hass, profile, measure, expected) # Encountering a throttled data. await async_update_entity(data.hass, get_entity_id(measure, profile)) assert_state_equals(data.hass, profile, measure, expected) EMPTY_SENSOR_TEST_DATA = [ (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), ] @pytest.mark.parametrize("measure,expected", EMPTY_SENSOR_TEST_DATA) async def test_health_sensor_state_empty( withings_factory: WithingsFactory, measure, expected ): """Test method.""" data = await withings_factory( WithingsFactoryConfig( measures=measure, withings_measures_response=WithingsMeasures({"measuregrps": []}), withings_sleep_response=WithingsSleep({"series": []}), withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) profile = WithingsFactoryConfig.PROFILE_1 await data.configure_all(profile, "authorization_code") # Checking initial data. assert_state_equals(data.hass, profile, measure, expected) # Encountering a throttled data. await async_update_entity(data.hass, get_entity_id(measure, profile)) assert_state_equals(data.hass, profile, measure, expected) SLEEP_STATES_TEST_DATA = [ ( const.STATE_AWAKE, [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_AWAKE], ), ( const.STATE_LIGHT, [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_LIGHT], ), ( const.STATE_REM, [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_REM], ), ( const.STATE_DEEP, [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, const.MEASURE_TYPE_SLEEP_STATE_DEEP], ), (const.STATE_UNKNOWN, [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, "blah,"]), ] @pytest.mark.parametrize("expected,sleep_states", SLEEP_STATES_TEST_DATA) async def test_sleep_state_throttled( withings_factory: WithingsFactory, expected, sleep_states ): """Test method.""" measure = const.MEAS_SLEEP_STATE data = await withings_factory( WithingsFactoryConfig( measures=[measure], withings_sleep_response=withings_sleep_response(sleep_states), ) ) profile = WithingsFactoryConfig.PROFILE_1 await data.configure_all(profile, "authorization_code") # Check initial data. assert_state_equals(data.hass, profile, measure, expected) # Encountering a throttled data. await async_update_entity(data.hass, get_entity_id(measure, profile)) assert_state_equals(data.hass, profile, measure, expected) async def test_async_setup_check_credentials( hass: HomeAssistantType, withings_factory: WithingsFactory ): """Test method.""" check_creds_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" ".check_authenticated", side_effect=NotAuthenticatedError(), ) async_init_patch = asynctest.patch.object( hass.config_entries.flow, "async_init", wraps=hass.config_entries.flow.async_init, ) with check_creds_patch, async_init_patch as async_init_mock: data = await withings_factory( WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M]) ) profile = WithingsFactoryConfig.PROFILE_1 await data.configure_all(profile, "authorization_code") async_init_mock.assert_called_with( const.DOMAIN, context={"source": SOURCE_USER, const.PROFILE: profile}, data={}, ) async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): """Test method.""" expected_creds = { "access_token": "my_access_token2", "refresh_token": "my_refresh_token2", "token_type": "my_token_type2", "expires_in": "2", } original_withings_api = WithingsApi withings_api_instance = None def new_withings_api(*args, **kwargs): nonlocal withings_api_instance withings_api_instance = original_withings_api(*args, **kwargs) withings_api_instance.request = MagicMock() return withings_api_instance withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( hass.config_entries, "async_update_entry", wraps=hass.config_entries.async_update_entry, ) with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( version=1, domain=const.DOMAIN, title="my title", data={ const.PROFILE: "Person 1", const.CREDENTIALS: { "access_token": "my_access_token", "refresh_token": "my_refresh_token", "token_type": "my_token_type", "token_expiry": "9999999999", }, }, source="source", connection_class="conn_class", system_options={}, ) await async_setup_entry(hass, config_entry, async_add_entities) withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2"