From 1219ca3c3bc083c8f919c4db7eb3670686e52861 Mon Sep 17 00:00:00 2001 From: Thom Troy Date: Fri, 13 Jan 2017 17:15:46 +0000 Subject: [PATCH] [sensor] Add Dublin bus RTPI sensor (#5257) --- .coveragerc | 1 + .../components/sensor/dublin_bus_transport.py | 184 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 homeassistant/components/sensor/dublin_bus_transport.py diff --git a/.coveragerc b/.coveragerc index bc67b24d907..aa5921526de 100644 --- a/.coveragerc +++ b/.coveragerc @@ -266,6 +266,7 @@ omit = homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/bom.py homeassistant/components/sensor/broadlink.py + homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cups.py diff --git a/homeassistant/components/sensor/dublin_bus_transport.py b/homeassistant/components/sensor/dublin_bus_transport.py new file mode 100644 index 00000000000..10d2c2b39f0 --- /dev/null +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -0,0 +1,184 @@ +"""Support for Dublin RTPI information from data.dublinked.ie. + +For more info on the API see : +https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus-bus-eireann-luas-and-irish-rail/resource/4b9f2c4f-6bf5-4958-a43a-f12dab04cf61 + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.dublin_public_transport/ +""" +import logging +from datetime import timedelta, datetime + +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation' + +ATTR_STOP_ID = "Stop ID" +ATTR_ROUTE = "Route" +ATTR_DUE_IN = "Due in" +ATTR_DUE_AT = "Due at" +ATTR_NEXT_UP = "Later Bus" + +CONF_ATTRIBUTION = "Data provided by data.dublinked.ie" +CONF_STOP_ID = 'stopid' +CONF_ROUTE = 'route' + +DEFAULT_NAME = 'Next Bus' +ICON = 'mdi:bus' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +TIME_STR_FORMAT = "%H:%M" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=""): cv.string, +}) + + +def due_in_minutes(timestamp): + """Get the time in minutes from a timestamp. + + The timestamp should be in the format day/month/year hour/minute/second + """ + diff = datetime.strptime( + timestamp, "%d/%m/%Y %H:%M:%S") - dt_util.now().replace(tzinfo=None) + + return str(int(diff.total_seconds() / 60)) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Get the Dublin public transport sensor.""" + name = config.get(CONF_NAME) + stop = config.get(CONF_STOP_ID) + route = config.get(CONF_ROUTE) + + data = PublicTransportData(stop, route) + add_devices([DublinPublicTransportSensor(data, stop, route, name)]) + + +class DublinPublicTransportSensor(Entity): + """Implementation of an Dublin public transport sensor.""" + + def __init__(self, data, stop, route, name): + """Initialize the sensor.""" + self.data = data + self._name = name + self._stop = stop + self._route = route + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._times is not None: + next_up = "None" + if len(self._times) > 1: + next_up = self._times[1][ATTR_ROUTE] + " in " + next_up += self._times[1][ATTR_DUE_IN] + + return { + ATTR_DUE_IN: self._times[0][ATTR_DUE_IN], + ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], + ATTR_STOP_ID: self._stop, + ATTR_ROUTE: self._times[0][ATTR_ROUTE], + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_NEXT_UP: next_up + } + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return "min" + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data from opendata.ch and update the states.""" + self.data.update() + self._times = self.data.info + try: + self._state = self._times[0][ATTR_DUE_IN] + except TypeError: + pass + + +class PublicTransportData(object): + """The Class for handling the data retrieval.""" + + def __init__(self, stop, route): + """Initialize the data object.""" + self.stop = stop + self.route = route + self.info = [{ATTR_DUE_AT: 'n/a', + ATTR_ROUTE: self.route, + ATTR_DUE_IN: 'n/a'}] + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from opendata.ch.""" + params = {} + params['stopid'] = self.stop + + if len(self.route) > 0: + params['routeid'] = self.route + + params['maxresults'] = 2 + params['format'] = 'json' + + response = requests.get( + _RESOURCE, + params, + timeout=10) + + if response.status_code != 200: + self.info = [{ATTR_DUE_AT: 'n/a', + ATTR_ROUTE: self.route, + ATTR_DUE_IN: 'n/a'}] + return + + result = response.json() + + if str(result['errorcode']) != '0': + self.info = [{ATTR_DUE_AT: 'n/a', + ATTR_ROUTE: self.route, + ATTR_DUE_IN: 'n/a'}] + return + + self.info = [] + for item in result['results']: + due_at = item.get('departuredatetime') + route = item.get('route') + if due_at is not None and route is not None: + bus_data = {ATTR_DUE_AT: due_at, + ATTR_ROUTE: route, + ATTR_DUE_IN: + due_in_minutes(due_at)} + self.info.append(bus_data) + + if len(self.info) == 0: + self.info = [{ATTR_DUE_AT: 'n/a', + ATTR_ROUTE: self.route, + ATTR_DUE_IN: 'n/a'}]