Add service to 17track to archive package (#123493)

* Add service archive package

* Update homeassistant/components/seventeentrack/icons.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* CR fix in tests

* CR fix in services.py

* string references

* extract constant keys

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Shai Ungar 2024-09-04 22:56:11 +03:00 committed by GitHub
parent 1f59bd9f92
commit adda02b6b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 229 additions and 121 deletions

View file

@ -1,136 +1,30 @@
"""The seventeentrack component.""" """The seventeentrack component."""
from typing import Final
from pyseventeentrack import Client as SeventeenTrackClient from pyseventeentrack import Client as SeventeenTrackClient
from pyseventeentrack.errors import SeventeenTrackError from pyseventeentrack.errors import SeventeenTrackError
from pyseventeentrack.package import PACKAGE_STATUS_MAP
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
ATTR_FRIENDLY_NAME, from homeassistant.core import HomeAssistant
ATTR_LOCATION, from homeassistant.exceptions import ConfigEntryNotReady
CONF_PASSWORD, from homeassistant.helpers import config_validation as cv
CONF_USERNAME,
Platform,
)
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from .const import ( from .const import DOMAIN
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,
)
from .coordinator import SeventeenTrackCoordinator from .coordinator import SeventeenTrackCoordinator
from .services import setup_services
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) 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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the 17Track component.""" """Set up the 17Track component."""
async def get_packages(call: ServiceCall) -> ServiceResponse: setup_services(hass)
"""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
]
live_packages = sorted(
await seventeen_coordinator.client.profile.packages(
show_archived=seventeen_coordinator.show_archived
)
)
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,
ATTR_TIMESTAMP: package.timestamp,
ATTR_INFO_TEXT: package.info_text,
ATTR_FRIENDLY_NAME: package.friendly_name,
}
for package in live_packages
if slugify(package.status) in package_states or package_states == []
]
}
hass.services.async_register(
DOMAIN,
SERVICE_GET_PACKAGES,
get_packages,
schema=SERVICE_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
return True return True

View file

@ -42,8 +42,11 @@ NOTIFICATION_DELIVERED_MESSAGE = (
VALUE_DELIVERED = "Delivered" VALUE_DELIVERED = "Delivered"
SERVICE_GET_PACKAGES = "get_packages" SERVICE_GET_PACKAGES = "get_packages"
SERVICE_ARCHIVE_PACKAGE = "archive_package"
ATTR_PACKAGE_STATE = "package_state" ATTR_PACKAGE_STATE = "package_state"
ATTR_PACKAGE_TRACKING_NUMBER = "package_tracking_number"
ATTR_CONFIG_ENTRY_ID = "config_entry_id" ATTR_CONFIG_ENTRY_ID = "config_entry_id"
DEPRECATED_KEY = "deprecated" DEPRECATED_KEY = "deprecated"

View file

@ -30,6 +30,9 @@
"services": { "services": {
"get_packages": { "get_packages": {
"service": "mdi:package" "service": "mdi:package"
},
"archive_package": {
"service": "mdi:archive"
} }
} }
} }

View file

@ -0,0 +1,145 @@
"""Services for the seventeentrack integration."""
from typing import Final
from pyseventeentrack.package import PACKAGE_STATUS_MAP
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_LOCATION
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.util import slugify
from . import SeventeenTrackCoordinator
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_DESTINATION_COUNTRY,
ATTR_INFO_TEXT,
ATTR_ORIGIN_COUNTRY,
ATTR_PACKAGE_STATE,
ATTR_PACKAGE_TRACKING_NUMBER,
ATTR_PACKAGE_TYPE,
ATTR_STATUS,
ATTR_TIMESTAMP,
ATTR_TRACKING_INFO_LANGUAGE,
ATTR_TRACKING_NUMBER,
DOMAIN,
SERVICE_ARCHIVE_PACKAGE,
SERVICE_GET_PACKAGES,
)
SERVICE_ADD_PACKAGES_SCHEMA: Final = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
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,
)
),
}
)
SERVICE_ARCHIVE_PACKAGE_SCHEMA: Final = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_PACKAGE_TRACKING_NUMBER): cv.string,
}
)
def setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the seventeentrack integration."""
async def get_packages(call: ServiceCall) -> ServiceResponse:
"""Get packages from 17Track."""
config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
package_states = call.data.get(ATTR_PACKAGE_STATE, [])
await _validate_service(config_entry_id)
seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][
config_entry_id
]
live_packages = sorted(
await seventeen_coordinator.client.profile.packages(
show_archived=seventeen_coordinator.show_archived
)
)
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,
ATTR_TIMESTAMP: package.timestamp,
ATTR_INFO_TEXT: package.info_text,
ATTR_FRIENDLY_NAME: package.friendly_name,
}
for package in live_packages
if slugify(package.status) in package_states or package_states == []
]
}
async def archive_package(call: ServiceCall) -> None:
config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
tracking_number = call.data[ATTR_PACKAGE_TRACKING_NUMBER]
await _validate_service(config_entry_id)
seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][
config_entry_id
]
await seventeen_coordinator.client.profile.archive_package(tracking_number)
async def _validate_service(config_entry_id):
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,
},
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_PACKAGES,
get_packages,
schema=SERVICE_ADD_PACKAGES_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_ARCHIVE_PACKAGE,
archive_package,
schema=SERVICE_ARCHIVE_PACKAGE_SCHEMA,
)

View file

@ -18,3 +18,14 @@ get_packages:
selector: selector:
config_entry: config_entry:
integration: seventeentrack integration: seventeentrack
archive_package:
fields:
package_tracking_number:
required: true
selector:
text:
config_entry_id:
required: true
selector:
config_entry:
integration: seventeentrack

View file

@ -100,6 +100,20 @@
"description": "The packages will be retrieved for the selected service." "description": "The packages will be retrieved for the selected service."
} }
} }
},
"archive_package": {
"name": "Archive package",
"description": "Archive a package",
"fields": {
"package_tracking_number": {
"name": "Package tracking number",
"description": "The package will be archived for the specified tracking number."
},
"config_entry_id": {
"name": "[%key:component::seventeentrack::services::get_packages::fields::config_entry_id::name%]",
"description": "The package will be archived for the selected service."
}
}
} }
}, },
"selector": { "selector": {

View file

@ -40,6 +40,11 @@ NEW_SUMMARY_DATA = {
"Returned": 1, "Returned": 1,
} }
ARCHIVE_PACKAGE_NUMBER = "123"
CONFIG_ENTRY_ID_KEY = "config_entry_id"
PACKAGE_TRACKING_NUMBER_KEY = "package_tracking_number"
PACKAGE_STATE_KEY = "package_state"
VALID_CONFIG = { VALID_CONFIG = {
CONF_USERNAME: "test", CONF_USERNAME: "test",
CONF_PASSWORD: "test", CONF_PASSWORD: "test",

View file

@ -5,14 +5,24 @@ from unittest.mock import AsyncMock
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.seventeentrack import DOMAIN, SERVICE_GET_PACKAGES from homeassistant.components.seventeentrack import DOMAIN
from homeassistant.components.seventeentrack.const import (
SERVICE_ARCHIVE_PACKAGE,
SERVICE_GET_PACKAGES,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from . import init_integration from . import init_integration
from .conftest import get_package from .conftest import (
ARCHIVE_PACKAGE_NUMBER,
CONFIG_ENTRY_ID_KEY,
PACKAGE_STATE_KEY,
PACKAGE_TRACKING_NUMBER_KEY,
get_package,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -30,8 +40,8 @@ async def test_get_packages_from_list(
DOMAIN, DOMAIN,
SERVICE_GET_PACKAGES, SERVICE_GET_PACKAGES,
{ {
"config_entry_id": mock_config_entry.entry_id, CONFIG_ENTRY_ID_KEY: mock_config_entry.entry_id,
"package_state": ["in_transit", "delivered"], PACKAGE_STATE_KEY: ["in_transit", "delivered"],
}, },
blocking=True, blocking=True,
return_response=True, return_response=True,
@ -53,7 +63,7 @@ async def test_get_all_packages(
DOMAIN, DOMAIN,
SERVICE_GET_PACKAGES, SERVICE_GET_PACKAGES,
{ {
"config_entry_id": mock_config_entry.entry_id, CONFIG_ENTRY_ID_KEY: mock_config_entry.entry_id,
}, },
blocking=True, blocking=True,
return_response=True, return_response=True,
@ -76,7 +86,7 @@ async def test_service_called_with_unloaded_entry(
DOMAIN, DOMAIN,
SERVICE_GET_PACKAGES, SERVICE_GET_PACKAGES,
{ {
"config_entry_id": mock_config_entry.entry_id, CONFIG_ENTRY_ID_KEY: mock_config_entry.entry_id,
}, },
blocking=True, blocking=True,
return_response=True, return_response=True,
@ -110,13 +120,36 @@ async def test_service_called_with_non_17track_device(
DOMAIN, DOMAIN,
SERVICE_GET_PACKAGES, SERVICE_GET_PACKAGES,
{ {
"config_entry_id": device_entry.id, CONFIG_ENTRY_ID_KEY: device_entry.id,
}, },
blocking=True, blocking=True,
return_response=True, return_response=True,
) )
async def test_archive_package(
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Ensure service archives package."""
await _mock_packages(mock_seventeentrack)
await init_integration(hass, mock_config_entry)
await hass.services.async_call(
DOMAIN,
SERVICE_ARCHIVE_PACKAGE,
{
CONFIG_ENTRY_ID_KEY: mock_config_entry.entry_id,
PACKAGE_TRACKING_NUMBER_KEY: ARCHIVE_PACKAGE_NUMBER,
},
blocking=True,
)
mock_seventeentrack.return_value.profile.archive_package.assert_called_once_with(
ARCHIVE_PACKAGE_NUMBER
)
async def _mock_packages(mock_seventeentrack): async def _mock_packages(mock_seventeentrack):
package1 = get_package(status=10) package1 = get_package(status=10)
package2 = get_package( package2 = get_package(