diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 6d89c4c0a76..37dbdd486a7 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -29,10 +29,14 @@ from homeassistant.util import slugify from .const import ( ATTR_CONFIG_ENTRY_ID, + ATTR_DESTINATION_COUNTRY, ATTR_INFO_TEXT, + ATTR_ORIGIN_COUNTRY, ATTR_PACKAGE_STATE, + ATTR_PACKAGE_TYPE, ATTR_STATUS, ATTR_TIMESTAMP, + ATTR_TRACKING_INFO_LANGUAGE, ATTR_TRACKING_NUMBER, DOMAIN, SERVICE_GET_PACKAGES, @@ -104,6 +108,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return { "packages": [ { + ATTR_DESTINATION_COUNTRY: package.destination_country, + ATTR_ORIGIN_COUNTRY: package.origin_country, + ATTR_PACKAGE_TYPE: package.package_type, + ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language, ATTR_TRACKING_NUMBER: package.tracking_number, ATTR_LOCATION: package.location, ATTR_STATUS: package.status, diff --git a/homeassistant/components/seventeentrack/const.py b/homeassistant/components/seventeentrack/const.py index 39932d31935..584eca507e9 100644 --- a/homeassistant/components/seventeentrack/const.py +++ b/homeassistant/components/seventeentrack/const.py @@ -45,3 +45,5 @@ SERVICE_GET_PACKAGES = "get_packages" ATTR_PACKAGE_STATE = "package_state" ATTR_CONFIG_ENTRY_ID = "config_entry_id" + +DEPRECATED_KEY = "deprecated" diff --git a/homeassistant/components/seventeentrack/repairs.py b/homeassistant/components/seventeentrack/repairs.py new file mode 100644 index 00000000000..71616e98506 --- /dev/null +++ b/homeassistant/components/seventeentrack/repairs.py @@ -0,0 +1,49 @@ +"""Repairs for the SeventeenTrack integration.""" + +import voluptuous as vol + +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import DEPRECATED_KEY + + +class SensorDeprecationRepairFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, entry: ConfigEntry) -> None: + """Create flow.""" + self.entry = entry + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + data = {**self.entry.data, DEPRECATED_KEY: True} + self.hass.config_entries.async_update_entry(self.entry, data=data) + return self.async_create_entry(data={}) + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, issue_id: str, data: dict +) -> RepairsFlow: + """Create flow.""" + if issue_id.startswith("deprecate_sensor_"): + entry = hass.config_entries.async_get_entry(data["entry_id"]) + assert entry + return SensorDeprecationRepairFlow(entry) + return ConfirmRepairFlow() diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index fa6f283427d..3122065adae 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -20,7 +20,11 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + issue_registry as ir, +) from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -41,6 +45,7 @@ from .const import ( ATTRIBUTION, CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, + DEPRECATED_KEY, DOMAIN, LOGGER, NOTIFICATION_DELIVERED_MESSAGE, @@ -113,8 +118,12 @@ async def async_setup_entry( coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][config_entry.entry_id] previous_tracking_numbers: set[str] = set() + # This has been deprecated in 2024.8, will be removed in 2025.2 @callback def _async_create_remove_entities(): + if config_entry.data.get(DEPRECATED_KEY): + remove_packages(hass, coordinator.account_id, previous_tracking_numbers) + return live_tracking_numbers = set(coordinator.data.live_packages.keys()) new_tracking_numbers = live_tracking_numbers - previous_tracking_numbers @@ -157,11 +166,12 @@ async def async_setup_entry( for status, summary_data in coordinator.data.summary.items() ) - _async_create_remove_entities() - - config_entry.async_on_unload( - coordinator.async_add_listener(_async_create_remove_entities) - ) + if not config_entry.data.get(DEPRECATED_KEY): + deprecate_sensor_issue(hass, config_entry.entry_id) + _async_create_remove_entities() + config_entry.async_on_unload( + coordinator.async_add_listener(_async_create_remove_entities) + ) class SeventeenTrackSensor(CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity): @@ -206,6 +216,7 @@ class SeventeenTrackSummarySensor(SeventeenTrackSensor): """Return the state of the sensor.""" return self.coordinator.data.summary[self._status]["quantity"] + # This has been deprecated in 2024.8, will be removed in 2025.2 @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" @@ -225,6 +236,7 @@ class SeventeenTrackSummarySensor(SeventeenTrackSensor): } +# The dynamic package sensors have been replaced by the seventeentrack.get_packages service class SeventeenTrackPackageSensor(SeventeenTrackSensor): """Define an individual package sensor.""" @@ -298,3 +310,20 @@ def notify_delivered(hass: HomeAssistant, friendly_name: str, tracking_number: s persistent_notification.create( hass, message, title=title, notification_id=notification_id ) + + +@callback +def deprecate_sensor_issue(hass: HomeAssistant, entry_id: str) -> None: + """Ensure an issue is registered.""" + ir.async_create_issue( + hass, + DOMAIN, + f"deprecate_sensor_{entry_id}", + breaks_in_ha_version="2025.2.0", + issue_domain=DOMAIN, + is_fixable=True, + is_persistent=True, + translation_key="deprecate_sensor", + severity=ir.IssueSeverity.WARNING, + data={"entry_id": entry_id}, + ) diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index cad04fca8b9..d166fb32673 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -45,6 +45,17 @@ "deprecated_yaml_import_issue_invalid_auth": { "title": "The 17Track YAML configuration import request failed due to invalid authentication", "description": "Configuring 17Track using YAML is being removed but there were invalid credentials provided while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your 17Track credentials are correct and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the 17Track configuration from your YAML configuration entirely, restart Home Assistant, and add the 17Track integration manually." + }, + "deprecate_sensor": { + "title": "17Track package sensors are being deprecated", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::seventeentrack::issues::deprecate_sensor::title%]", + "description": "17Track package sensors are deprecated and will be removed.\nPlease update your automations and scripts to get data using the `seventeentrack.get_packages` service call." + } + } + } } }, "entity": { diff --git a/tests/components/seventeentrack/snapshots/test_services.ambr b/tests/components/seventeentrack/snapshots/test_services.ambr index 185a1d44fe0..202c5a3d667 100644 --- a/tests/components/seventeentrack/snapshots/test_services.ambr +++ b/tests/components/seventeentrack/snapshots/test_services.ambr @@ -3,27 +3,39 @@ dict({ 'packages': list([ dict({ + 'destination_country': 'Belgium', 'friendly_name': 'friendly name 3', 'info_text': 'info text 1', 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', 'status': 'Expired', 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_info_language': 'Unknown', 'tracking_number': '123', }), dict({ + 'destination_country': 'Belgium', 'friendly_name': 'friendly name 1', 'info_text': 'info text 1', 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', 'status': 'In Transit', 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_info_language': 'Unknown', 'tracking_number': '456', }), dict({ + 'destination_country': 'Belgium', 'friendly_name': 'friendly name 2', 'info_text': 'info text 1', 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', 'status': 'Delivered', 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_info_language': 'Unknown', 'tracking_number': '789', }), ]), @@ -33,19 +45,27 @@ dict({ 'packages': list([ dict({ + 'destination_country': 'Belgium', 'friendly_name': 'friendly name 1', 'info_text': 'info text 1', 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', 'status': 'In Transit', 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_info_language': 'Unknown', 'tracking_number': '456', }), dict({ + 'destination_country': 'Belgium', 'friendly_name': 'friendly name 2', 'info_text': 'info text 1', 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', 'status': 'Delivered', 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_info_language': 'Unknown', 'tracking_number': '789', }), ]), diff --git a/tests/components/seventeentrack/test_repairs.py b/tests/components/seventeentrack/test_repairs.py new file mode 100644 index 00000000000..0f697c1ad49 --- /dev/null +++ b/tests/components/seventeentrack/test_repairs.py @@ -0,0 +1,95 @@ +"""Tests for the seventeentrack repair flow.""" + +from http import HTTPStatus +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.components.repairs.websocket_api import RepairsFlowIndexView +from homeassistant.components.seventeentrack import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from . import goto_future, init_integration +from .conftest import DEFAULT_SUMMARY_LENGTH, get_package + +from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator + + +async def test_repair( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + issue_registry: ir.IssueRegistry, + hass_client: ClientSessionGenerator, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure everything starts correctly.""" + await init_integration(hass, mock_config_entry) # 2 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + assert len(issue_registry.issues) == 1 + + package = get_package() + mock_seventeentrack.return_value.profile.packages.return_value = [package] + await goto_future(hass, freezer) + + assert hass.states.get("sensor.17track_package_friendly_name_1") + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 + + assert "deprecated" not in mock_config_entry.data + + repair_issue = issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"deprecate_sensor_{mock_config_entry.entry_id}" + ) + + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + + client = await hass_client() + + resp = await client.post( + RepairsFlowIndexView.url, + json={"handler": DOMAIN, "issue_id": repair_issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "confirm", + "data_schema": [], + "errors": None, + "description_placeholders": None, + "last_step": None, + "preview": None, + } + + resp = await client.post(RepairsFlowIndexView.url + f"/{flow_id}") + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "create_entry", + "handler": DOMAIN, + "flow_id": flow_id, + "description": None, + "description_placeholders": None, + } + + assert mock_config_entry.data["deprecated"] + + repair_issue = issue_registry.async_get_issue( + domain=DOMAIN, issue_id="deprecate_sensor" + ) + + assert repair_issue is None + + await goto_future(hass, freezer) + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 75cc6435073..0675f457795 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -317,4 +317,4 @@ async def test_full_valid_platform_config( assert await async_setup_component(hass, "sensor", VALID_PLATFORM_CONFIG_FULL) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys()) - assert len(issue_registry.issues) == 1 + assert len(issue_registry.issues) == 2