Remove code for old fitbit config import (#130783)

* Remove code for old fitbit config import

* Remove translations related to issues
pull/130719/head
Allen Porter 2024-11-16 20:09:59 -08:00 committed by GitHub
parent f58b5418ea
commit 96299b16e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 27 additions and 456 deletions

View File

@ -6,30 +6,16 @@ from collections.abc import Callable
from dataclasses import dataclass
import datetime
import logging
import os
from typing import Any, Final, cast
from fitbit import Fitbit
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
import voluptuous as vol
from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_TOKEN,
CONF_UNIT_SYSTEM,
PERCENTAGE,
EntityCategory,
UnitOfLength,
@ -38,33 +24,13 @@ from homeassistant.const import (
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.json import load_json_object
from .api import FitbitApi
from .const import (
ATTR_ACCESS_TOKEN,
ATTR_LAST_SAVED_AT,
ATTR_REFRESH_TOKEN,
ATTRIBUTION,
BATTERY_LEVELS,
CONF_CLOCK_FORMAT,
CONF_MONITORED_RESOURCES,
DEFAULT_CLOCK_FORMAT,
DEFAULT_CONFIG,
DOMAIN,
FITBIT_CONFIG_FILE,
FITBIT_DEFAULT_RESOURCES,
FitbitScope,
FitbitUnitSystem,
)
from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem
from .coordinator import FitbitData, FitbitDeviceCoordinator
from .exceptions import FitbitApiException, FitbitAuthException
from .model import FitbitDevice, config_from_entry_data
@ -533,126 +499,6 @@ FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
native_unit_of_measurement=PERCENTAGE,
)
FITBIT_RESOURCES_KEYS: Final[list[str]] = [
desc.key
for desc in (*FITBIT_RESOURCES_LIST, FITBIT_RESOURCE_BATTERY, SLEEP_START_TIME)
]
PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Optional(
CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES
): vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_KEYS)]),
vol.Optional(CONF_CLOCK_FORMAT, default=DEFAULT_CLOCK_FORMAT): vol.In(
["12H", "24H"]
),
vol.Optional(CONF_UNIT_SYSTEM, default=FitbitUnitSystem.LEGACY_DEFAULT): vol.In(
[
FitbitUnitSystem.EN_GB,
FitbitUnitSystem.EN_US,
FitbitUnitSystem.METRIC,
FitbitUnitSystem.LEGACY_DEFAULT,
]
),
}
)
# Only import configuration if it was previously created successfully with all
# of the following fields.
FITBIT_CONF_KEYS = [
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
ATTR_ACCESS_TOKEN,
ATTR_REFRESH_TOKEN,
ATTR_LAST_SAVED_AT,
]
def load_config_file(config_path: str) -> dict[str, Any] | None:
"""Load existing valid fitbit.conf from disk for import."""
if os.path.isfile(config_path):
config_file = load_json_object(config_path)
if config_file != DEFAULT_CONFIG and all(
key in config_file for key in FITBIT_CONF_KEYS
):
return config_file
return None
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Fitbit sensor."""
config_path = hass.config.path(FITBIT_CONFIG_FILE)
config_file = await hass.async_add_executor_job(load_config_file, config_path)
_LOGGER.debug("loaded config file: %s", config_file)
if config_file is not None:
_LOGGER.debug("Importing existing fitbit.conf application credentials")
# Refresh the token before importing to ensure it is working and not
# expired on first initialization.
authd_client = Fitbit(
config_file[CONF_CLIENT_ID],
config_file[CONF_CLIENT_SECRET],
access_token=config_file[ATTR_ACCESS_TOKEN],
refresh_token=config_file[ATTR_REFRESH_TOKEN],
expires_at=config_file[ATTR_LAST_SAVED_AT],
refresh_cb=lambda x: None,
)
try:
updated_token = await hass.async_add_executor_job(
authd_client.client.refresh_token
)
except OAuth2Error as err:
_LOGGER.debug("Unable to import fitbit OAuth2 credentials: %s", err)
translation_key = "deprecated_yaml_import_issue_cannot_connect"
else:
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
),
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
"auth_implementation": DOMAIN,
CONF_TOKEN: {
ATTR_ACCESS_TOKEN: updated_token[ATTR_ACCESS_TOKEN],
ATTR_REFRESH_TOKEN: updated_token[ATTR_REFRESH_TOKEN],
"expires_at": updated_token["expires_at"],
"scope": " ".join(updated_token.get("scope", [])),
},
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
},
)
translation_key = "deprecated_yaml_import"
if (
result.get("type") == FlowResultType.ABORT
and result.get("reason") == "cannot_connect"
):
translation_key = "deprecated_yaml_import_issue_cannot_connect"
else:
translation_key = "deprecated_yaml_no_import"
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2024.5.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=translation_key,
)
async def async_setup_entry(
hass: HomeAssistant,

View File

@ -40,19 +40,5 @@
"name": "Battery level"
}
}
},
"issues": {
"deprecated_yaml_no_import": {
"title": "Fitbit YAML configuration is being removed",
"description": "Configuring Fitbit using YAML is being removed.\n\nRemove the `fitbit` configuration from your configuration.yaml file and remove fitbit.conf if it exists and restart Home Assistant and [set up the integration](/config/integrations/dashboard/add?domain=fitbit) manually."
},
"deprecated_yaml_import": {
"title": "Fitbit YAML configuration is being removed",
"description": "Configuring Fitbit using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically, including OAuth Application Credentials.\n\nRemove the `fitbit` configuration from your configuration.yaml file and remove fitbit.conf and restart Home Assistant to fix this issue."
},
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The Fitbit YAML configuration import failed",
"description": "Configuring Fitbit using YAML is being removed but there was a connection error importing your YAML configuration.\n\nRestart Home Assistant to try again or remove the Fitbit YAML configuration from your configuration.yaml file and remove the fitbit.conf and continue to [set up the integration](/config/integrations/dashboard/add?domain=fitbit) manually."
}
}
}

View File

@ -1,6 +1,6 @@
"""Test fixtures for fitbit."""
from collections.abc import Awaitable, Callable, Generator
from collections.abc import Awaitable, Callable
import datetime
from http import HTTPStatus
import time
@ -14,12 +14,7 @@ from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.fitbit.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
DOMAIN,
OAUTH_SCOPES,
)
from homeassistant.components.fitbit.const import DOMAIN, OAUTH_SCOPES
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@ -83,13 +78,16 @@ def mock_token_entry(token_expiration_time: float, scopes: list[str]) -> dict[st
@pytest.fixture(name="config_entry")
def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry:
def mock_config_entry(
token_entry: dict[str, Any], imported_config_data: dict[str, Any]
) -> MockConfigEntry:
"""Fixture for a config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
"auth_implementation": FAKE_AUTH_IMPL,
"token": token_entry,
**imported_config_data,
},
unique_id=PROFILE_USER_ID,
)
@ -107,37 +105,6 @@ async def setup_credentials(hass: HomeAssistant) -> None:
)
@pytest.fixture(name="fitbit_config_yaml")
def mock_fitbit_config_yaml(token_expiration_time: float) -> dict[str, Any] | None:
"""Fixture for the yaml fitbit.conf file contents."""
return {
CONF_CLIENT_ID: CLIENT_ID,
CONF_CLIENT_SECRET: CLIENT_SECRET,
"access_token": FAKE_ACCESS_TOKEN,
"refresh_token": FAKE_REFRESH_TOKEN,
"last_saved_at": token_expiration_time,
}
@pytest.fixture(name="fitbit_config_setup")
def mock_fitbit_config_setup(
fitbit_config_yaml: dict[str, Any] | None,
) -> Generator[None]:
"""Fixture to mock out fitbit.conf file data loading and persistence."""
has_config = fitbit_config_yaml is not None
with (
patch(
"homeassistant.components.fitbit.sensor.os.path.isfile",
return_value=has_config,
),
patch(
"homeassistant.components.fitbit.sensor.load_json_object",
return_value=fitbit_config_yaml,
),
):
yield
@pytest.fixture(name="monitored_resources")
def mock_monitored_resources() -> list[str] | None:
"""Fixture for the fitbit yaml config monitored_resources field."""
@ -150,8 +117,8 @@ def mock_configured_unit_syststem() -> str | None:
return None
@pytest.fixture(name="sensor_platform_config")
def mock_sensor_platform_config(
@pytest.fixture(name="imported_config_data")
def mock_imported_config_data(
monitored_resources: list[str] | None,
configured_unit_system: str | None,
) -> dict[str, Any]:
@ -164,32 +131,6 @@ def mock_sensor_platform_config(
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
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""

View File

@ -2,7 +2,6 @@
from collections.abc import Awaitable, Callable
from http import HTTPStatus
import time
from typing import Any
from unittest.mock import patch
@ -13,7 +12,7 @@ from homeassistant import config_entries
from homeassistant.components.fitbit.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir
from homeassistant.helpers import config_entry_oauth2_flow
from .conftest import (
CLIENT_ID,
@ -255,207 +254,6 @@ async def test_config_entry_already_exists(
assert result.get("reason") == "already_configured"
@pytest.mark.parametrize(
"token_expiration_time",
[time.time() + 86400, time.time() - 86400],
ids=("token_active", "token_expired"),
)
async def test_import_fitbit_config(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
requests_mock: Mocker,
) -> None:
"""Test that platform configuration is imported successfully."""
requests_mock.register_uri(
"POST",
OAUTH2_TOKEN,
status_code=HTTPStatus.OK,
json=SERVER_ACCESS_TOKEN,
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
# Verify valid profile can be fetched from the API
config_entry = entries[0]
assert config_entry.title == DISPLAY_NAME
assert config_entry.unique_id == PROFILE_USER_ID
data = dict(config_entry.data)
# Verify imported values from fitbit.conf and configuration.yaml and
# that the token is updated.
assert "token" in data
expires_at = data["token"]["expires_at"]
assert expires_at > time.time()
del data["token"]["expires_at"]
assert dict(config_entry.data) == {
"auth_implementation": DOMAIN,
"clock_format": "24H",
"monitored_resources": ["activities/steps"],
"token": {
"access_token": "server-access-token",
"refresh_token": "server-refresh-token",
"scope": "activity heartrate nutrition profile settings sleep weight",
},
"unit_system": "default",
}
# Verify an issue is raised for deprecated configuration.yaml
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import"
async def test_import_fitbit_config_failure_cannot_connect(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
requests_mock: Mocker,
) -> None:
"""Test platform configuration fails to import successfully."""
requests_mock.register_uri(
"POST",
OAUTH2_TOKEN,
status_code=HTTPStatus.OK,
json=SERVER_ACCESS_TOKEN,
)
requests_mock.register_uri(
"GET", PROFILE_API_URL, status_code=HTTPStatus.INTERNAL_SERVER_ERROR
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
assert len(mock_setup.mock_calls) == 0
# Verify an issue is raised that we were unable to import configuration
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import_issue_cannot_connect"
@pytest.mark.parametrize(
"status_code",
[
(HTTPStatus.UNAUTHORIZED),
(HTTPStatus.INTERNAL_SERVER_ERROR),
],
)
async def test_import_fitbit_config_cannot_refresh(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
requests_mock: Mocker,
status_code: HTTPStatus,
) -> None:
"""Test platform configuration import fails when refreshing the token."""
requests_mock.register_uri(
"POST",
OAUTH2_TOKEN,
status_code=status_code,
json="",
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
assert len(mock_setup.mock_calls) == 0
# Verify an issue is raised that we were unable to import configuration
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import_issue_cannot_connect"
async def test_import_fitbit_config_already_exists(
hass: HomeAssistant,
config_entry: MockConfigEntry,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
requests_mock: Mocker,
) -> None:
"""Test that platform configuration is not imported if it already exists."""
requests_mock.register_uri(
"POST",
OAUTH2_TOKEN,
status_code=HTTPStatus.OK,
json=SERVER_ACCESS_TOKEN,
)
# Verify existing config entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_config_entry_setup:
await integration_setup()
assert len(mock_config_entry_setup.mock_calls) == 1
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_import_setup:
await sensor_platform_setup()
assert len(mock_import_setup.mock_calls) == 0
# Still one config entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
# Verify an issue is raised for deprecated configuration.yaml
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import"
async def test_platform_setup_without_import(
hass: HomeAssistant,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
) -> None:
"""Test platform configuration.yaml but no existing fitbit.conf credentials."""
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
# Verify no configuration entry is imported since the integration is not
# fully setup properly
assert len(mock_setup.mock_calls) == 0
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 0
# Verify an issue is raised for deprecated configuration.yaml
assert len(issue_registry.issues) == 1
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_no_import"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth_flow(
hass: HomeAssistant,

View File

@ -212,8 +212,8 @@ def mock_token_refresh(requests_mock: Mocker) -> None:
)
async def test_sensors(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
register_timeseries: Callable[[str, dict[str, Any]], None],
entity_registry: er.EntityRegistry,
entity_id: str,
@ -226,7 +226,7 @@ async def test_sensors(
register_timeseries(
api_resource, timeseries_response(api_resource.replace("/", "-"), api_value)
)
await sensor_platform_setup()
await integration_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
@ -243,13 +243,13 @@ async def test_sensors(
)
async def test_device_battery(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
entity_registry: er.EntityRegistry,
) -> None:
"""Test battery level sensor for devices."""
assert await sensor_platform_setup()
assert await integration_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
@ -290,13 +290,13 @@ async def test_device_battery(
)
async def test_device_battery_level(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
entity_registry: er.EntityRegistry,
) -> None:
"""Test battery level sensor for devices."""
assert await sensor_platform_setup()
assert await integration_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
@ -347,15 +347,15 @@ async def test_device_battery_level(
)
async def test_profile_local(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
register_timeseries: Callable[[str, dict[str, Any]], None],
expected_unit: str,
) -> None:
"""Test the fitbit profile locale impact on unit of measure."""
register_timeseries("body/weight", timeseries_response("body-weight", "175"))
await sensor_platform_setup()
await integration_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
@ -365,7 +365,7 @@ async def test_profile_local(
@pytest.mark.parametrize(
("sensor_platform_config", "api_response", "expected_state"),
("imported_config_data", "api_response", "expected_state"),
[
(
{"clock_format": "12H", "monitored_resources": ["sleep/startTime"]},
@ -396,8 +396,8 @@ async def test_profile_local(
)
async def test_sleep_time_clock_format(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
register_timeseries: Callable[[str, dict[str, Any]], None],
api_response: str,
expected_state: str,
@ -407,7 +407,7 @@ async def test_sleep_time_clock_format(
register_timeseries(
"sleep/startTime", timeseries_response("sleep-startTime", api_response)
)
await sensor_platform_setup()
assert await integration_setup()
state = hass.states.get("sensor.sleep_start_time")
assert state