Skip to content

Commit

Permalink
Add support for 17track.net package sensors (#18038)
Browse files Browse the repository at this point in the history
* Add support for 17track.net package sensors

* Updated CODEOWNERS

* Addressing comments

* Fixed requirements

* Member comments

* Revert "Member comments"

This reverts commit 61a19d7.

* Member comments

* Member comments
  • Loading branch information
bachya authored Nov 8, 2018
1 parent e2fca06 commit 954191c
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ omit =
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/lg_soundbar.py
homeassistant/components/media_player/lg_soundbar.py
homeassistant/components/media_player/liveboxplaytv.py
homeassistant/components/media_player/mediaroom.py
homeassistant/components/media_player/mpchc.py
Expand Down Expand Up @@ -776,6 +776,7 @@ omit =
homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/seventeentrack.py
homeassistant/components/sensor/sht31.py
homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/sigfox.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ homeassistant/components/sensor/pvoutput.py @fabaff
homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/scrape.py @fabaff
homeassistant/components/sensor/serial.py @fabaff
homeassistant/components/sensor/seventeentrack.py @bachya
homeassistant/components/sensor/shodan.py @fabaff
homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes
Expand Down
288 changes: 288 additions & 0 deletions homeassistant/components/sensor/seventeentrack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
"""
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.0.2']
_LOGGER = logging.getLogger(__name__)

ATTR_DESTINATION_COUNTRY = 'destination_country'
ATTR_INFO_TEXT = 'info_text'
ATTR_ORIGIN_COUNTRY = 'origin_country'
ATTR_PACKAGE_TYPE = 'package_type'
ATTR_TRACKING_INFO_LANGUAGE = 'tracking_info_language'

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 '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()

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,
}
self._data = data
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."""
return self._tracking_number

@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 = {}
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,9 @@ py-melissa-climate==2.0.0
# homeassistant.components.camera.synology
py-synology==0.2.0

# homeassistant.components.sensor.seventeentrack
py17track==2.0.2

# homeassistant.components.hdmi_cec
pyCEC==0.4.13

Expand Down

0 comments on commit 954191c

Please sign in to comment.