From 91308bcce61b1901b0303d2a824507d06b2e1df7 Mon Sep 17 00:00:00 2001 From: Alex Jung Date: Fri, 3 Jan 2025 01:12:09 +0100 Subject: [PATCH] ready for first release (#28) --- README.md | 48 +++++++++++++------ custom_components/ha_departures/__init__.py | 2 +- .../ha_departures/config_flow.py | 30 +++++++----- custom_components/ha_departures/const.py | 12 +++-- custom_components/ha_departures/helper.py | 10 ++-- custom_components/ha_departures/manifest.json | 2 +- custom_components/ha_departures/sensor.py | 40 ++++++++++++---- custom_components/ha_departures/strings.json | 6 +-- .../ha_departures/translations/de.json | 6 +-- 9 files changed, 104 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 5e2363e..111a1f1 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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. @@ -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) @@ -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: @@ -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 diff --git a/custom_components/ha_departures/__init__.py b/custom_components/ha_departures/__init__.py index 34fc39b..f6d3af3 100644 --- a/custom_components/ha_departures/__init__.py +++ b/custom_components/ha_departures/__init__.py @@ -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") diff --git a/custom_components/ha_departures/config_flow.py b/custom_components/ha_departures/config_flow.py index 2b25c08..8d4c5a9 100644 --- a/custom_components/ha_departures/config_flow.py +++ b/custom_components/ha_departures/config_flow.py @@ -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 @@ -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) @@ -63,14 +64,14 @@ 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), @@ -78,7 +79,7 @@ async def async_step_user(self, user_input=None): ) 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() @@ -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) @@ -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] = [ @@ -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( @@ -171,11 +175,12 @@ 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( @@ -183,6 +188,7 @@ async def async_step_lines(self, user_input=None): data_schema=vol.Schema( {vol.Required(CONF_LINES): cv.multi_select(_directions)} ), + errors=_errors, ) @staticmethod diff --git a/custom_components/ha_departures/const.py b/custom_components/ha_departures/const.py index 8623770..53531b1 100644 --- a/custom_components/ha_departures/const.py +++ b/custom_components/ha_departures/const.py @@ -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] @@ -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/", } diff --git a/custom_components/ha_departures/helper.py b/custom_components/ha_departures/helper.py index 83995a6..44e7571 100644 --- a/custom_components/ha_departures/helper.py +++ b/custom_components/ha_departures/helper.py @@ -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" diff --git a/custom_components/ha_departures/manifest.json b/custom_components/ha_departures/manifest.json index 15048c2..57fa870 100644 --- a/custom_components/ha_departures/manifest.json +++ b/custom_components/ha_departures/manifest.json @@ -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" } \ No newline at end of file diff --git a/custom_components/ha_departures/sensor.py b/custom_components/ha_departures/sensor.py index 8849fd1..0fd5b90 100644 --- a/custom_components/ha_departures/sensor.py +++ b/custom_components/ha_departures/sensor.py @@ -1,6 +1,5 @@ """Sensor platform for Public Transport Departures.""" -from datetime import datetime, timedelta import logging from apyefa import Departure, Line, TransportType @@ -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__) @@ -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 = ( @@ -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) @@ -76,7 +80,11 @@ 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" @@ -84,7 +92,7 @@ def icon(self) -> str: 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" @@ -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() diff --git a/custom_components/ha_departures/strings.json b/custom_components/ha_departures/strings.json index 2daa649..606cf99 100644 --- a/custom_components/ha_departures/strings.json +++ b/custom_components/ha_departures/strings.json @@ -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" } }, @@ -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%]" diff --git a/custom_components/ha_departures/translations/de.json b/custom_components/ha_departures/translations/de.json index 432ac8a..76e57a5 100644 --- a/custom_components/ha_departures/translations/de.json +++ b/custom_components/ha_departures/translations/de.json @@ -3,9 +3,9 @@ "step": { "user": { "title": "Public Transport Departures", - "description": "Bitte EFA Endpoint auswählen und Haltestellenname eingeben:", + "description": "Bitte API Endpoint und Haltestellenname eingeben:", "data": { - "endpoint": "EFA Endpoint", + "endpoint": "Endpoint", "stop_name": "Haltestellenname" } }, @@ -27,7 +27,7 @@ "error": { "no_stop_found": "Keine zutreffende Haltestelle gefunden", "invalid_api_response": "EFA Endpoint ungültige Antwort", - "connection_failed": "Verbindung zu EFA Endpoint fehlgeschlagen" + "connection_failed": "Verbindung zu EFA Endpoint fehlgeschlagen. Versuchen Sie später nochmal." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]"