""" Support for package tracking sensors from 17track.net. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.seventeentrack/ """ 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 REQUIREMENTS = ['py17track==2.1.1'] _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) NOTIFICATION_DELIVERED_ID_SCAFFOLD = 'package_delivered_{0}' NOTIFICATION_DELIVERED_TITLE = 'Package Delivered' NOTIFICATION_DELIVERED_URL_SCAFFOLD = 'https://t.17track.net/track#nums={0}' 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() sensors = [] for status, quantity in data.summary.items(): sensors.append(SeventeenTrackSummarySensor(data, status, quantity)) for package in data.packages: sensors.append(SeventeenTrackPackageSensor(data, package)) async_add_entities(sensors, True) 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 'Seventeentrack Packages {0}'.format(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: 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 @property def available(self): """Return whether the entity is available.""" return bool([ p for p in self._data.packages if p.tracking_number == self._tracking_number ]) @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 'Seventeentrack Package: {0}'.format(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 'package_{0}_{1}'.format( self._data.account_id, self._tracking_number) async def async_update(self): """Update the sensor.""" await self._data.async_update() if not self._data.packages: return try: package = next(( p for p in self._data.packages if p.tracking_number == self._tracking_number)) except StopIteration: # If the package no longer exists in the data, log a message and # delete this entity: _LOGGER.info( 'Deleting entity for stale package: %s', self._tracking_number) self.hass.async_create_task(self.async_remove()) return # If the user has elected to not see delivered packages and one gets # delivered, post a notification and delete the entity: if package.status == VALUE_DELIVERED and not self._data.show_delivered: _LOGGER.info('Package delivered: %s', self._tracking_number) self.hass.components.persistent_notification.create( 'Package Delivered: {0}
' 'Visit 17.track for more infomation: {1}' ''.format( self._tracking_number, NOTIFICATION_DELIVERED_URL_SCAFFOLD.format( self._tracking_number)), title=NOTIFICATION_DELIVERED_TITLE, notification_id=NOTIFICATION_DELIVERED_ID_SCAFFOLD.format( self._tracking_number)) self.hass.async_create_task(self.async_remove()) return self._attrs.update({ ATTR_INFO_TEXT: package.info_text, ATTR_LOCATION: package.location, }) self._state = package.status 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) 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) if not self.show_delivered: packages = [p for p in packages if p.status != VALUE_DELIVERED] # Add new packages: to_add = set(packages) - set(self.packages) if self.packages and to_add: self._async_add_entities([ SeventeenTrackPackageSensor(self, package) for package in to_add ], True) self.packages = packages except SeventeenTrackError as err: _LOGGER.error('There was an error retrieving packages: %s', err) self.packages = [] try: self.summary = await self._client.profile.summary( show_archived=self._show_archived) _LOGGER.debug('New summary data received: %s', self.summary) except SeventeenTrackError as err: _LOGGER.error('There was an error retrieving the summary: %s', err) self.summary = {}