"""Support for package tracking sensors from 17track.net.""" import logging from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, ) from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, slugify _LOGGER = logging.getLogger(__name__) ATTR_DESTINATION_COUNTRY = "destination_country" ATTR_FRIENDLY_NAME = "friendly_name" ATTR_INFO_TEXT = "info_text" ATTR_ORIGIN_COUNTRY = "origin_country" ATTR_PACKAGES = "packages" ATTR_PACKAGE_TYPE = "package_type" ATTR_STATUS = "status" ATTR_TRACKING_INFO_LANGUAGE = "tracking_info_language" ATTR_TRACKING_NUMBER = "tracking_number" CONF_SHOW_ARCHIVED = "show_archived" CONF_SHOW_DELIVERED = "show_delivered" DATA_PACKAGES = "package_data" DATA_SUMMARY = "summary_data" DEFAULT_ATTRIBUTION = "Data provided by 17track.net" DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) UNIQUE_ID_TEMPLATE = "package_{0}_{1}" ENTITY_ID_TEMPLATE = "sensor.seventeentrack_package_{0}" NOTIFICATION_DELIVERED_ID = "package_delivered_{0}" NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered" NOTIFICATION_DELIVERED_MESSAGE = ( "Package Delivered: {0}
" + "Visit 17.track for more information: " "https://t.17track.net/track#nums={1}" ) VALUE_DELIVERED = "Delivered" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SHOW_ARCHIVED, default=False): cv.boolean, vol.Optional(CONF_SHOW_DELIVERED, default=False): cv.boolean, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" from py17track import Client from py17track.errors import SeventeenTrackError websession = aiohttp_client.async_get_clientsession(hass) client = Client(websession) try: login_result = await client.profile.login( config[CONF_USERNAME], config[CONF_PASSWORD] ) if not login_result: _LOGGER.error("Invalid username and password provided") return except SeventeenTrackError as err: _LOGGER.error("There was an error while logging in: %s", err) return scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) data = SeventeenTrackData( client, async_add_entities, scan_interval, config[CONF_SHOW_ARCHIVED], config[CONF_SHOW_DELIVERED], ) await data.async_update() class SeventeenTrackSummarySensor(Entity): """Define a summary sensor.""" def __init__(self, data, status, initial_state): """Initialize.""" self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._data = data self._state = initial_state self._status = status @property def available(self): """Return whether the entity is available.""" return self._state is not None @property def device_state_attributes(self): """Return the device state attributes.""" return self._attrs @property def icon(self): """Return the icon.""" return "mdi:package" @property def name(self): """Return the name.""" return f"Seventeentrack Packages {self._status}" @property def state(self): """Return the state.""" return self._state @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" return "summary_{0}_{1}".format(self._data.account_id, slugify(self._status)) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return "packages" async def async_update(self): """Update the sensor.""" await self._data.async_update() package_data = [] for package in self._data.packages.values(): if package.status != self._status: continue package_data.append( { ATTR_FRIENDLY_NAME: package.friendly_name, ATTR_INFO_TEXT: package.info_text, ATTR_STATUS: package.status, ATTR_TRACKING_NUMBER: package.tracking_number, } ) if package_data: self._attrs[ATTR_PACKAGES] = package_data self._state = self._data.summary.get(self._status) class SeventeenTrackPackageSensor(Entity): """Define an individual package sensor.""" def __init__(self, data, package): """Initialize.""" self._attrs = { ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_DESTINATION_COUNTRY: package.destination_country, ATTR_INFO_TEXT: package.info_text, ATTR_LOCATION: package.location, 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, } self._data = data self._friendly_name = package.friendly_name self._state = package.status self._tracking_number = package.tracking_number self.entity_id = ENTITY_ID_TEMPLATE.format(self._tracking_number) @property def available(self): """Return whether the entity is available.""" return self._data.packages.get(self._tracking_number) is not None @property def device_state_attributes(self): """Return the device state attributes.""" return self._attrs @property def icon(self): """Return the icon.""" return "mdi:package" @property def name(self): """Return the name.""" name = self._friendly_name if not name: name = self._tracking_number return f"Seventeentrack Package: {name}" @property def state(self): """Return the state.""" return self._state @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" return UNIQUE_ID_TEMPLATE.format(self._data.account_id, self._tracking_number) async def async_update(self): """Update the sensor.""" await self._data.async_update() if not self.available: self.hass.async_create_task(self._remove()) return package = self._data.packages.get(self._tracking_number, None) # If the user has elected to not see delivered packages and one gets # delivered, post a notification: if package.status == VALUE_DELIVERED and not self._data.show_delivered: self._notify_delivered() self.hass.async_create_task(self._remove()) return self._attrs.update( {ATTR_INFO_TEXT: package.info_text, ATTR_LOCATION: package.location} ) self._state = package.status self._friendly_name = package.friendly_name async def _remove(self): """Remove entity itself.""" await self.async_remove() reg = await self.hass.helpers.entity_registry.async_get_registry() entity_id = reg.async_get_entity_id( "sensor", "seventeentrack", UNIQUE_ID_TEMPLATE.format(self._data.account_id, self._tracking_number), ) if entity_id: reg.async_remove(entity_id) def _notify_delivered(self): """Notify when package is delivered.""" _LOGGER.info("Package delivered: %s", self._tracking_number) identification = ( self._friendly_name if self._friendly_name else self._tracking_number ) message = NOTIFICATION_DELIVERED_MESSAGE.format( self._tracking_number, identification ) title = NOTIFICATION_DELIVERED_TITLE.format(identification) notification_id = NOTIFICATION_DELIVERED_TITLE.format(self._tracking_number) self.hass.components.persistent_notification.create( message, title=title, notification_id=notification_id ) class SeventeenTrackData: """Define a data handler for 17track.net.""" def __init__( self, client, async_add_entities, scan_interval, show_archived, show_delivered ): """Initialize.""" self._async_add_entities = async_add_entities self._client = client self._scan_interval = scan_interval self._show_archived = show_archived self.account_id = client.profile.account_id self.packages = {} self.show_delivered = show_delivered self.summary = {} self.async_update = Throttle(self._scan_interval)(self._async_update) self.first_update = True async def _async_update(self): """Get updated data from 17track.net.""" from py17track.errors import SeventeenTrackError try: packages = await self._client.profile.packages( show_archived=self._show_archived ) _LOGGER.debug("New package data received: %s", packages) new_packages = {p.tracking_number: p for p in packages} to_add = set(new_packages) - set(self.packages) _LOGGER.debug("Will add new tracking numbers: %s", to_add) if to_add: self._async_add_entities( [ SeventeenTrackPackageSensor(self, new_packages[tracking_number]) for tracking_number in to_add ], True, ) self.packages = new_packages except SeventeenTrackError as err: _LOGGER.error("There was an error retrieving packages: %s", err) try: self.summary = await self._client.profile.summary( show_archived=self._show_archived ) _LOGGER.debug("New summary data received: %s", self.summary) # creating summary sensors on first update if self.first_update: self.first_update = False self._async_add_entities( [ SeventeenTrackSummarySensor(self, status, quantity) for status, quantity in self.summary.items() ], True, ) except SeventeenTrackError as err: _LOGGER.error("There was an error retrieving the summary: %s", err) self.summary = {}