diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b60aeed --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/.HA_VERSION +/.cloud +/.storage +/.vscode +/automations.yaml +/configuration.yaml +/deps +/groups.yaml +/home-assistant.log +/home-assistant_v2.db +/scenes.yaml +/scripts.yaml +/secrets.yaml +/tts +/custom_components/emt_madrid/__pycache__ +*.pyc \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8581b65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Fernando Martínez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..65c9647 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +_Please :star: this repo if you find it useful_ + +# EMT Madrid bus platform for Home Assistant + +This is a custom sensor for Home Assistant that allows you tu have the waiting time for a specific Madrid-EMT bus stop. Each sensor will provide the arrival time for the next 2 buses of the line specified in the configuration. + +Thanks to [EMT Madrid MobilityLabs](https://mobilitylabs.emtmadrid.es/) for providing the data and [documentation](https://apidocs.emtmadrid.es/). + +![Example](example.png) + +## Prerequisites + +To use the EMT Mobilitylabs API you need to register in their [website](https://mobilitylabs.emtmadrid.es/). You have to provide a valid email account and a password that will be used to configure the sensor. Once you are registered you will receive a confirmation email to activate your account. It will not work until you have completed all the steps. + +## Manual Installation + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +1. If you do not have a `custom_components` directory (folder) there, you need to create it. +1. In the `custom_components` directory (folder) create a new folder called `emt_madrid`. +1. Download _all_ the files from the `custom_components/emt_madrid/` directory (folder) in this repository. +1. Place the files you downloaded in the new directory (folder) you created. +1. Restart Home Assistant +1. Add `emt_madrid` sensor to your `configuration.yaml` file: + + ```yaml + # Example configuration.yaml entry + sensor: + - platform: emt_madrid + email: !secret EMT_EMAIL + password: !secret EMT_PASSWORD + stop: "72" + line: "27" + name: "Bus 27 en Cibeles" + icon: "mdi:fountain" + ``` + +### Configuration Variables + +**email**:\ + _(string) (Required)_\ + Email account used to register in the EMT Madrid API. + +**password**:\ + _(string) (Required)_\ + Password used to register in the EMT Madrid API. + +**stop**:\ + _(string) (Required)_\ + Bus stop ID. + +**line**:\ + _(string) (Required)_\ + Bus line that stops at the previous bus stop. + +**name**:\ + _(string) (Optional)_\ + Name to use in the frontend. +_Default value: "Bus at "_ + +**icon**:\ + _(string) (Optional)_\ + Icon to use in the frontend. +_Default value: "mdi:bus"_ + +## Sensor status and attributes + +Once you have you sensor up and running it will update the data automatically every 30 seconds and you should have the following data: + +**state**:\ + _(int)_\ + Arrival time in minutes for the next bus. It will show "-" when there are no more buses coming and 30 when the arrival time is over 30 minutes. + +### Attributes + +**later_bus**:\ + _(int)_\ + Arrival time in minutes for the second bus. It will show "-" when there are no more buses coming and 30 when the arrival time is over 30 minutes. + +**bus_stop_id**:\ + _(int)_\ + Bus stop id given in the configuration. + +**bus_line**:\ + _(int)_\ + Bus line given in the configuration. + +### Second bus sensor + +If you want to have a specific sensor to show the arrival time for the second bus, you can add the following lines to your `configuration.yaml` file below the `emt_madrid` bus sensor. See the official Home Assistant [template sensor](https://www.home-assistant.io/integrations/template/) for more information. + +```yaml +# Example configuration.yaml entry +- platform: template + sensors: + siguiente_27: + friendly_name: "Siguiente bus 27" + unit_of_measurement: "min" + value_template: "{{ state_attr('sensor.bus_27_en_cibeles', 'later_bus') }}" +``` diff --git a/custom_components/emt_madrid/__init__.py b/custom_components/emt_madrid/__init__.py new file mode 100755 index 0000000..c449013 --- /dev/null +++ b/custom_components/emt_madrid/__init__.py @@ -0,0 +1 @@ +"""Madrid EMT bus sensor.""" \ No newline at end of file diff --git a/custom_components/emt_madrid/manifest.json b/custom_components/emt_madrid/manifest.json new file mode 100755 index 0000000..be37f4e --- /dev/null +++ b/custom_components/emt_madrid/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "emt_madrid", + "name": "EMT Madrid", + "documentation": "https://github.com/fermartv/EMT-Madrid/", + "issue_tracker": "https://github.com/fermartv/EMT-Madrid/issues", + "codeowners": ["@FerMartV"], + "version": "1.0.1" +} diff --git a/custom_components/emt_madrid/sensor.py b/custom_components/emt_madrid/sensor.py new file mode 100755 index 0000000..9276fe5 --- /dev/null +++ b/custom_components/emt_madrid/sensor.py @@ -0,0 +1,199 @@ +import requests +import json +import math + +import logging + +import voluptuous as vol + +"""Platform for sensor integration.""" +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_EMAIL, + CONF_ICON, + CONF_NAME, + CONF_PASSWORD, + TIME_MINUTES, +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +ATTRIBUTION = "Data provided by EMT Madrid MobilityLabs" + +CONF_STOP = "stop" +CONF_LINE = "line" + +DEFAULT_NAME = "EMT Madrid" +DEFAULT_ICON = "mdi:bus" + +ATTR_NEXT_UP = "later_bus" +ATTR_BUS_STOP = "bus_stop_id" +ATTR_BUS_LINE = "bus_line" + +BASE_URL = "https://openapi.emtmadrid.es/" +ENDPOINT_LOGIN = "v1/mobilitylabs/user/login/" +ENDPOINT_ARRIVAL_TIME = "v2/transport/busemtmad/stops/" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_STOP): cv.string, + vol.Required(CONF_LINE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.string, + } +) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + email = config.get(CONF_EMAIL) + password = config.get(CONF_PASSWORD) + bus_stop = config.get(CONF_STOP) + line = config.get(CONF_LINE) + name = config.get(CONF_NAME) + icon = config.get(CONF_ICON) + api_emt = APIEMT(email, password) + api_emt.update(bus_stop, line) + add_entities([BusStopSensor(api_emt, bus_stop, line, name, icon)]) + + +class BusStopSensor(Entity): + """Implementation of an EMT-Madrid bus stop sensor.""" + + def __init__(self, api_emt, bus_stop, line, name, icon): + """Initialize the sensor.""" + self._state = None + self._api_emt = api_emt + self._bus_stop = bus_stop + self._bus_line = line + self._icon = icon + self._name = name + if self._name == DEFAULT_NAME: + self._name = "Next {} at {}".format(self._bus_line, self._bus_stop) + + @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._api_emt.get_stop_data(self._bus_line, "arrival") + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TIME_MINUTES + + @property + def icon(self): + """Return sensor specific icon.""" + return self._icon + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + return { + ATTR_NEXT_UP: self._api_emt.get_stop_data(self._bus_line, "next_arrival"), + ATTR_BUS_STOP: self._bus_stop, + ATTR_BUS_LINE: self._bus_line, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + def update(self): + """Fetch new state data for the sensor.""" + self._api_emt.update(self._bus_stop, self._bus_line) + + +class APIEMT: + """ + Interface for the EMT REST API. Docs at https://apidocs.emtmadrid.es/ + """ + + def __init__(self, user, password): + """ Initialize the class and retrieve the token from the API. """ + self._user = user + self._password = password + self.token = self.update_token() + + def update(self, stop, line): + """ Update the arrival times data from the API. """ + url = "{}{}{}/arrives/{}/".format(BASE_URL, ENDPOINT_ARRIVAL_TIME, stop, line) + headers = {"accessToken": self.token} + data = {"stopId": stop, "lineArrive": line, "Text_EstimationsRequired_YN": "Y"} + response = self.api_call(url, headers, data) + try: + self.set_stop_data(response, line) + except: + _LOGGER.error("Invalid stop ID") + raise Exception("Unable to get the arrival times from the API") + + def set_stop_data(self, data, target_line): + """ Create a dictionary to store the arrival time data in the following format: + { + line:{ + 'arrival': arrival_time, + 'next_arrival': arrival_time + } + } + """ + arrival_data = {} + + for bus in data["data"][0]["Arrive"]: + estimated_time = math.trunc(bus["estimateArrive"] / 60) + if estimated_time > 30: + estimated_time = 30 + line = bus["line"] + if line not in arrival_data: + arrival_data[line] = {"arrival": estimated_time} + elif "next_arrival" not in arrival_data[line]: + arrival_data[line]["next_arrival"] = estimated_time + + if target_line not in arrival_data: + arrival_data[target_line] = {"arrival": "-"} + if "next_arrival" not in arrival_data[target_line]: + arrival_data[target_line]["next_arrival"] = "-" + self._arrival_time = arrival_data + + def get_stop_data(self, line, bus): + """ Get the data of the next bus ("arrival") or the one after that ("next_arrival") """ + return self._arrival_time[str(line)][bus] + + def api_call(self, url, headers, payload=None, method="POST"): + """ Request data from the API. """ + if method == "POST": + payload = json.dumps(payload) + response = requests.post(url, data=payload, headers=headers) + + elif method == "GET": + response = requests.get(url, headers=headers) + + else: + raise Exception("Invalid HTTP method: " + method) + + if response.status_code != 200: + _LOGGER.error("Invalid response: %s", response.status_code) + return response.json() + + def update_token(self): + """ Update or create the token when the sensor is initialized. """ + headers = {"email": self._user, "password": self._password} + url = BASE_URL + ENDPOINT_LOGIN + response = self.api_call(url, headers, None, "GET") + try: + self.token = response["data"][0]["accessToken"] + except: + if response["code"] == "80": + _LOGGER.error("Invalid credentials") + else: + _LOGGER.error("Unable to retrieve the token from the API") + raise Exception("Unable to connect to the API") + return self.token + diff --git a/example.png b/example.png new file mode 100644 index 0000000..4f697b9 Binary files /dev/null and b/example.png differ diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..d73301e --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "EMT-Madrid bus", + "render_readme": true, + "country": "ES", + "domains": ["sensor"] +}