diff --git a/.coveragerc b/.coveragerc index eee5bcf8c49..8d037f0c2e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 @@ -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 diff --git a/CODEOWNERS b/CODEOWNERS index 0d498d89cae..9fc93b958e8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/sensor/seventeentrack.py b/homeassistant/components/sensor/seventeentrack.py new file mode 100644 index 00000000000..7ad0e453760 --- /dev/null +++ b/homeassistant/components/sensor/seventeentrack.py @@ -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}
' + '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 = {} diff --git a/requirements_all.txt b/requirements_all.txt index e92e1d1e143..b8156d137a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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