Skip to content

Commit

Permalink
ready for first release (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-jung authored Jan 3, 2025
1 parent fb42340 commit 91308bc
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 52 deletions.
48 changes: 34 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@
This integration provides information about next departures for different public transport types like Bus, Subway, Tram etc.

***

## API Endpoints
The `Public Transport Departures` integration uses EFA (Elektronische Fahrplanauskunft) endpoints as data source. This endpoints are maintained by different federal states (Bundesländern) and/or municipalities.

### Supported EFA endpoints
There is a list of known endpoints (will be updated continuously with each release)
> If more EFA endpoints are known to you, please write me a short messge or create a new issue with URL. After a check I will add them to supported API's.
#### Baden-Württemberg
| Name | API URL | Supports realtime |
|--------|------|:---------------------:|
|Verkehrsverbund Rhein-Neckar (VRN)| https://www.vrn.de/mngvrn/ |:x:|
|Verkehrs- und Tarifverbund Stuttgart (VVS)|https://www3.vvs.de/mngvvs/|:x:|

#### Bayern
| Name | API URL | Supports realtime |
|--------|------|:---------------------:|
|MoBY (Bahnland Bayern)|https://bahnland-bayern.de/efa/|:white_check_mark:|
|Regensburger Verkehrsverbund (RVV)|https://efa.rvv.de/efa/|:x:|
|Verkehrsverbund Großraum Nürnberg (VGN)| https://efa.vgn.de/vgnExt_oeffi/ |:x:|

## Installation

### HACS Installation (recommended)
Expand All @@ -18,12 +39,12 @@ Until it's finished you can install the integration by adding this repository as

### 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 `ha_departures`.
1. Download all the files from the `custom_components/ha_departures/` directory (folder) in this repository.
1. Place the files you downloaded in the new directory (folder) you created.
1. Restart Home Assistant
1. Using the tool of choice open the folder for your HA configuration (where you find `configuration.yaml`).
2. If you do not have a `custom_components` folder there, you need to create it.
3. In the `custom_components` folder create a new folder called `ha_departures`.
4. Download all the files from the `custom_components/ha_departures/` folder in this repository.
5. Place the files you downloaded in the new folder you created in `step 3`.
6. Restart Home Assistant

## Configuration

Expand All @@ -35,11 +56,10 @@ The configuration of integration is made via Home Assistant GUI
4. Click on integration to start [configuration dialog](#Configure-a-new-station)

### Configure a new station
#### Step 1 - Choose the API endpoint providing departures information and enter stop name
> Currently is only `Verkehrsverbund Großraum Nürnberg` is supported. If you know an endpoint for you region, let me know. I will add the endpoint to the list.

![image](https://github.com/user-attachments/assets/6341bb9c-58b1-4d94-bfc5-277dea779d37)
#### Step 1 - Choose the [API endpoint](#supported-efa-endpoints) and enter stop name

![image](https://github.com/user-attachments/assets/6341bb9c-58b1-4d94-bfc5-277dea779d37)

#### Step 2 - Choose stop
> In this step `ha-departures` integration will search for all locations matching provided stop name.
Expand All @@ -53,7 +73,7 @@ The configuration of integration is made via Home Assistant GUI
![image](https://github.com/user-attachments/assets/2e51a94b-ef8a-4422-8e3b-dec921a1a366)

As result a new `Hub` is created incl. new sensor(s) for each direction you selected in previous step:
As result a new `Hub` has been created incl. new sensor(s) for each connection you selected in previous step:
![image](https://github.com/user-attachments/assets/e3d4de2c-adda-4414-8f8a-d8c52e0bdd38)

![image](https://github.com/user-attachments/assets/7a54e888-df7f-4098-a644-f93279f043d7)
Expand All @@ -73,10 +93,10 @@ sensor:
- platform: template
sensors:
furth_197:
friendly_name: 'Fürth Hauptbahnhof - Bus 179 - Fürth Süd(only time)'
friendly_name: 'Fürth Hauptbahnhof - Bus 179 - Fürth Süd(time only)'
value_template: "{{ (as_datetime(states('sensor.furth_hauptbahnhof_bus_179_furth_sud'))).strftime('%H:%m') }}"
```
Add entity (or entites) card to your Dashboars(don't forget to reload yaml before)\
Add entity (or entites) card to your Dashboars(don't forget to reload yaml before)
```yaml
type: entities
entities:
Expand All @@ -87,8 +107,8 @@ entities:
![image](https://github.com/user-attachments/assets/d813c9e4-0d5f-498e-81de-6abc88430c8c)
### Option 2 (with time-bar-card)
To get more fancy stuff, you can use e.g. [time-bar-card](https://github.com/rianadon/timer-bar-card) to visualize remaining time to next departure:
yaml conifuguration:
You can use other cards like [time-bar-card](https://github.com/rianadon/timer-bar-card) to visualize remaining time to the next departure.
card yaml configuration:
```yaml
type: custom:timer-bar-card
name: Abfahrten Fürth-Hbf
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ha_departures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def _async_update_data(self):
async with EfaClient(self._url) as client:
try:
self._data = await client.departures_by_location(
self._stop_id, date=now_time
self._stop_id, arg_date=now_time, realtime=True
)
except EfaConnectionError as err:
_LOGGER.error("Connection to EFA client failed")
Expand Down
30 changes: 18 additions & 12 deletions custom_components/ha_departures/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from copy import deepcopy
import logging

from apyefa import EfaClient, Line, Location, LocationFilter
from aiohttp import ConnectionTimeoutError
from apyefa import EfaClient, Line, LineRequestType, Location, LocationFilter
from apyefa.exceptions import EfaConnectionError, EfaResponseInvalid
import voluptuous as vol

Expand Down Expand Up @@ -48,10 +49,10 @@ def __init__(self) -> None:
self._all_stops: list[Location] = []
self._stop: Location | None = None
self._lines: list[Line] = []
self._errors: dict[str, str] = {}

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
_errors: dict[str, str] = {}

_LOGGER.debug("Start step_user: %s", user_input)

Expand All @@ -63,22 +64,22 @@ async def async_step_user(self, user_input=None):
self._all_stops = await client.locations_by_name(
user_input[CONF_STOP_NAME], filters=[LocationFilter.STOPS]
)
except EfaConnectionError as err:
self._errors["base"] = CONF_ERROR_CONNECTION_FAILED
except (EfaConnectionError, ConnectionTimeoutError) as err:
_errors[CONF_ERROR_CONNECTION_FAILED] = CONF_ERROR_CONNECTION_FAILED
_LOGGER.error('Failed to connect EFA api "%s"', self._url, exc_info=err)
except EfaResponseInvalid as err:
self._errors["base"] = CONF_ERROR_INVALID_RESPONSE
_errors[CONF_ERROR_INVALID_RESPONSE] = CONF_ERROR_INVALID_RESPONSE
_LOGGER.error("Received invalid response from api", exc_info=err)

if not self._errors:
if not _errors:
_LOGGER.debug(
"%s stop(s) found for %s",
len(self._all_stops),
user_input[CONF_STOP_NAME],
)

if not self._all_stops:
self._errors["base"] = CONF_ERROR_NO_STOP_FOUND
_errors[CONF_ERROR_NO_STOP_FOUND] = CONF_ERROR_NO_STOP_FOUND
else:
return await self.async_step_stop()

Expand All @@ -102,11 +103,12 @@ async def async_step_user(self, user_input=None):
vol.Required(CONF_STOP_NAME): str,
}
),
errors=self._errors,
errors=_errors,
)

async def async_step_stop(self, user_input=None):
"""Handle step to choose a stop from the available list."""
_errors: dict[str, str] = {}

_LOGGER.debug("Start step_stop: %s", user_input)

Expand All @@ -122,7 +124,7 @@ async def async_step_stop(self, user_input=None):
await self.async_set_unique_id(slugify(self._stop.id))
self._abort_if_unique_id_configured()

if not self._errors:
if not _errors:
return await self.async_step_lines()

options: list[SelectOptionDict] = [
Expand All @@ -147,10 +149,12 @@ async def async_step_stop(self, user_input=None):
)
}
),
errors=_errors,
)

async def async_step_lines(self, user_input=None):
"""Handle step to choose needed lines."""
_errors: dict[str, str] = {}

if user_input is not None:
connections: list[Line] = list(
Expand All @@ -171,18 +175,20 @@ async def async_step_lines(self, user_input=None):
self._lines = []

async with EfaClient(self._url) as client:
self._lines = await client.lines_by_location(self._stop.id)
self._lines = await client.lines_by_location(
self._stop.id, req_types=[LineRequestType.DEPARTURE_MONITOR]
)

_directions: dict = {
x.id: f"{transport_to_str(x.product)} {x.name} - {x.destination.name}"
for x in self._lines
x.id: f"{x.name} - {x.destination.name}" for x in self._lines
}

return self.async_show_form(
step_id="lines",
data_schema=vol.Schema(
{vol.Required(CONF_LINES): cv.multi_select(_directions)}
),
errors=_errors,
)

@staticmethod
Expand Down
12 changes: 7 additions & 5 deletions custom_components/ha_departures/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
NAME = "Public Transport Departures"
DOMAIN = "ha_departures"
DOMAIN_DATA = f"{DOMAIN}_data"
VERSION = "0.0.1"
VERSION = "1.0.0"

ISSUE_URL = "https://github.com/alex-jung/ha-departures/issues"

# Device classes

# Platforms
SENSOR = "sensor"
PLATFORMS = [SENSOR]
Expand Down Expand Up @@ -39,12 +37,16 @@
ATTR_DELAY: Final = "delay"
ATTR_OCCUPANCY_LEVEL: Final = "occupancy_level"
ATTR_PLANNED_DEPARTURE_TIME: Final = "planned_departure_time"
ATTR_ACTUAL_DEPARTURE_TIME: Final = "actual_departure_time"
ATTR_ESTIMATED_DEPARTURE_TIME: Final = "estimated_departure_time"


# Endpoints
EFA_ENDPOINTS: Final = {
"Verkehrsverbund Großraum Nürnberg(VGN)": "https://efa.vgn.de/vgnExt_oeffi/"
"MoBY (Bahnland Bayern)": "https://bahnland-bayern.de/efa/",
"Regensburger Verkehrsverbund (RVV)": "https://efa.rvv.de/efa/",
"Verkehrsverbund Großraum Nürnberg (VGN)": "https://efa.vgn.de/vgnExt_oeffi/",
"Verkehrsverbund Rhein-Neckar (VRN)": "https://www.vrn.de/mngvrn/",
"Verkehrs- und Tarifverbund Stuttgart (VVS)": "https://www3.vvs.de/mngvvs/",
}


Expand Down
10 changes: 6 additions & 4 deletions custom_components/ha_departures/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
def transport_to_str(t_type: TransportType):
"""Return human readable german translation of transport type."""
match t_type:
case TransportType.BUS:
case TransportType.CITY_BUS:
return "Bus"
case TransportType.REGIONAL_BUS:
return "Reginal Bus"
case TransportType.EXPRESS_BUS:
return "Express Bus"
case TransportType.SUBWAY:
return "U-Bahn"
case TransportType.TRAM:
return "Tram"
case TransportType.RBUS:
return "Regional Bus"
case TransportType.RAIL:
case TransportType.TRAIN:
return "Zug"
case TransportType.SUBURBAN:
return "S-Bahn"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ha_departures/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/alex-jung/ha-departures/issues",
"requirements": ["pytz>=2013.6", "apyefa==0.0.3"],
"requirements": ["pytz>=2013.6", "apyefa==1.0.0"],
"version": "0.0.2"
}
40 changes: 31 additions & 9 deletions custom_components/ha_departures/sensor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Sensor platform for Public Transport Departures."""

from datetime import datetime, timedelta
import logging

from apyefa import Departure, Line, TransportType
Expand All @@ -9,7 +8,13 @@
from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import ATTR_DIRECTION, ATTR_LINE_ID, ATTR_LINE_NAME, ATTR_TRANSPORT_TYPE
from .const import (
ATTR_DIRECTION,
ATTR_LINE_ID,
ATTR_LINE_NAME,
ATTR_PLANNED_DEPARTURE_TIME,
ATTR_TRANSPORT_TYPE,
)
from .helper import create_unique_id

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -46,9 +51,8 @@ def __init__(
self._transport = line.product
self._line = line.name
self._destination = line.destination
self._planned_departure_time: datetime | None = None
self._delay: timedelta | None = None
self._occupancy_level: str | None = None
# self._delay: timedelta | None = None
# self._occupancy_level: str | None = None
self._value = None

self._attr_name = (
Expand All @@ -62,7 +66,7 @@ def __init__(
ATTR_DIRECTION: line.destination.name,
ATTR_LINE_ID: line.id,
# ATTR_OCCUPANCY_LEVEL: self._occupancy_level,
# ATTR_PLANNED_DEPARTURE_TIME: self._planned_departure_time,
ATTR_PLANNED_DEPARTURE_TIME: None,
}

_LOGGER.debug('ha-departures sensor "%s" created', self.unique_id)
Expand All @@ -76,15 +80,19 @@ def native_value(self):
def icon(self) -> str:
"""Icon of the entity, based on transport type."""
match self._transport:
case TransportType.BUS | TransportType.RBUS | TransportType.EXPRESS_BUS:
case (
TransportType.CITY_BUS
| TransportType.REGIONAL_BUS
| TransportType.EXPRESS_BUS
):
return "mdi:bus"
case TransportType.TRAM:
return "mdi:tram"
case TransportType.SUBWAY:
return "mdi:subway"
case TransportType.AST:
return "mdi:taxi"
case TransportType.SUBURBAN | TransportType.RAIL | TransportType.CITY_RAIL:
case TransportType.SUBURBAN | TransportType.TRAIN | TransportType.CITY_RAIL:
return "mdi:train"
case _:
return "mdi:train-bus"
Expand All @@ -105,6 +113,20 @@ def _handle_coordinator_update(self) -> None:
_LOGGER.debug("No departures found for %s", self.unique_id)
return

self._value = departures[0].estimated_time or departures[0].planned_time
self._value = departures[0].planned_time

estimated_time = departures[0].estimated_time
planned_time = departures[0].planned_time

if estimated_time:
self._value = estimated_time
else:
self._value = planned_time

self._attr_extra_state_attributes.update(
{
ATTR_PLANNED_DEPARTURE_TIME: planned_time,
}
)

self.async_write_ha_state()
6 changes: 3 additions & 3 deletions custom_components/ha_departures/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
{
"user": {
"title": "Public Transport Departures",
"description": "Please select EFA connection and enter stop name:",
"description": "Please select API endpoint and enter stop name:",
"data": {
"endpoint": "EFA Endpoint",
"endpoint": "Endpoint",
"stop_name": "Stop name"
}
},
Expand All @@ -29,7 +29,7 @@
"error": {
"no_stop_found": "No locations found",
"invalid_api_response": "Received invalid response from EFA endpoint",
"connection_failed": "Connection to EFA endpoint failed"
"connection_failed": "Connection to EFA endpoint failed. Try later again."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
Expand Down
Loading

0 comments on commit 91308bc

Please sign in to comment.