diff --git a/.coveragerc b/.coveragerc index d91825943561b19656ff67f6c11dd71b9fc324af..12095eef2472145bf1868a57dae9903008fcdf2b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -389,7 +389,6 @@ omit = homeassistant/components/firmata/pin.py homeassistant/components/firmata/sensor.py homeassistant/components/firmata/switch.py - homeassistant/components/fitbit/* homeassistant/components/fivem/__init__.py homeassistant/components/fivem/binary_sensor.py homeassistant/components/fivem/coordinator.py diff --git a/homeassistant/components/fitbit/const.py b/homeassistant/components/fitbit/const.py index 1578359356d2056da7dfd06d003c038400ae0f20..045b58cfc5e3a0800312b2525b71c69e187a6a5b 100644 --- a/homeassistant/components/fitbit/const.py +++ b/homeassistant/components/fitbit/const.py @@ -12,6 +12,8 @@ from homeassistant.const import ( UnitOfVolume, ) +DOMAIN: Final = "fitbit" + ATTR_ACCESS_TOKEN: Final = "access_token" ATTR_REFRESH_TOKEN: Final = "refresh_token" ATTR_LAST_SAVED_AT: Final = "last_saved_at" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c469e25e54b10b450651c4e99a3165e8c13b671..ce69841233bc14cd10bdef4691da9524ef4e5c54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -629,6 +629,9 @@ feedparser==6.0.10 # homeassistant.components.file file-read-backwards==2.0.0 +# homeassistant.components.fitbit +fitbit==0.3.1 + # homeassistant.components.fivem fivem-api==0.1.2 diff --git a/tests/components/fitbit/__init__.py b/tests/components/fitbit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0b639a3faa86a235ce53825b67529da89e18d7db --- /dev/null +++ b/tests/components/fitbit/__init__.py @@ -0,0 +1 @@ +"""Tests for fitbit component.""" diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..e3e5bbd1d1862507a7a5ba04a6549d6f881fcfd4 --- /dev/null +++ b/tests/components/fitbit/conftest.py @@ -0,0 +1,167 @@ +"""Test fixtures for fitbit.""" + +from collections.abc import Awaitable, Callable, Generator +import datetime +from http import HTTPStatus +import time +from typing import Any +from unittest.mock import patch + +import pytest +from requests_mock.mocker import Mocker + +from homeassistant.components.fitbit.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +PROFILE_USER_ID = "fitbit-api-user-id-1" +FAKE_TOKEN = "some-token" +FAKE_REFRESH_TOKEN = "some-refresh-token" + +PROFILE_API_URL = "https://api.fitbit.com/1/user/-/profile.json" +DEVICES_API_URL = "https://api.fitbit.com/1/user/-/devices.json" +TIMESERIES_API_URL_FORMAT = ( + "https://api.fitbit.com/1/user/-/{resource}/date/today/7d.json" +) + + +@pytest.fixture(name="token_expiration_time") +def mcok_token_expiration_time() -> float: + """Fixture for expiration time of the config entry auth token.""" + return time.time() + 86400 + + +@pytest.fixture(name="fitbit_config_yaml") +def mock_fitbit_config_yaml(token_expiration_time: float) -> dict[str, Any]: + """Fixture for the yaml fitbit.conf file contents.""" + return { + "access_token": FAKE_TOKEN, + "refresh_token": FAKE_REFRESH_TOKEN, + "last_saved_at": token_expiration_time, + } + + +@pytest.fixture(name="fitbit_config_setup", autouse=True) +def mock_fitbit_config_setup( + fitbit_config_yaml: dict[str, Any], +) -> Generator[None, None, None]: + """Fixture to mock out fitbit.conf file data loading and persistence.""" + + with patch( + "homeassistant.components.fitbit.sensor.os.path.isfile", return_value=True + ), patch( + "homeassistant.components.fitbit.sensor.load_json_object", + return_value=fitbit_config_yaml, + ), patch( + "homeassistant.components.fitbit.sensor.save_json", + ): + yield + + +@pytest.fixture(name="monitored_resources") +def mock_monitored_resources() -> list[str] | None: + """Fixture for the fitbit yaml config monitored_resources field.""" + return None + + +@pytest.fixture(name="sensor_platform_config") +def mock_sensor_platform_config( + monitored_resources: list[str] | None, +) -> dict[str, Any]: + """Fixture for the fitbit sensor platform configuration data in configuration.yaml.""" + config = {} + if monitored_resources is not None: + config["monitored_resources"] = monitored_resources + return config + + +@pytest.fixture(name="sensor_platform_setup") +async def mock_sensor_platform_setup( + hass: HomeAssistant, + sensor_platform_config: dict[str, Any], +) -> Callable[[], Awaitable[bool]]: + """Fixture to set up the integration.""" + + async def run() -> bool: + result = await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": DOMAIN, + **sensor_platform_config, + } + ] + }, + ) + await hass.async_block_till_done() + return result + + return run + + +@pytest.fixture(name="profile_id") +async def mock_profile_id() -> str: + """Fixture for the profile id returned from the API response.""" + return PROFILE_USER_ID + + +@pytest.fixture(name="profile", autouse=True) +async def mock_profile(requests_mock: Mocker, profile_id: str) -> None: + """Fixture to setup fake requests made to Fitbit API during config flow.""" + requests_mock.register_uri( + "GET", + PROFILE_API_URL, + status_code=HTTPStatus.OK, + json={ + "user": { + "encodedId": profile_id, + "fullName": "My name", + "locale": "en_US", + }, + }, + ) + + +@pytest.fixture(name="devices_response") +async def mock_device_response() -> list[dict[str, Any]]: + """Return the list of devices.""" + return [] + + +@pytest.fixture(autouse=True) +async def mock_devices(requests_mock: Mocker, devices_response: dict[str, Any]) -> None: + """Fixture to setup fake device responses.""" + requests_mock.register_uri( + "GET", + DEVICES_API_URL, + status_code=HTTPStatus.OK, + json=devices_response, + ) + + +def timeseries_response(resource: str, value: str) -> dict[str, Any]: + """Create a timeseries response value.""" + return { + resource: [{"dateTime": datetime.datetime.today().isoformat(), "value": value}] + } + + +@pytest.fixture(name="register_timeseries") +async def mock_register_timeseries( + requests_mock: Mocker, +) -> Callable[[str, dict[str, Any]], None]: + """Fixture to setup fake timeseries API responses.""" + + def register(resource: str, response: dict[str, Any]) -> None: + requests_mock.register_uri( + "GET", + TIMESERIES_API_URL_FORMAT.format(resource=resource), + status_code=HTTPStatus.OK, + json=response, + ) + + return register diff --git a/tests/components/fitbit/test_sensor.py b/tests/components/fitbit/test_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..6918a712f72cb8c0f0085aa2f555078a0e065702 --- /dev/null +++ b/tests/components/fitbit/test_sensor.py @@ -0,0 +1,92 @@ +"""Tests for the fitbit sensor platform.""" + + +from collections.abc import Awaitable, Callable +from typing import Any + +import pytest + +from homeassistant.core import HomeAssistant + +from .conftest import timeseries_response + +DEVICE_RESPONSE_CHARGE_2 = { + "battery": "Medium", + "batteryLevel": 60, + "deviceVersion": "Charge 2", + "id": "816713257", + "lastSyncTime": "2019-11-07T12:00:58.000", + "mac": "16ADD56D54GD", + "type": "TRACKER", +} +DEVICE_RESPONSE_ARIA_AIR = { + "battery": "High", + "batteryLevel": 95, + "deviceVersion": "Aria Air", + "id": "016713257", + "lastSyncTime": "2019-11-07T12:00:58.000", + "mac": "06ADD56D54GD", + "type": "SCALE", +} + + +@pytest.mark.parametrize( + "monitored_resources", + [["activities/steps"]], +) +async def test_step_sensor( + hass: HomeAssistant, + sensor_platform_setup: Callable[[], Awaitable[bool]], + register_timeseries: Callable[[str, dict[str, Any]], None], +) -> None: + """Test battery level sensor.""" + + register_timeseries( + "activities/steps", timeseries_response("activities-steps", "5600") + ) + await sensor_platform_setup() + + state = hass.states.get("sensor.steps") + assert state + assert state.state == "5600" + assert state.attributes == { + "attribution": "Data provided by Fitbit.com", + "friendly_name": "Steps", + "icon": "mdi:walk", + "unit_of_measurement": "steps", + } + + +@pytest.mark.parametrize( + ("devices_response", "monitored_resources"), + [([DEVICE_RESPONSE_CHARGE_2, DEVICE_RESPONSE_ARIA_AIR], ["devices/battery"])], +) +async def test_device_battery_level( + hass: HomeAssistant, + sensor_platform_setup: Callable[[], Awaitable[bool]], +) -> None: + """Test battery level sensor for devices.""" + + await sensor_platform_setup() + + state = hass.states.get("sensor.charge_2_battery") + assert state + assert state.state == "Medium" + assert state.attributes == { + "attribution": "Data provided by Fitbit.com", + "friendly_name": "Charge 2 Battery", + "icon": "mdi:battery-50", + "model": "Charge 2", + "type": "tracker", + } + + state = hass.states.get("sensor.aria_air_battery") + assert state + assert state.state == "High" + assert state.attributes == { + "attribution": "Data provided by Fitbit.com", + "friendly_name": "Aria Air Battery", + "icon": "mdi:battery", + "model": "Aria Air", + "type": "scale", + }