Skip to content

Commit

Permalink
Merge pull request #3 from fermartv/dev
Browse files Browse the repository at this point in the history
Resolves #1: Add new attributes and multiple lines to a bus stop
  • Loading branch information
fermartv committed Jul 4, 2023
2 parents 066ddf4 + c09015f commit f394609
Show file tree
Hide file tree
Showing 8 changed files with 767 additions and 174 deletions.
102 changes: 68 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,32 @@ This is a custom sensor for Home Assistant that allows you to have the waiting t
Thanks to [EMT Madrid MobilityLabs](https://mobilitylabs.emtmadrid.es/) for providing the data and [documentation](https://apidocs.emtmadrid.es/).

![Example](example.png)
![Example attributes](example_attributes.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:
1. Using the tool of choice open the directory for your HA configuration (where you find `configuration.yaml`).
2. If you do not have a `custom_components` directory there, you need to create it.
3. In the `custom_components` directory create a new directory called `emt_madrid`.
4. Download _all_ the files from the `custom_components/emt_madrid/` directory in this repository.
5. Place the files you downloaded in the new directory you created.
6. Restart Home Assistant
7. 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"
stop: 72
lines:
- "27"
- "N26"
icon: "mdi:fountain"
```

Expand All @@ -45,55 +47,87 @@ To use the EMT Mobilitylabs API you need to register in their [website](https://
Password used to register in the EMT Madrid API.

**stop**:\
_(string) (Required)_\
_(integer) (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 <bus_line> at <bus_stop>"_
**lines**:\
_(list) (Optional)_\
One or more line numbers.

**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:
## Sensors, status and attributes

Once you have the platform up and running, you will have one sensor per line specified. If no lines were provided, it'll create a sensor for each line in that stop ID. The name of the sensor will be automaticalle generated using the following structure: `Bus {line} - {stop_name}`.All the sensors will update the data automatically every minute 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.
Arrival time in minutes for the next bus. It will show "unknown" when there are no more buses coming and 45 when the arrival time is over 45 minutes.

### Attributes

**later_bus**:\
**next_bus**:\
_(int)_\
Arrival time in minutes for the second bus. It will show "unknown" when there are no more buses coming and 45 when the arrival time is over 45 minutes.

**stop_id**:\
_(int)_\
Bus stop ID given in the configuration.

**stop_name**:\
_(int)_\
Bus stop name from EMT.

**stop_address**:\
_(int)_\
Bus stop address from EMT.

**line**:\
_(int)_\
Bus line.

**destination**:\
_(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 line last stop.

**bus_stop_id**:\
**origin**:\
_(int)_\
Bus stop id given in the configuration.
Bus line first stop.

**bus_line**:\
**start_time**:\
_(int)_\
Bus line given in the configuration.
Time at which the first bus leaves the first stop.

**end_time**:\
_(int)_\
Time at which the last bus leaves the first stop.

**max_frequency**:\
_(int)_\
Maximum frequency for this line.

**min_frequency**:\
_(int)_\
Minimum frequency for this line.

**distance**:\
_(int)_\
Distance (in metres) from the next bus to the stop.


### 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') }}"
template:
- sensor:
- name: "Siguiente bus 27"
unit_of_measurement: "min"
state: "{{ state_attr('sensor.bus_27_cibeles_casa_de_america', 'next_bus') }}"
```
2 changes: 1 addition & 1 deletion custom_components/emt_madrid/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""Madrid EMT bus sensor."""
"""EMT Madrid integration."""
190 changes: 190 additions & 0 deletions custom_components/emt_madrid/emt_madrid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"""Support for EMT Madrid API."""

import json
import logging
import math

import requests

BASE_URL = "https://openapi.emtmadrid.es/"
ENDPOINT_LOGIN = "v1/mobilitylabs/user/login/"
ENDPOINT_ARRIVAL_TIME = "v2/transport/busemtmad/stops/"
ENDPOINT_STOP_INFO = "v1/transport/busemtmad/stops/"


_LOGGER = logging.getLogger(__name__)


class APIEMT:
"""A class representing an API client for EMT (Empresa Municipal de Transportes) services.
This class provides methods to authenticate with the EMT API, retrieve bus stop information,
update arrival times, and access the retrieved data.
"""

def __init__(self, user, password, stop_id) -> None:
"""Initialize an instance of the APIEMT class."""
self._user = user
self._password = password
self._token = None
self._stop_info = {
"bus_stop_id": stop_id,
"bus_stop_name": None,
"bus_stop_coordinates": None,
"bus_stop_address": None,
"lines": {},
}

def authenticate(self):
"""Authenticate the user using the provided credentials."""
headers = {"email": self._user, "password": self._password}
url = f"{BASE_URL}{ENDPOINT_LOGIN}"
response = self._make_request(url, headers=headers, method="GET")
self._token = self._extract_token(response)

def _extract_token(self, response):
"""Extract the access token from the API response."""
try:
if response.get("code") != "01":
_LOGGER.error("Invalid email or password")
return "Invalid token"
return response["data"][0]["accessToken"]
except (KeyError, IndexError) as e:
raise ValueError("Unable to get token from the API") from e

def update_stop_info(self, stop_id):
"""Update all the lines and information from the bus stop."""
url = f"{BASE_URL}{ENDPOINT_STOP_INFO}{stop_id}/detail/"
headers = {"accessToken": self._token}
data = {"idStop": stop_id}
if self._token != "Invalid token":
response = self._make_request(url, headers=headers, data=data, method="GET")
self._parse_stop_info(response)

def get_stop_info(
self,
):
"""Retrieve all the information from the bus stop."""
return self._stop_info

def _parse_stop_info(self, response):
"""Parse the stop info from the API response."""
try:
if response.get("code") != "00":
_LOGGER.warning("Bus stop disabled or does not exist")
else:
stop_info = response["data"][0]["stops"][0]
self._stop_info.update(
{
"bus_stop_name": stop_info["name"],
"bus_stop_coordinates": stop_info["geometry"]["coordinates"],
"bus_stop_address": stop_info["postalAddress"],
"lines": self._parse_lines(stop_info["dataLine"]),
}
)
except (KeyError, IndexError) as e:
raise ValueError("Unable to get bus stop information") from e

def _parse_lines(self, lines):
"""Parse the line info from the API response."""
line_info = {}
for line in lines:
line_number = line["label"]
line_info[line_number] = {
"destination": line["headerA"]
if line["direction"] == "A"
else line["headerB"],
"origin": line["headerA"]
if line["direction"] == "B"
else line["headerB"],
"max_freq": int(line["maxFreq"]),
"min_freq": int(line["minFreq"]),
"start_time": line["startTime"],
"end_time": line["stopTime"],
"day_type": line["dayType"],
"distance": [],
"arrivals": [],
}
return line_info

def update_arrival_times(self, stop):
"""Update the arrival times for the specified bus stop and line."""
url = f"{BASE_URL}{ENDPOINT_ARRIVAL_TIME}{stop}/arrives/"
headers = {"accessToken": self._token}
data = {"stopId": stop, "Text_EstimationsRequired_YN": "Y"}
if self._token != "Invalid token":
response = self._make_request(
url, headers=headers, data=data, method="POST"
)
self._parse_arrivals(response)

def get_arrival_time(self, line):
"""Retrieve arrival times in minutes for the specified bus line."""
try:
arrivals = self._stop_info["lines"][line].get("arrivals")
except KeyError:
return [None, None]
while len(arrivals) < 2:
arrivals.append(None)
return arrivals

def get_line_info(self, line):
"""Retrieve the information for a specific line."""
lines = self._stop_info["lines"]
if line in lines:
line_info = lines.get(line)
if "distance" in line_info and len(line_info["distance"]) == 0:
line_info["distance"].append(None)
return line_info

_LOGGER.warning(f"The bus line {line} does not exist at this stop.")
line_info = {
"destination": None,
"origin": None,
"max_freq": None,
"min_freq": None,
"start_time": None,
"end_time": None,
"day_type": None,
"distance": [None],
"arrivals": [None, None],
}
return line_info

def _parse_arrivals(self, response):
"""Parse the arrival times and distance from the API response."""
try:
if response.get("code") == "80":
_LOGGER.warning("Bus Stop disabled or does not exist")
else:
for line_info in self._stop_info["lines"].values():
line_info["arrivals"] = []
line_info["distance"] = []
arrivals = response["data"][0].get("Arrive", [])
for arrival in arrivals:
line = arrival.get("line")
line_info = self._stop_info["lines"].get(line)
arrival_time = min(
math.trunc(arrival.get("estimateArrive") / 60), 45
)
if line_info:
line_info["arrivals"].append(arrival_time)
line_info["distance"].append(arrival.get("DistanceBus"))
except (KeyError, IndexError) as e:
raise ValueError("Unable to get the arrival times from the API") from e
except TypeError as e:
_LOGGER.error(f"ERROR {e} --> RESPONSE: {response}")

def _make_request(self, url: str, headers=None, data=None, method="POST"):
"""Send an HTTP request to the specified URL."""
try:
if method not in ["POST", "GET"]:
raise ValueError(f"Invalid HTTP method: {method}")
kwargs = {"url": url, "headers": headers, "timeout": 10}
if method == "POST":
kwargs["data"] = json.dumps(data)
response = requests.request(method, **kwargs)
response.raise_for_status()
return response.json()
except requests.HTTPError as e:
raise requests.HTTPError(f"Error while connecting to EMT API: {e}") from e
Loading

0 comments on commit f394609

Please sign in to comment.