313 lines
10 KiB
Python
313 lines
10 KiB
Python
|
"""
|
||
|
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.2.2']
|
||
|
_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}<br />'
|
||
|
'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 = {}
|