diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 40c9c8d58d1..6d89c4c0a76 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -1,9 +1,13 @@ """The seventeentrack component.""" +from typing import Final + from py17track import Client as SeventeenTrackClient from py17track.errors import SeventeenTrackError +from py17track.package import PACKAGE_STATUS_MAP +import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_LOCATION, @@ -17,8 +21,8 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError +from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -39,6 +43,27 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +SERVICE_SCHEMA: Final = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + vol.Optional(ATTR_PACKAGE_STATE): selector.SelectSelector( + selector.SelectSelectorConfig( + multiple=True, + options=[ + value.lower().replace(" ", "_") + for value in PACKAGE_STATUS_MAP.values() + ], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key=ATTR_PACKAGE_STATE, + ) + ), + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the 17Track component.""" @@ -47,6 +72,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Get packages from 17Track.""" config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID] package_states = call.data.get(ATTR_PACKAGE_STATE, []) + + entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id) + + if not entry: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + translation_placeholders={ + "config_entry_id": config_entry_id, + }, + ) + if entry.state != ConfigEntryState.LOADED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="unloaded_config_entry", + translation_placeholders={ + "config_entry_id": entry.title, + }, + ) + seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][ config_entry_id ] @@ -75,6 +120,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_GET_PACKAGES, get_packages, + schema=SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) return True diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index 626af29e856..cad04fca8b9 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -18,6 +18,14 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } }, + "exceptions": { + "invalid_config_entry": { + "message": "Invalid config entry provided. Got {config_entry_id}" + }, + "unloaded_config_entry": { + "message": "Invalid config entry provided. {config_entry_id} is not loaded." + } + }, "options": { "step": { "init": { diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py index 148286d66d4..4347189a5c0 100644 --- a/tests/components/seventeentrack/test_services.py +++ b/tests/components/seventeentrack/test_services.py @@ -2,10 +2,14 @@ from unittest.mock import AsyncMock +import pytest from syrupy import SnapshotAssertion from homeassistant.components.seventeentrack import DOMAIN, SERVICE_GET_PACKAGES -from homeassistant.core import HomeAssistant, SupportsResponse +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from . import init_integration from .conftest import get_package @@ -30,7 +34,7 @@ async def test_get_packages_from_list( "package_state": ["in_transit", "delivered"], }, blocking=True, - return_response=SupportsResponse.ONLY, + return_response=True, ) assert service_response == snapshot @@ -52,12 +56,67 @@ async def test_get_all_packages( "config_entry_id": mock_config_entry.entry_id, }, blocking=True, - return_response=SupportsResponse.ONLY, + return_response=True, ) assert service_response == snapshot +async def test_service_called_with_unloaded_entry( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test service call with not ready config entry.""" + await init_integration(hass, mock_config_entry) + mock_config_entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": mock_config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + + +async def test_service_called_with_non_17track_device( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + device_registry: dr.DeviceRegistry, +) -> None: + """Test service calls with non 17Track device.""" + await init_integration(hass, mock_config_entry) + + other_domain = "Not17Track" + other_config_id = "555" + other_mock_config_entry = MockConfigEntry( + title="Not 17Track", domain=other_domain, entry_id=other_config_id + ) + other_mock_config_entry.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=other_config_id, + identifiers={(other_domain, "1")}, + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": device_entry.id, + }, + blocking=True, + return_response=True, + ) + + async def _mock_packages(mock_seventeentrack): package1 = get_package(status=10) package2 = get_package(