Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NMBS (Belgian railway) sensor platform #18610

Merged
merged 23 commits into from Dec 14, 2018
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions .coveragerc
Expand Up @@ -731,7 +731,6 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py
Expand All @@ -757,9 +756,10 @@ omit =
homeassistant/components/sensor/mvglive.py
homeassistant/components/sensor/nederlandse_spoorwegen.py
homeassistant/components/sensor/netatmo_public.py
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/netdata_public.py
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nmbs.py
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
homeassistant/components/sensor/noaa_tides.py
homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py
Expand Down Expand Up @@ -790,8 +790,8 @@ omit =
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/sht31.py
homeassistant/components/sensor/sigfox.py
homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py
Expand All @@ -802,14 +802,14 @@ omit =
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py
homeassistant/components/sensor/srp_energy.py
homeassistant/components/sensor/starlingbank.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/syncthru.py
homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/srp_energy.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sytadin.py
homeassistant/components/sensor/tank_utility.py
Expand All @@ -824,6 +824,7 @@ omit =
homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py
Expand Down
207 changes: 207 additions & 0 deletions homeassistant/components/sensor/nmbs.py
@@ -0,0 +1,207 @@
"""
Get ride details and liveboard details for NMBS (Belgian railway).
For more details about this platform, please refer to the documentation at
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
https://home-assistant.io/components/sensor.nmbs/
"""
import logging

import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = 'NMBS'
DEFAULT_NAME_LIVE = "NMBS Live"

DEFAULT_ICON = "mdi:train"
DEFAULT_ICON_ALERT = "mdi:alert-octagon"

CONF_STATION_FROM = 'station_from'
CONF_STATION_TO = 'station_to'
CONF_STATION_LIVE = 'station_live'
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

REQUIREMENTS = ["pyrail==0.0.3"]

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATION_FROM): cv.string,
vol.Required(CONF_STATION_TO): cv.string,
vol.Optional(CONF_STATION_LIVE, default=None): cv.string,
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})


def get_time_until(departure_time=None):
"""Calculate the time between now and a train's departure time."""
if departure_time is None:
return 0

delta = dt_util.utc_from_timestamp(int(departure_time)) - dt_util.now()
return round((delta.total_seconds() / 60))


def convert_ms_to_sec(delay=0):
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
"""Calculate the delay in minutes. Delays are expressed in seconds."""
return round((int(delay) / 60))


def get_ride_duration(departure_time, arrival_time, delay=0):
"""Calculate the total travel time in minutes."""
duration = dt_util.utc_from_timestamp(
int(arrival_time)) - dt_util.utc_from_timestamp(int(departure_time))
duration_time = int(round((duration.total_seconds() / 60)))
return duration_time + convert_ms_to_sec(delay)


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NMBS sensor with iRail API."""
from pyrail import iRail
api_client = iRail()

name = config.get(CONF_NAME)
station_from = config.get(CONF_STATION_FROM)
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
station_to = config.get(CONF_STATION_TO)
station_live = config.get(CONF_STATION_LIVE)

sensors = [NMBSSensor(name, station_from, station_to, api_client)]

if station_live is not None:
sensors.append(NMBSLiveBoard(station_live, api_client))

add_entities(sensors, True)


class NMBSLiveBoard(Entity):
"""Get the next train from a station's liveboard."""

def __init__(self, live_station, api_client):
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
"""Initialize the sensor for getting liveboard data."""
self._station = live_station
self._api_client = api_client
self._state = None

@property
def name(self):
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
"""Return the sensor default name."""
return DEFAULT_NAME_LIVE

@property
def icon(self):
"""Return the default icon or an alert icon if delays."""
if self._attrs is not None and int(self._attrs['delay']) > 0:
return DEFAULT_ICON_ALERT

return DEFAULT_ICON

@property
def state(self):
"""Return sensor state."""
return self._state

@property
def device_state_attributes(self):
"""Return the sensor attributes if data is available."""
if self._state is None or self._attrs is None:
return None

delay = convert_ms_to_sec(self._attrs["delay"])
departure = get_time_until(self._attrs['time'])

return {
'Delay': "{} minutes".format(delay) if delay > 0 else None,
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
"Vehicle ID": self._attrs['vehicle'],
'Occupancy': self._attrs['occupancy']['name'],
"Extra train": True if int(self._attrs['isExtra']) > 0 else False,
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
'Departure': "In {} minutes".format(departure),
ATTR_ATTRIBUTION: "https://api.irail.be/",
}

def update(self):
"""Set the state equal to the next departure."""
liveboard = self._api_client.get_liveboard(self._station)
next_departure = liveboard['departures']['departure'][0]

self._attrs = next_departure
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
self._state = "Track {} - {}".format(
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
next_departure['platform'], next_departure['station'])


class NMBSSensor(Entity):
"""Get the the total travel time for a given connection."""

def __init__(self, name, station_from, station_to, api_client):
self._name = name
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
self._station_from = station_from
self._station_to = station_to
self._api_client = api_client
self._state = None

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return 'min'

@property
def icon(self):
"""Return the sensor default icon or an alert icon if any delay."""
delay = convert_ms_to_sec(self._attrs['departure']['delay'])
if self._attrs is not None and delay > 0:
return "mdi:alert-octagon"

return "mdi:train"

@property
def device_state_attributes(self):
"""Return sensor attributes if data is available."""
if self._state is None or self._attrs is None:
return None

delay = convert_ms_to_sec(self._attrs['departure']['delay'])
departure = get_time_until(self._attrs['departure']['time'])

return {
'Delay': "{} minutes".format(delay) if delay > 0 else None,
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
'Departure': "In {} minutes".format(departure),
'Direction': self._attrs['departure']['direction']['name'],
'Occupancy': self._attrs['departure']['occupancy']['name'],
"Platform (arriving)": self._attrs['arrival']['platform'],
"Platform (departing)": self._attrs['departure']['platform'],
"Vehicle ID": self._attrs['departure']['vehicle'],
ATTR_ATTRIBUTION: "https://api.irail.be/",
}

@property
def state(self):
"""Return the state of the device."""
return self._state

def update(self):
"""Set the state to the available amount of bikes as a number"""
thibmaek marked this conversation as resolved.
Show resolved Hide resolved
connections = self._api_client.get_connections(
self._station_from, self._station_to)

next_connection = None

if int(connections['connection'][0]['departure']['left']) > 0:
next_connection = connections['connection'][1]
else:
next_connection = connections['connection'][0]

self._attrs = next_connection

duration = get_ride_duration(
next_connection['departure']['time'],
next_connection['arrival']['time'],
next_connection['departure']['delay'],
)

self._state = duration
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -1662,3 +1662,6 @@ zigpy==0.2.0

# homeassistant.components.zoneminder
zm-py==0.1.0

# homeassistant.components.sensor.nmbs
pyrail==0.0.3