diff --git a/CODEOWNERS b/CODEOWNERS index 44df2624eaa7a..f8022d6d83321 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,6 +64,7 @@ /bundles/org.openhab.binding.dbquery/ @lujop /bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis +/bundles/org.openhab.binding.deutschebahn/ @soenkekueper /bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele /bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 9ddc08a15bf0c..e00079be49c15 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -311,6 +311,11 @@ org.openhab.binding.denonmarantz ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.deutschebahn + ${project.version} + org.openhab.addons.bundles org.openhab.binding.digiplex diff --git a/bundles/org.openhab.binding.deutschebahn/NOTICE b/bundles/org.openhab.binding.deutschebahn/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.deutschebahn/README.md b/bundles/org.openhab.binding.deutschebahn/README.md new file mode 100644 index 0000000000000..23184d523fea2 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/README.md @@ -0,0 +1,345 @@ +# Deutsche Bahn Binding + +The Deutsche Bahn Binding provides the latest timetable information for all trains that arrive or depart at a specific train station, including live information for delays and changes in timetable. +The information are requested from the timetable api of Deutsche Bahn developer portal, so you'll need a (free) developer account to use this binding. + +## Supported Things + +- **timetable** The timetable bridge connects to the timetable api and provides information for the next trains that will arrive or depart at the configured station. +- **train** The train thing represents one trains within the configured timetable. This may be an arrival or a departure. + +## Thing Configuration + +### Generate Access-Key for timetable API + +To configure a timetable you first need to register at Deutsche Bahn developer portal and register for timetable API to get an access key. + +1. Go to [Deutsche Bahn Developer](https://developer.deutschebahn.com) +2. Register new account or login with an existing one +3. If no application is configured yet (check Tab "Meine Anwendungen") create a new application. Only the name is required, any other fields can be left blank. +4. Go to APIs - Timetables v1 (may be displayed on second page) +5. Choose your previously created application and hit "Abonnieren" +6. In confirmation-dialog choose "Wechsel zu meine Abonnements" +7. Create an access key for the production environment by hitting "Schlüssel Erstellen" +8. Copy the "Zugangstoken". This is required to access the api from openHAB. + +### Determine the EVA-No of your station + +For the selection of the station within openHAB you need the eva no. of the station. +You can look up the number within the csv file available at [Haltestellendaten](https://data.deutschebahn.com/dataset.tags.EVA-Nr..html). + +### Configure timetable bridge + +With access key for developer portal and eva no. of your station you're ready to configure a timetable (bridge) for this station. +In addition you can configure if only arrivals, only departures or all trains should be contained within the timetable. + +**timetable** parameters: + +| Property | Default | Required | Description | +|-|-|-|-| +| `accessToken` | | Yes | The access token for the timetable api within the developer portal of Deutsche Bahn. | +| `evaNo` | | Yes | The eva nr. of the train station for which the timetable will be requested.| +| `trainFilter` | | Yes | Selects the trains that will be displayed in the timetable. Either only arrivals, only departures or all trains can be displayed. | + + +### Configuring the trains + +Once you've created the timetable you can add train-things that represent the trains within this timetable. +Each train represents one position within the timetable. For example: If you configure a train with position 1 this will be +the next train that arrives / departs at the given station. Position 2 will be the second one, and so on. If you want to +show the next 4 trains for a station, create 4 things with positions 1 to 4. + +**Attention:** The timetable api only provides data for the next 18 hours. If the timetable contains less train entries than you've created +train things, the channels of these trains will be undefined. + +**train** parameters: + +| Property | Default | Required | Description | +|-|-|-|-| +| `position` | | Yes | The position of the train within the timetable. | + + +## Channels + +Each train has a set of channels, that provides access to any information served by the timetable API. A detailed description of the values and their meaning can be found within +the [Timetables V1 API Description](https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&). +The information are grouped into three channel-groups: +The first channel group (trip) contains all information for the trip of the train, for example the category (like ICE, RE, S). +The second and third channel group contains information about the the arrival and the departure of the train at the given station. +Both of the groups may provide an 'UNDEF' channel value, when the train does not arrive / depart at this station +(due it starts or ends at the given station). If you have configured your timetable to contain only departures (with property trainFilter) the departure channel values will always be defined +and if you have selected only arrivals the arrival channel values will always be defined. +Channels will have a 'NULL' channel value, when the corresponding attribute is not set. + +Basically most information are available as planned and changed value. This allows to easy display changed values (for example the delay or changed platform). + + +**Channels for trip information** +| channel | type | description | +|----------|--------|------------------------------| +| category | String | Provides the category of the trip, e.g. "ICE" or "RE". | +| number | String | Provides the trip/train number, e.g. "4523". | +| filter-flags | String | Provides the filter flags. | +| trip-type | String | Provides the type of the trip. | +| owner | String | Provides the owner of the train. A unique short-form and only intended to map a trip to specific evu (EisenbahnVerkehrsUnternehmen). | + + +**Channels for arrival / departure** +| channel | type | description | +|----------|--------|------------------------------| +| planned-path | String | Provides the planned path of a train. | +| changed-path | String | Provides the changed path of a train. | +| planned-platform | String | Provides the planned platform of a train. | +| changed-platform | String | Provides the changed platform of a train. | +| planned-time | DateTime | Provides the planned time of a train. | +| changed-time | DateTime | Provides the changed time of a train. | +| planned-status | String | Provides the planned status (planned, added, cancelled) of a train. | +| changed-status | String | Provides the changed status (planned, added, cancelled) of a train. | +| cancellation-time | DateTime | Time when the cancellation of this stop was created. | +| line | String | The line of the train. | +| messages | String | Messages for this train. Contains all translated codes from the messages of the selected train stop. Multiple messages will be separated with a single dash. | +| hidden | Switch | On if the event should not be shown because travellers are not supposed to enter or exit the train at this stop. | +| wings | String | A sequence of trip id separated by pipe symbols. | +| transition | String | Trip id of the next or previous train of a shared train. At the start stop this references the previous trip, at the last stop it references the next trip. | +| planned-distant-endpoint | String | Planned distant endpoint of a train. | +| changed-distant-endpoint | String | Changed distant endpoint of a train. | +| distant-change | Number | Distant change | +| planned-final-station | String | Planned final station of the train. For arrivals the starting station is returned, for departures the target station is returned. | +| planned-intermediate-stations | String | Returns the planned stations this train came from (for arrivals) or the stations this train will go to (for departures). Stations will be separated by single dash. | +| changed-final-station | String | Changed final station of the train. For arrivals the starting station is returned, for departures the target station is returned. | +| changed-intermediate-stations | String | Returns the changed stations this train came from (for arrivals) or the stations this train will go to (for departures). Stations will be separated by single dash. | + +## Full Example + +timetable.things + +``` +Bridge deutschebahn:timetable:timetableLehrte "Fahrplan Lehrte" [ accessToken="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", trainFilter="departures", evaNo="8000226" ] { + Thing deutschebahn:train:timetableLehrte:lehrteZug1 "Zug 1" [ position="1" ] + Thing deutschebahn:train:timetableLehrte:lehrteZug2 "Zug 2" [ position="2" ] +} +``` + +timetable.items + +``` +// Groups +Group zug1 "Zug 1" +Group zug1Fahrt "Zug 1 Fahrt" (zug1) +Group zug1Ankunft "Zug 1 Ankunft" (zug1) +Group zug1Abfahrt "Zug 1 Abfahrt" (zug1) + +// Trip Information +String Zug1_Trip_Category "Kategorie" (zug1Fahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:trip#category"} +String Zug1_Trip_Number "Nummer" (zug1Fahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:trip#number"} +String Zug1_Trip_FilterFlags "Filter" (zug1Fahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:trip#filter-flags"} +String Zug1_Trip_TripType "Fahrttyp" (zug1Fahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:trip#trip-type"} +String Zug1_Trip_Owner "Unternehmen" (zug1Fahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:trip#owner"} + + +// Arrival Information +DateTime Zug1_Arrival_Plannedtime "Geplante Zeit" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-time"} +DateTime Zug1_Arrival_Changedtime "Geänderte Zeit" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-time"} +String Zug1_Arrival_Plannedplatform "Geplantes Gleis" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-platform"} +String Zug1_Arrival_Changedplatform "Geändertes Gleis" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-platform"} +String Zug1_Arrival_Line "Linie" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#line"} +String Zug1_Arrival_Plannedintermediatestations "Geplante Halte" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-intermediate-stations"} +String Zug1_Arrival_Changedintermediatestations "Geänderte Halte" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-intermediate-stations"} +String Zug1_Arrival_Plannedfinalstation "Geplanter Start-/Zielbahnhof" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-final-station"} +String Zug1_Arrival_Changedfinalstation "Geänderter Start-/Zielbahnhof" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-final-station"} +String Zug1_Arrival_Messages "Meldungen" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#messages"} +String Zug1_Arrival_Plannedstatus "Geplanter Status" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-status"} +String Zug1_Arrival_Changedstatus "Geänderter Status" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-status"} +DateTime Zug1_Arrival_Cancellationtime "Stornierungs-Zeitpunkt" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#cancellation-time"} + +// Arrival advanced information +String Zug1_Arrival_Planneddistantendpoint "Geplanter entfernter Endpunkt" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-distant-endpoint"} +String Zug1_Arrival_Changeddistantendpoint "Geänderter entfernter Endpunkt" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-distant-endpoint"} +String Zug1_Arrival_Plannedpath "Geplante Route" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#planned-path"} +String Zug1_Arrival_Changedpath "Geändert Route" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#changed-path"} +Number Zug1_Arrival_Distantchange "Geänderter Zielbahnhof" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#distant-change"} +Switch Zug1_Arrival_Hidden "Versteckt" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#hidden"} +String Zug1_Arrival_Transition "Übergang" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#transition"} +String Zug1_Arrival_Wings "Wings" (zug1Ankunft) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:arrival#wings"} + +// Departure Information +DateTime Zug1_Departure_Plannedtime "Geplante Zeit" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-time"} +DateTime Zug1_Departure_Changedtime "Geänderte Zeit" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-time"} +String Zug1_Departure_Plannedplatform "Geplantes Gleis" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-platform"} +String Zug1_Departure_Changedplatform "Geändertes Gleis" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-platform"} +String Zug1_Departure_Line "Linie" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#line"} +String Zug1_Departure_Plannedintermediatestations "Geplante Halte" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-intermediate-stations"} +String Zug1_Departure_Changedintermediatestations "Geänderte Halte" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-intermediate-stations"} +String Zug1_Departure_Plannedfinalstation "Geplanter Start-/Zielbahnhof" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-final-station"} +String Zug1_Departure_Changedfinalstation "Geänderter Start-/Zielbahnhof" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-final-station"} +String Zug1_Departure_Messages "Meldungen" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#messages"} +String Zug1_Departure_Plannedstatus "Geplanter Status" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-status"} +String Zug1_Departure_Changedstatus "Geänderter Status" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-status"} +DateTime Zug1_Departure_Cancellationtime "Stornierungs-Zeitpunkt" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#cancellation-time"} + +// Departure advanced information +String Zug1_Departure_Planneddistantendpoint "Geplanter entfernter Endpunkt" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-distant-endpoint"} +String Zug1_Departure_Changeddistantendpoint "Geänderter entfernter Endpunkt" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-distant-endpoint"} +String Zug1_Departure_Plannedpath "Geplante Route" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#planned-path"} +String Zug1_Departure_Changedpath "Geändert Route" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#changed-path"} +Number Zug1_Departure_Distantchange "Geänderter Zielbahnhof" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#distant-change"} +Switch Zug1_Departure_Hidden "Versteckt" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#hidden"} +String Zug1_Departure_Transition "Übergang" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#transition"} +String Zug1_Departure_Wings "Wings" (zug1Abfahrt) {channel="deutschebahn:train:timetableLehrte:lehrteZug1:departure#wings"} + +``` + +Example widget for displaying train details + +``` +uid: timetable_train_details +tags: + - card +props: + parameters: + - context: item + label: Geplante Zeit + name: planned_time + required: true + type: TEXT + - context: item + label: Geänderte Zeit + name: changed_time + required: true + type: TEXT + - context: item + label: Geplantes Gleis + name: planned_platform + required: true + type: TEXT + - context: item + label: Geändertes Gleis + name: changed_platform + required: true + type: TEXT + - context: item + label: Linie + name: line + required: true + type: TEXT + - context: item + label: Meldungen + name: messages + required: true + type: TEXT + - context: item + label: Geplanter Start-/Zielbahnhof + name: planned_final_station + required: true + type: TEXT + - context: item + label: Geplante Halte + name: planned_intermediate_stations + required: true + type: TEXT + - context: item + label: Geändeter Start-/Zielbahnhof + name: changed_final_station + required: true + type: TEXT + - context: item + label: Geänderte Halte + name: changed_intermediate_stations + required: true + type: TEXT + - context: item + label: Geänderter Status + name: changed_state + required: true + type: TEXT + - context: item + label: Kategorie + name: category + required: true + type: TEXT + - context: item + label: Nummer + name: number + required: true + type: TEXT + parameterGroups: [] +timestamp: Oct 14, 2021, 11:24:45 AM +component: f7-card +config: + style: + padding: 10px +slots: + default: + - component: f7-row + slots: + default: + - component: f7-col + config: + width: 15 + slots: + default: + - component: Label + config: + text: "=items[props.planned_time].displayState + (items[props.changed_time].state != 'NULL' && items[props.changed_time].state != items[props.planned_time].state ? ' (' + items[props.changed_time].displayState + ')' : '')" + style: + color: "=items[props.changed_time].state != 'NULL' && items[props.changed_time].state != items[props.planned_time].state ? 'red' : ''" + - component: f7-col + config: + width: 75 + slots: + default: + - component: Label + config: + text: "=(items[props.changed_state].state == 'c' ? 'Zug fällt aus - ' : '') + (items[props.messages].state != 'NULL' ? items[props.messages].state : '')" + style: + color: red + - component: f7-col + config: + width: 10 + slots: + default: + - component: Label + config: + text: "=items[props.changed_platform].state != 'NULL' ? items[props.changed_platform].state : items[props.planned_platform].state" + style: + color: "=items[props.changed_platform].state != 'NULL' ? 'red' : ''" + text-align: right + - component: f7-row + slots: + default: + - component: f7-col + config: + width: 15 + slots: + default: + - component: Label + config: + text: "=items[props.line].state != 'NULL' ? (items[props.category].state + ' ' + items[props.line].state) : (items[props.category].state + ' ' + items[props.number].state)" + - component: f7-col + config: + width: 50 + slots: + default: + - component: Label + config: + text: "=items[props.changed_intermediate_stations].state != 'NULL' ? items[props.changed_intermediate_stations].state : items[props.planned_intermediate_stations].state" + style: + color: "=items[props.changed_intermediate_stations].state != 'NULL' ? 'red' : ''" + - component: f7-col + config: + width: 35 + slots: + default: + - component: Label + config: + text: "=items[props.changed_final_station].state != 'NULL' ? items[props.changed_final_station].state : items[props.planned_final_station].state" + style: + color: "=items[props.changed_final_station].state != 'NULL' ? 'red' : ''" + font-weight: bold + text-align: right +``` + + +Using the widget for displaying the next four departures: + +![Departures Hannover HBF](doc/Abfahrten_HannoverHBF.png "openHAB page with four widgets displaying the next departures at Hannover HBF") diff --git a/bundles/org.openhab.binding.deutschebahn/doc/Abfahrten_HannoverHBF.png b/bundles/org.openhab.binding.deutschebahn/doc/Abfahrten_HannoverHBF.png new file mode 100644 index 0000000000000..2bc58850ac17d Binary files /dev/null and b/bundles/org.openhab.binding.deutschebahn/doc/Abfahrten_HannoverHBF.png differ diff --git a/bundles/org.openhab.binding.deutschebahn/pom.xml b/bundles/org.openhab.binding.deutschebahn/pom.xml new file mode 100644 index 0000000000000..48ddb0006be01 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.binding.deutschebahn + + openHAB Add-ons :: Bundles :: Deutsche Bahn Binding + + + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + 0.14.0 + + + generate-jaxb-sources + + generate + + + + + org.openhab.binding.deutschebahn.internal.timetable.dto + src/main/resources/xsd + true + en + false + true + + -Xxew + -Xxew:instantiate early + + + + com.github.jaxb-xew-plugin + jaxb-xew-plugin + 1.10 + + + + + + + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/feature/feature.xml b/bundles/org.openhab.binding.deutschebahn/src/main/feature/feature.xml new file mode 100644 index 0000000000000..4269910d79c44 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.deutschebahn/${project.version} + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java new file mode 100644 index 0000000000000..b5c6db1040b75 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AbstractDtoAttributeSelector.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.JaxbEntity; +import org.openhab.core.types.State; + +/** + * Accessor for attribute value of an DTO-Object. + * + * @author Sönke Küper - Initial contribution. + * + * @param type of value in Bean. + * @param type of value in Bean. + * @param type of state. + */ +@NonNullByDefault +public abstract class AbstractDtoAttributeSelector { + + private final Function getter; + private final BiConsumer setter; + private final Function getState; + private final String channelTypeName; + private final Class stateType; + + /** + * Creates an new {@link EventAttribute}. + * + * @param getter Function to get the raw value. + * @param setter Function to set the raw value. + * @param getState Function to get the Value as {@link State}. + */ + protected AbstractDtoAttributeSelector(final String channelTypeName, // + final Function getter, // + final BiConsumer setter, // + final Function getState, // + final Class stateType) { + this.channelTypeName = channelTypeName; + this.getter = getter; + this.setter = setter; + this.getState = getState; + this.stateType = stateType; + } + + /** + * Returns the type of the state value. + */ + public final Class getStateType() { + return this.stateType; + } + + /** + * Returns the name of the corresponding channel-type. + */ + public final String getChannelTypeName() { + return this.channelTypeName; + } + + /** + * Returns the {@link State} for the selected attribute from the given DTO object + * Returns null if the value is null. + */ + @Nullable + public final STATE_TYPE getState(final DTO_TYPE object) { + final VALUE_TYPE value = this.getValue(object); + if (value == null) { + return null; + } + return this.getState.apply(value); + } + + /** + * Returns the value for the selected attribute from the given DTO object. + */ + @Nullable + public final VALUE_TYPE getValue(final DTO_TYPE object) { + return this.getter.apply(object); + } + + /** + * Sets the value for the selected attribute in the given DTO object + */ + public final void setValue(final DTO_TYPE event, final VALUE_TYPE object) { + this.setter.accept(event, object); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java new file mode 100644 index 0000000000000..6c0d767066949 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/AttributeSelection.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.core.types.State; + +/** + * Selection of an attribute within an {@link TimetableStop} that provides a channel {@link State}. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public interface AttributeSelection { + + /** + * Returns the {@link State} that should be set for the channels'value for this attribute. + */ + @Nullable + public abstract State getState(TimetableStop stop); +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnBindingConstants.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnBindingConstants.java new file mode 100644 index 0000000000000..539b22e738f61 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnBindingConstants.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link DeutscheBahnBindingConstants} class defines common constants, which are used across the whole binding. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class DeutscheBahnBindingConstants { + + /** + * Binding-ID. + */ + public static final String BINDING_ID = "deutschebahn"; + + /** + * {@link ThingTypeUID} for Timetable-API Bridge. + */ + public static final ThingTypeUID TIMETABLE_TYPE = new ThingTypeUID(BINDING_ID, "timetable"); + + /** + * {@link ThingTypeUID} for Train. + */ + public static final ThingTypeUID TRAIN_TYPE = new ThingTypeUID(BINDING_ID, "train"); +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnHandlerFactory.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnHandlerFactory.java new file mode 100644 index 0000000000000..059f6b4dc53df --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnHandlerFactory.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import static org.openhab.binding.deutschebahn.internal.DeutscheBahnBindingConstants.TIMETABLE_TYPE; +import static org.openhab.binding.deutschebahn.internal.DeutscheBahnBindingConstants.TRAIN_TYPE; + +import java.util.Date; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Impl; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link DeutscheBahnHandlerFactory} is responsible for creating things and thing handlers. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.deutschebahn", service = ThingHandlerFactory.class) +public class DeutscheBahnHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(TIMETABLE_TYPE, TRAIN_TYPE); + + @Override + public boolean supportsThingType(final ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (TIMETABLE_TYPE.equals(thingTypeUID)) { + return new DeutscheBahnTimetableHandler((Bridge) thing, TimetablesV1Impl::new, Date::new); + } else if (TRAIN_TYPE.equals(thingTypeUID)) { + return new DeutscheBahnTrainHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java new file mode 100644 index 0000000000000..ee93c69650e1b --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DeutscheBahnTimetableConfiguration} for the Timetable bridge-type. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class DeutscheBahnTimetableConfiguration { + + /** + * Access-Token. + */ + public String accessToken = ""; + + /** + * evaNo of the station to be queried. + */ + public String evaNo = ""; + + /** + * Filter for timetable stops. + */ + public String trainFilter = ""; + + /** + * Returns the {@link TimetableStopFilter}. + */ + public TimetableStopFilter getTimetableStopFilter() { + return TimetableStopFilter.valueOf(this.trainFilter.toUpperCase()); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java new file mode 100644 index 0000000000000..616493a999157 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandler.java @@ -0,0 +1,302 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.TimetableLoader; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Api; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ApiFactory; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.core.io.net.http.HttpUtil; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +/** + * The {@link DeutscheBahnTimetableHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class DeutscheBahnTimetableHandler extends BaseBridgeHandler { + + /** + * Wrapper containing things grouped by their position and calculates the max. required position. + */ + private static final class GroupedThings { + + private int maxPosition = 0; + private final Map> thingsPerPosition = new HashMap<>(); + + public void addThing(Thing thing) { + if (isTrain(thing)) { + int position = thing.getConfiguration().as(DeutscheBahnTrainConfiguration.class).position; + this.maxPosition = Math.max(this.maxPosition, position); + List thingsAtPosition = this.thingsPerPosition.get(position); + if (thingsAtPosition == null) { + thingsAtPosition = new ArrayList<>(); + this.thingsPerPosition.put(position, thingsAtPosition); + } + thingsAtPosition.add(thing); + } + } + + /** + * Returns the things at the given position. + */ + @Nullable + public List getThingsAtPosition(int position) { + return this.thingsPerPosition.get(position); + } + + /** + * Returns the max. configured position. + */ + public int getMaxPosition() { + return this.maxPosition; + } + } + + private static final long UPDATE_INTERVAL_SECONDS = 30; + + private final Lock monitor = new ReentrantLock(); + private @Nullable ScheduledFuture updateJob; + + private final Logger logger = LoggerFactory.getLogger(DeutscheBahnTimetableHandler.class); + private @Nullable TimetableLoader loader; + + private TimetablesV1ApiFactory timetablesV1ApiFactory; + + private Supplier currentTimeProvider; + + /** + * Creates an new {@link DeutscheBahnTimetableHandler}. + */ + public DeutscheBahnTimetableHandler( // + final Bridge bridge, // + final TimetablesV1ApiFactory timetablesV1ApiFactory, // + final Supplier currentTimeProvider) { + super(bridge); + this.timetablesV1ApiFactory = timetablesV1ApiFactory; + this.currentTimeProvider = currentTimeProvider; + } + + private List loadTimetable() { + final TimetableLoader currentLoader = this.loader; + if (currentLoader == null) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR); + return Collections.emptyList(); + } + + try { + final List stops = currentLoader.getTimetableStops(); + this.updateStatus(ThingStatus.ONLINE); + return stops; + } catch (final IOException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return Collections.emptyList(); + } + } + + /** + * The Bridge-Handler does not handle any commands. + */ + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + } + + @Override + public void initialize() { + final DeutscheBahnTimetableConfiguration config = this.getConfigAs(DeutscheBahnTimetableConfiguration.class); + + try { + final TimetablesV1Api api = this.timetablesV1ApiFactory.create(config.accessToken, HttpUtil::executeUrl); + + final TimetableStopFilter stopFilter = config.getTimetableStopFilter(); + + final EventType eventSelection = stopFilter == TimetableStopFilter.ARRIVALS ? EventType.ARRIVAL + : EventType.ARRIVAL; + + this.loader = new TimetableLoader( // + api, // + stopFilter, // + eventSelection, // + currentTimeProvider, // + config.evaNo, // + 1); // will be updated on first call + + this.updateStatus(ThingStatus.UNKNOWN); + + this.scheduler.execute(() -> { + this.updateChannels(); + this.restartJob(); + }); + } catch (JAXBException | SAXException | URISyntaxException e) { + this.logger.error("Error initializing api", e); + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + @Override + public void dispose() { + this.stopUpdateJob(); + } + + /** + * Schedules an job that updates the timetable every 30 seconds. + */ + private void restartJob() { + this.logger.debug("Restarting jobs for bridge {}", this.getThing().getUID()); + this.monitor.lock(); + try { + this.stopUpdateJob(); + if (this.getThing().getStatus() == ThingStatus.ONLINE) { + this.updateJob = this.scheduler.scheduleWithFixedDelay(// + this::updateChannels, // + 0L, // + UPDATE_INTERVAL_SECONDS, // + TimeUnit.SECONDS // + ); + + this.logger.debug("Scheduled {} update of deutsche bahn timetable", this.updateJob); + } + } finally { + this.monitor.unlock(); + } + } + + /** + * Stops the update job. + */ + private void stopUpdateJob() { + this.monitor.lock(); + try { + final ScheduledFuture job = this.updateJob; + if (job != null) { + job.cancel(true); + } + this.updateJob = null; + } finally { + this.monitor.unlock(); + } + } + + private void updateChannels() { + final TimetableLoader currentLoader = this.loader; + if (currentLoader == null) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR); + return; + } + final GroupedThings groupedThings = this.groupThingsPerPosition(); + currentLoader.setStopCount(groupedThings.getMaxPosition()); + final List timetableStops = this.loadTimetable(); + if (timetableStops.isEmpty()) { + updateThingsToUndefined(groupedThings); + return; + } + + this.logger.debug("Retrieved {} timetable stops.", timetableStops.size()); + this.updateThings(groupedThings, timetableStops); + } + + /** + * No data was retrieved, so update all channel values to undefined. + */ + private void updateThingsToUndefined(GroupedThings groupedThings) { + for (List things : groupedThings.thingsPerPosition.values()) { + for (Thing thing : things) { + updateChannelsToUndefined(thing); + } + } + } + + private void updateChannelsToUndefined(Thing thing) { + for (Channel channel : thing.getChannels()) { + this.updateState(channel.getUID(), UnDefType.UNDEF); + } + } + + private void updateThings(GroupedThings groupedThings, final List timetableStops) { + int position = 1; + for (final TimetableStop stop : timetableStops) { + final List thingsAtPosition = groupedThings.getThingsAtPosition(position); + + if (thingsAtPosition != null) { + for (Thing thing : thingsAtPosition) { + final ThingHandler thingHandler = thing.getHandler(); + if (thingHandler != null) { + assert thingHandler instanceof DeutscheBahnTrainHandler; + ((DeutscheBahnTrainHandler) thingHandler).updateChannels(stop); + } + } + } + position++; + } + + // Update all things to undefined, for which no data was received. + while (position <= groupedThings.getMaxPosition()) { + final List thingsAtPosition = groupedThings.getThingsAtPosition(position); + if (thingsAtPosition != null) { + for (Thing thing : thingsAtPosition) { + updateChannelsToUndefined(thing); + } + } + position++; + } + } + + /** + * Returns an map containing the things grouped by timetable stop position. + */ + private GroupedThings groupThingsPerPosition() { + final GroupedThings groupedThings = new GroupedThings(); + for (Thing child : this.getThing().getThings()) { + groupedThings.addThing(child); + } + return groupedThings; + } + + private static boolean isTrain(Thing thing) { + final ThingTypeUID thingTypeUid = thing.getThingTypeUID(); + return thingTypeUid.equals(DeutscheBahnBindingConstants.TRAIN_TYPE); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainConfiguration.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainConfiguration.java new file mode 100644 index 0000000000000..196d6acca37ca --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DeutscheBahnTrainConfiguration} for the train thing type. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class DeutscheBahnTrainConfiguration { + + /** + * Position of the train in the timetable. + */ + public int position = 0; +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainHandler.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainHandler.java new file mode 100644 index 0000000000000..e04b95ce48c8f --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainHandler.java @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler for an Train-Thing in DeutscheBahn Binding. + * + * Represents an Train that arrives / departs at the station selected by the DeutscheBahnTimetable-Bridge. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class DeutscheBahnTrainHandler extends BaseThingHandler { + + /** + * Wraps the Channel-UID with the configured {@link AttributeSelection}. + */ + private final class ChannelWithConfig { + + private final ChannelUID channelUid; + private final AttributeSelection attributeSelection; + + /** + * Creates an new ChannelWithConfig. + * + * @param channelUid The UID of the channel + * @param configuration Configuration for the given channel. + * @param attributeSelection The attribute that provides the state that will be displayed. + */ + public ChannelWithConfig( // + final ChannelUID channelUid, // + final AttributeSelection attributeSelection) { + this.channelUid = channelUid; + this.attributeSelection = attributeSelection; + } + + /** + * Updates the value for the channel from given {@link TimetableStop}. + */ + public void updateChannelValue(final TimetableStop stop) { + final State newState = this.determineState(stop); + if (newState != null) { + DeutscheBahnTrainHandler.this.updateState(this.channelUid, newState); + } else { + DeutscheBahnTrainHandler.this.updateState(this.channelUid, UnDefType.NULL); + } + } + + @Nullable + private State determineState(final TimetableStop stop) { + return this.attributeSelection.getState(stop); + } + + @Override + public String toString() { + return this.channelUid.toString(); + } + } + + private final Logger logger = LoggerFactory.getLogger(DeutscheBahnTrainHandler.class); + private final List configuredChannels = new ArrayList<>(); + + /** + * Creates an new {@link DeutscheBahnTrainHandler}. + */ + public DeutscheBahnTrainHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + this.updateStatus(ThingStatus.UNKNOWN); + + if (this.getBridge() == null) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Please select bridge"); + return; + } + + this.createChannelMapping(); + this.updateStatus(ThingStatus.ONLINE); + } + + private void createChannelMapping() { + this.configuredChannels.clear(); + for (Channel channel : this.getThing().getChannelsOfGroup("trip")) { + this.createTripChannelConfiguration(channel); + } + for (Channel channel : this.getThing().getChannelsOfGroup("arrival")) { + this.createEventChannelConfiguration(EventType.ARRIVAL, channel); + } + for (Channel channel : this.getThing().getChannelsOfGroup("departure")) { + this.createEventChannelConfiguration(EventType.DEPARTURE, channel); + } + this.logger.debug("Created {} configured channels for thing {}.", this.configuredChannels.size(), + this.getThing().getUID()); + } + + /** + * Creates an {@link ChannelWithConfig} for an channel that represents an attribute of an + * {@link org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel}. + */ + private void createTripChannelConfiguration(Channel channel) { + final ChannelUID channelUid = channel.getUID(); + final String attributeName = getAttributeName(channelUid); + final TripLabelAttribute attribute = TripLabelAttribute.getByChannelName(attributeName); + if (attribute == null) { + this.logger.warn("Could not find trip attribute {} of channel: {} .", attribute, channelUid.getId()); + return; + } + final ChannelWithConfig channelWithConfig = new ChannelWithConfig( // + channelUid, // + attribute); + this.configuredChannels.add(channelWithConfig); + } + + /** + * Creates the {@link ChannelWithConfig} for an channel that represents an attribute of an + * {@link org.openhab.binding.deutschebahn.internal.timetable.dto.Event}.} + */ + private void createEventChannelConfiguration(EventType eventType, Channel channel) { + final ChannelUID channelUid = channel.getUID(); + final String attributeName = getAttributeName(channelUid); + final EventAttribute attribute = EventAttribute.getByChannelName(attributeName, eventType); + if (attribute == null) { + this.logger.warn("Could not find event attribute {} of channel: {} .", attribute, channelUid.getId()); + return; + } + final ChannelWithConfig channelWithConfig = new ChannelWithConfig( // + channelUid, // + new EventAttributeSelection(eventType, attribute)); + this.configuredChannels.add(channelWithConfig); + } + + /** + * Strips the attribute name from the channel-UID. + */ + private static String getAttributeName(ChannelUID channelUid) { + final String channelId = channelUid.getId(); + int hashIndex = channelId.indexOf("#"); + assert hashIndex > 0; + final String attributeName = channelId.substring(hashIndex + 1); + return attributeName; + } + + /** + * Does not handle any commands. + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + /** + * Updates the value for the channels of this train from the given {@link TimetableStop}. + */ + void updateChannels(TimetableStop stop) { + for (ChannelWithConfig channel : this.configuredChannels) { + channel.updateChannelValue(stop); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java new file mode 100644 index 0000000000000..26ad3e5a098ca --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttribute.java @@ -0,0 +1,427 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.EventStatus; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Message; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; + +/** + * Selector for the Attribute of an {@link Event}. + * + * chapter "1.2.11 Event" in Technical Interface Description for external Developers + * + * @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&#tab1 + * + * @author Sönke Küper - initial contribution + * + * @param type of value in Bean. + * @param type of state. + */ +@NonNullByDefault +public final class EventAttribute + extends AbstractDtoAttributeSelector { + + /** + * Planned Path. + */ + public static final EventAttribute PPTH = new EventAttribute<>("planned-path", Event::getPpth, + Event::setPpth, StringType::new, StringType.class); + + /** + * Changed Path. + */ + public static final EventAttribute CPTH = new EventAttribute<>("changed-path", Event::getCpth, + Event::setCpth, StringType::new, StringType.class); + /** + * Planned platform. + */ + public static final EventAttribute PP = new EventAttribute<>("planned-platform", Event::getPp, + Event::setPp, StringType::new, StringType.class); + /** + * Changed platform. + */ + public static final EventAttribute CP = new EventAttribute<>("changed-platform", Event::getCp, + Event::setCp, StringType::new, StringType.class); + /** + * Planned time. + */ + public static final EventAttribute PT = new EventAttribute<>("planned-time", + getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType, DateTimeType.class); + /** + * Changed time. + */ + public static final EventAttribute CT = new EventAttribute<>("changed-time", + getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType, DateTimeType.class); + /** + * Planned status. + */ + public static final EventAttribute PS = new EventAttribute<>("planned-status", + Event::getPs, Event::setPs, EventAttribute::fromEventStatus, StringType.class); + /** + * Changed status. + */ + public static final EventAttribute CS = new EventAttribute<>("changed-status", + Event::getCs, Event::setCs, EventAttribute::fromEventStatus, StringType.class); + /** + * Hidden. + */ + public static final EventAttribute HI = new EventAttribute<>("hidden", Event::getHi, + Event::setHi, EventAttribute::parseHidden, OnOffType.class); + /** + * Cancellation time. + */ + public static final EventAttribute CLT = new EventAttribute<>("cancellation-time", + getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType, DateTimeType.class); + /** + * Wing. + */ + public static final EventAttribute WINGS = new EventAttribute<>("wings", Event::getWings, + Event::setWings, StringType::new, StringType.class); + /** + * Transition. + */ + public static final EventAttribute TRA = new EventAttribute<>("transition", Event::getTra, + Event::setTra, StringType::new, StringType.class); + /** + * Planned distant endpoint. + */ + public static final EventAttribute PDE = new EventAttribute<>("planned-distant-endpoint", + Event::getPde, Event::setPde, StringType::new, StringType.class); + /** + * Changed distant endpoint. + */ + public static final EventAttribute CDE = new EventAttribute<>("changed-distant-endpoint", + Event::getCde, Event::setCde, StringType::new, StringType.class); + /** + * Distant change. + */ + public static final EventAttribute DC = new EventAttribute<>("distant-change", Event::getDc, + Event::setDc, DecimalType::new, DecimalType.class); + /** + * Line. + */ + public static final EventAttribute L = new EventAttribute<>("line", Event::getL, Event::setL, + StringType::new, StringType.class); + + /** + * Messages. + */ + public static final EventAttribute, StringType> MESSAGES = new EventAttribute<>("messages", + EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages, StringType.class); + + /** + * Planned Start station. + */ + public static final EventAttribute PLANNED_START_STATION = new EventAttribute<>( + "planned-start-station", EventAttribute.getSingleStationFromPath(Event::getPpth, true), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Planned Previous stations. + */ + public static final EventAttribute PLANNED_PREVIOUS_STATIONS = new EventAttribute<>( + "planned-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, true), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Planned Target station. + */ + public static final EventAttribute PLANNED_TARGET_STATION = new EventAttribute<>( + "planned-target-station", EventAttribute.getSingleStationFromPath(Event::getPpth, false), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Planned Following stations. + */ + public static final EventAttribute PLANNED_FOLLOWING_STATIONS = new EventAttribute<>( + "planned-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, false), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Changed Start station. + */ + public static final EventAttribute CHANGED_START_STATION = new EventAttribute<>( + "changed-start-station", EventAttribute.getSingleStationFromPath(Event::getCpth, true), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Changed Previous stations. + */ + public static final EventAttribute CHANGED_PREVIOUS_STATIONS = new EventAttribute<>( + "changed-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, true), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Changed Target station. + */ + public static final EventAttribute CHANGED_TARGET_STATION = new EventAttribute<>( + "changed-target-station", EventAttribute.getSingleStationFromPath(Event::getCpth, false), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * Changed Following stations. + */ + public static final EventAttribute CHANGED_FOLLOWING_STATIONS = new EventAttribute<>( + "changed-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, false), + EventAttribute.voidSetter(), StringType::new, StringType.class); + + /** + * List containing all known {@link EventAttribute}. + */ + public static final List> ALL_ATTRIBUTES = Arrays.asList(PPTH, CPTH, PP, CP, PT, CT, PS, CS, + HI, CLT, WINGS, TRA, PDE, CDE, DC, L, MESSAGES); + + private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyMMddHHmm"); + + /** + * Creates an new {@link EventAttribute}. + * + * @param getter Function to get the raw value. + * @param setter Function to set the raw value. + * @param getState Function to get the Value as {@link State}. + */ + private EventAttribute(final String channelTypeName, // + final Function getter, // + final BiConsumer setter, // + final Function getState, // + final Class stateType) { + super(channelTypeName, getter, setter, getState, stateType); + } + + private static StringType fromEventStatus(final EventStatus value) { + return new StringType(value.value()); + } + + private static OnOffType parseHidden(@Nullable Integer value) { + return OnOffType.from(value != null && value == 1); + } + + private static Function getDate(final Function getValue) { + return (final Event event) -> { + return parseDate(getValue.apply(event)); + }; + } + + private static BiConsumer setDate(final BiConsumer setter) { + return (final Event event, final Date value) -> { + synchronized (DATETIME_FORMAT) { + String formattedDate = DATETIME_FORMAT.format(value); + setter.accept(event, formattedDate); + } + }; + } + + private static void setMessages(Event event, List messages) { + event.getM().clear(); + event.getM().addAll(messages); + } + + @Nullable + private static synchronized Date parseDate(@Nullable final String dateValue) { + if ((dateValue == null) || dateValue.isEmpty()) { + return null; + } + try { + synchronized (DATETIME_FORMAT) { + return DATETIME_FORMAT.parse(dateValue); + } + } catch (final ParseException e) { + return null; + } + } + + @Nullable + private static DateTimeType createDateTimeType(final @Nullable Date value) { + if (value == null) { + return null; + } else { + final ZonedDateTime d = ZonedDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault()); + return new DateTimeType(d); + } + } + + /** + * Maps the status codes from the messages into status texts. + */ + @Nullable + private static StringType mapMessages(final @Nullable List messages) { + if (messages == null || messages.isEmpty()) { + return StringType.EMPTY; + } else { + final String messageTexts = messages // + .stream()// + .filter((Message message) -> message.getC() != null) // + .map(Message::getC) // + .distinct() // + .map(MessageCodes::getMessage) // + .filter((String messageText) -> !messageText.isEmpty()) // + .collect(Collectors.joining(" - ")); + + return new StringType(messageTexts); + } + } + + private static Function> getMessages() { + return new Function>() { + + @Override + public @Nullable List apply(Event t) { + if (t.getM().isEmpty()) { + return null; + } else { + return t.getM(); + } + } + }; + } + + /** + * Returns an single station from an path value (i.e. pipe separated value of stations). + * + * @param getPath Getter for the path. + * @param returnFirst if true the first value will be returned, false will return the last + * value. + */ + private static Function getSingleStationFromPath( + final Function getPath, boolean returnFirst) { + return (final Event event) -> { + String path = getPath.apply(event); + if (path == null || path.isEmpty()) { + return null; + } + + final String[] stations = splitPath(path); + if (returnFirst) { + return stations[0]; + } else { + return stations[stations.length - 1]; + } + }; + } + + /** + * Returns all intermediate stations from an path. The first or last station will be omitted. The values will be + * separated by an single dash -. + * + * @param getPath Getter for the path. + * @param removeFirst if true the first value will be removed, false will remove the last + * value. + */ + private static Function getIntermediateStationsFromPath( + final Function getPath, boolean removeFirst) { + return (final Event event) -> { + final String path = getPath.apply(event); + if (path == null || path.isEmpty()) { + return null; + } + final String[] stationValues = splitPath(path); + Stream stations = Arrays.stream(stationValues); + if (removeFirst) { + stations = stations.skip(1); + } else { + stations = stations.limit(stationValues.length - 1); + } + return stations.collect(Collectors.joining(" - ")); + }; + } + + /** + * Setter that does nothing. + * Used for derived attributes that can't be set. + */ + private static BiConsumer voidSetter() { + return new BiConsumer() { + + @Override + public void accept(Event t, VALUE_TYPE u) { + } + }; + } + + private static String[] splitPath(final String path) { + return path.split("\\|"); + } + + /** + * Returns an {@link EventAttribute} for the given channel-type and {@link EventType}. + */ + @Nullable + public static EventAttribute getByChannelName(final String channelName, EventType eventType) { + switch (channelName) { + case "planned-path": + return PPTH; + case "changed-path": + return CPTH; + case "planned-platform": + return PP; + case "changed-platform": + return CP; + case "planned-time": + return PT; + case "changed-time": + return CT; + case "planned-status": + return PS; + case "changed-status": + return CS; + case "hidden": + return HI; + case "cancellation-time": + return CLT; + case "wings": + return WINGS; + case "transition": + return TRA; + case "planned-distant-endpoint": + return PDE; + case "changed-distant-endpoint": + return CDE; + case "distant-change": + return DC; + case "line": + return L; + case "messages": + return MESSAGES; + case "planned-final-station": + return eventType == EventType.ARRIVAL ? PLANNED_START_STATION : PLANNED_TARGET_STATION; + case "planned-intermediate-stations": + return eventType == EventType.ARRIVAL ? PLANNED_PREVIOUS_STATIONS : PLANNED_FOLLOWING_STATIONS; + case "changed-final-station": + return eventType == EventType.ARRIVAL ? CHANGED_START_STATION : CHANGED_TARGET_STATION; + case "changed-intermediate-stations": + return eventType == EventType.ARRIVAL ? CHANGED_PREVIOUS_STATIONS : CHANGED_FOLLOWING_STATIONS; + default: + return null; + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java new file mode 100644 index 0000000000000..51224949f9a10 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventAttributeSelection.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Selection that returns the value of an {@link EventAttribute}. The required {@link Event} is + * selected with the given {@link EventType}. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public final class EventAttributeSelection implements AttributeSelection { + + private final EventType eventType; + private final EventAttribute eventAttribute; + + /** + * Creates an new {@link EventAttributeSelection}. + */ + public EventAttributeSelection(EventType eventType, EventAttribute eventAttribute) { + this.eventType = eventType; + this.eventAttribute = eventAttribute; + } + + @Nullable + @Override + public State getState(TimetableStop stop) { + final Event event = eventType.getEvent(stop); + if (event == null) { + return UnDefType.UNDEF; + } else { + return this.eventAttribute.getState(event); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventType.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventType.java new file mode 100644 index 0000000000000..a8422aabced52 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/EventType.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; + +/** + * Type of an {@link Event} within an {@link TimetableStop}. + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public enum EventType { + + /** + * Selects the Arrival-Element (i.e. ar). + */ + ARRIVAL(TimetableStop::getAr, TimetableStop::getDp), + + /** + * Selects the departure element (i.e. dp). + */ + DEPARTURE(TimetableStop::getDp, TimetableStop::getAr); + + private final Function getter; + private final Function oppositeGetter; + + private EventType(Function getter, + Function oppositeGetter) { + this.getter = getter; + this.oppositeGetter = oppositeGetter; + } + + /** + * Returns the selected event from the given {@link TimetableStop}. + */ + @Nullable + public final Event getEvent(TimetableStop stop) { + return this.getter.apply(stop); + } + + /** + * Returns the opposite event from the given {@link TimetableStop}. + */ + @Nullable + public final Event getOppositeEvent(TimetableStop stop) { + return this.oppositeGetter.apply(stop); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/MessageCodes.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/MessageCodes.java new file mode 100644 index 0000000000000..fae86487fed87 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/MessageCodes.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class containing the mappings for all message status codes. + * + * chapter "2 List of all codes" in Technical Interface Description for external Developers + * + * @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&#tab1 + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public final class MessageCodes { + + private static Map codes = new HashMap<>(); + static { + codes.put(0, "keine Verspätungsbegründung"); + codes.put(2, "Polizeiliche Ermittlung"); + codes.put(3, "Feuerwehreinsatz an der Strecke"); + codes.put(4, "kurzfristiger Personalausfall"); + codes.put(5, "ärztliche Versorgung eines Fahrgastes"); + codes.put(6, "Betätigen der Notbremse"); + codes.put(7, "Personen im Gleis"); + codes.put(8, "Notarzteinsatz am Gleis"); + codes.put(9, "Streikauswirkungen"); + codes.put(10, "Tiere im Gleis"); + codes.put(11, "Unwetter"); + codes.put(12, "Warten auf ein verspätetes Schiff"); + codes.put(13, "Pass- und Zollkontrolle"); + codes.put(14, "Technische Störung am Bahnhof"); + codes.put(15, "Beeinträchtigung durch Vandalismus"); + codes.put(16, "Entschärfung einer Fliegerbombe"); + codes.put(17, "Beschädigung einer Brücke"); + codes.put(18, "umgestürzter Baum im Gleis"); + codes.put(19, "Unfall an einem Bahnübergang"); + codes.put(20, "Tiere im Gleis"); + codes.put(21, "Warten auf Fahrgäste aus einem anderen Zug"); + codes.put(22, "Witterungsbedingte Störung"); + codes.put(23, "Feuerwehreinsatz auf Bahngelände"); + codes.put(24, "Verspätung im Ausland"); + codes.put(25, "Warten auf weitere Wagen"); + codes.put(28, "Gegenstände im Gleis"); + codes.put(29, "Ersatzverkehr mit Bus ist eingerichtet"); + codes.put(31, "Bauarbeiten"); + codes.put(32, "Verzögerung beim Ein-/Ausstieg"); + codes.put(33, "Oberleitungsstörung"); + codes.put(34, "Signalstörung"); + codes.put(35, "Streckensperrung"); + codes.put(36, "technische Störung am Zug"); + codes.put(38, "technische Störung an der Strecke"); + codes.put(39, "Anhängen von zusätzlichen Wagen"); + codes.put(40, "Stellwerksstörung /-ausfall"); + codes.put(41, "Störung an einem Bahnübergang"); + codes.put(42, "außerplanmäßige Geschwindigkeitsbeschränkung"); + codes.put(43, "Verspätung eines vorausfahrenden Zuges"); + codes.put(44, "Warten auf einen entgegenkommenden Zug"); + codes.put(45, "Überholung"); + codes.put(46, "Warten auf freie Einfahrt"); + codes.put(47, "verspätete Bereitstellung des Zuges"); + codes.put(48, "Verspätung aus vorheriger Fahrt"); + codes.put(55, "technische Störung an einem anderen Zug"); + codes.put(56, "Warten auf Fahrgäste aus einem Bus"); + codes.put(57, "Zusätzlicher Halt zum Ein-/Ausstieg für Reisende"); + codes.put(58, "Umleitung des Zuges"); + codes.put(59, "Schnee und Eis"); + codes.put(60, "Reduzierte Geschwindigkeit wegen Sturm"); + codes.put(61, "Türstörung"); + codes.put(62, "behobene technische Störung am Zug"); + codes.put(63, "technische Untersuchung am Zug"); + codes.put(64, "Weichenstörung"); + codes.put(65, "Erdrutsch"); + codes.put(66, "Hochwasser"); + codes.put(70, "WLAN im gesamten Zug nicht verfügbar"); + codes.put(71, "WLAN in einem/mehreren Wagen nicht verfügbar"); + codes.put(72, "Info-/Entertainment nicht verfügbar"); + codes.put(73, "Heute: Mehrzweckabteil vorne"); + codes.put(74, "Heute: Mehrzweckabteil hinten"); + codes.put(75, "Heute: 1. Klasse vorne"); + codes.put(76, "Heute: 1. Klasse hinten"); + codes.put(77, "ohne 1. Klasse"); + codes.put(79, "ohne Mehrzweckabteil"); + codes.put(80, "andere Reihenfolge der Wagen"); + codes.put(82, "mehrere Wagen fehlen"); + codes.put(83, "Störung fahrzeuggebundene Einstiegshilfe"); + codes.put(84, "Zug verkehrt richtig gereiht"); + codes.put(85, "ein Wagen fehlt"); + codes.put(86, "gesamter Zug ohne Reservierung"); + codes.put(87, "einzelne Wagen ohne Reservierung"); + codes.put(88, "keine Qualitätsmängel"); + codes.put(89, "Reservierungen sind wieder vorhanden"); + codes.put(90, "kein gastronomisches Angebot"); + codes.put(91, "fehlende Fahrradbeförderung"); + codes.put(92, "Eingeschränkte Fahrradbeförderung"); + codes.put(93, "keine behindertengerechte Einrichtung"); + codes.put(94, "Ersatzbewirtschaftung"); + codes.put(95, "Ohne behindertengerechtes WC"); + codes.put(96, "Überbesetzung mit Kulanzleistungen"); + codes.put(97, "Überbesetzung ohne Kulanzleistungen"); + codes.put(98, "sonstige Qualitätsmängel"); + codes.put(99, "Verzögerungen im Betriebsablauf"); + } + + private MessageCodes() { + } + + /** + * Returns the message for the given code or emtpy string if not present. + */ + public static String getMessage(final int code) { + final String message = codes.get(code); + if (message == null) { + return ""; + } else { + return message; + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java new file mode 100644 index 0000000000000..e0256f42453e9 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TimetableStopFilter.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; + +/** + * Filter that selects {@link TimetableStop}, if they have an departure or an arrival element (or both). + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +public enum TimetableStopFilter implements Predicate { + + /** + * Selects all entries. + */ + ALL { + @Override + public boolean test(TimetableStop t) { + return true; + } + }, + + /** + * Selects only stops with an departure. + */ + DEPARTURES { + @Override + public boolean test(TimetableStop t) { + return t.getDp() != null; + } + }, + + /** + * Selects only stops with an arrival. + */ + ARRIVALS { + @Override + public boolean test(TimetableStop t) { + return t.getAr() != null; + } + }; +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java new file mode 100644 index 0000000000000..2acbaeaab5e40 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/TripLabelAttribute.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TripType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Selection that returns the value of an {@link TripLabel}. + * + * chapter "1.2.7 TripLabel" in Technical Interface Description for external Developers + * + * @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&#tab1 + * + * @author Sönke Küper - Initial contribution. + * + * @param type of value in Bean. + * @param type of state. + */ +@NonNullByDefault +public final class TripLabelAttribute extends + AbstractDtoAttributeSelector implements AttributeSelection { + + /** + * Trip category. + */ + public static final TripLabelAttribute C = new TripLabelAttribute<>("category", TripLabel::getC, + TripLabel::setC, StringType::new, StringType.class); + + /** + * Number. + */ + public static final TripLabelAttribute N = new TripLabelAttribute<>("number", TripLabel::getN, + TripLabel::setN, StringType::new, StringType.class); + + /** + * Filter flags. + */ + public static final TripLabelAttribute F = new TripLabelAttribute<>("filter-flags", + TripLabel::getF, TripLabel::setF, StringType::new, StringType.class); + /** + * Trip Type. + */ + public static final TripLabelAttribute T = new TripLabelAttribute<>("trip-type", + TripLabel::getT, TripLabel::setT, TripLabelAttribute::fromTripType, StringType.class); + /** + * Owner. + */ + public static final TripLabelAttribute O = new TripLabelAttribute<>("owner", TripLabel::getO, + TripLabel::setO, StringType::new, StringType.class); + + /** + * Creates an new {@link TripLabelAttribute}. + * + * @param getter Function to get the raw value. + * @param setter Function to set the raw value. + * @param getState Function to get the Value as {@link State}. + */ + private TripLabelAttribute(final String channelTypeName, // + final Function getter, // + final BiConsumer setter, // + final Function getState, // + final Class stateType) { + super(channelTypeName, getter, setter, getState, stateType); + } + + @Nullable + @Override + public State getState(TimetableStop stop) { + if (stop.getTl() == null) { + return UnDefType.UNDEF; + } + return super.getState(stop.getTl()); + } + + private static StringType fromTripType(final TripType value) { + return new StringType(value.value()); + } + + /** + * Returns an {@link TripLabelAttribute} for the given channel-name. + */ + @Nullable + public static TripLabelAttribute getByChannelName(final String channelName) { + switch (channelName) { + case "category": + return C; + case "number": + return N; + case "filter-flags": + return F; + case "trip-type": + return T; + case "owner": + return O; + default: + return null; + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java new file mode 100644 index 0000000000000..96d1cf38639dc --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoader.java @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.EventAttribute; +import org.openhab.binding.deutschebahn.internal.EventType; +import org.openhab.binding.deutschebahn.internal.TimetableStopFilter; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.core.library.types.DateTimeType; + +/** + * Helper for loading the required amount of {@link TimetableStop} via an {@link TimetablesV1Api}. + * This consists of a series of calls. + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public final class TimetableLoader { + + // The api provides at most 18 hours in advance. + private static final int MAX_ADVANCE_HOUR = 18; + + // The recent changes only contains all changes done within the last 2 minutes. + private static final int MAX_RECENT_CHANGE_UPDATE = 120; + + // The min. request interval for recent changes is 30 seconds. + private static final int MIN_RECENT_CHANGE_INTERVAL = 30; + + // Cache containing the TimetableStops per ID + private final Map cachedStopsPerId; + private final Map cachedChanges; + + private final TimetablesV1Api api; + private final TimetableStopFilter stopFilter; + private final TimetableStopComparator comparator; + private final Supplier currentTimeProvider; + private int stopCount; + + private final String evaNo; + + @Nullable + private Date lastRequestedPlan; + @Nullable + private Date lastRequestedChanges; + + /** + * Creates an new {@link TimetableLoader}. + * + * @param api {@link TimetablesV1Api} to use. + * @param stopFilter Filter for selection of loaded {@link TimetableStop}. + * @param requestedStopCount Count of stops to be loaded on each call. + * @param currentTimeProvider {@link Supplier} for the current time. + */ + public TimetableLoader(final TimetablesV1Api api, final TimetableStopFilter stopFilter, final EventType eventToSort, + final Supplier currentTimeProvider, final String evaNo, final int requestedStopCount) { + this.api = api; + this.stopFilter = stopFilter; + this.currentTimeProvider = currentTimeProvider; + this.evaNo = evaNo; + this.stopCount = requestedStopCount; + this.comparator = new TimetableStopComparator(eventToSort); + this.cachedStopsPerId = new HashMap<>(); + this.cachedChanges = new HashMap<>(); + this.lastRequestedChanges = null; + this.lastRequestedPlan = null; + } + + /** + * Sets the count of needed {@link TimetableStop} that is required at each call of {@link #getTimetableStops()}. + */ + public void setStopCount(int stopCount) { + this.stopCount = stopCount; + } + + /** + * Updates the cache with current data from plan and changes and returns the {@link TimetableStop}. + */ + public List getTimetableStops() throws IOException { + this.updateCache(); + final List result = new ArrayList<>(this.cachedStopsPerId.values()); + Collections.sort(result, this.comparator); + return result; + } + + /** + * Updates the cached {@link TimetableStop} to ensure that the requested amount of stops is available. + */ + private void updateCache() throws IOException { + final Date currentTime = this.currentTimeProvider.get(); + + // First update the changes. This will merge them into the existing plan data + // or cache them, if no corresponding stop is available. + this.updateChanges(currentTime); + + // Remove all stops that are in the past + this.removeOldStops(currentTime); + + // Finally fill up plan until required amount of data is available. + this.updatePlan(currentTime); + } + + /** + * Removes all stops from the cache with planned and changed time after the current time. + */ + private void removeOldStops(final Date currentTime) { + final Iterator> it = this.cachedStopsPerId.entrySet().iterator(); + while (it.hasNext()) { + final Entry currentEntry = it.next(); + final TimetableStop stop = currentEntry.getValue(); + + // Remove entry if planned and changed time are in the past + if (isInPast(stop, currentTime)) { + it.remove(); + } + } + } + + /** + * Returns true if the planned and changed time from arrival and departure are in the past. + */ + private static boolean isInPast(TimetableStop stop, Date currentTime) { + return isBefore(EventAttribute.PT, stop.getAr(), currentTime) // + && isBefore(EventAttribute.CT, stop.getAr(), currentTime) // + && isBefore(EventAttribute.PT, stop.getDp(), currentTime) // + && isBefore(EventAttribute.PT, stop.getDp(), currentTime); + } + + /** + * Checks if the value of the given {@link EventAttribute} is either null or before + * the given compareTime. + * If the {@link Event} is null it will return true. + */ + private static boolean isBefore( // + final EventAttribute attribute, // + final @Nullable Event event, // + final Date toCompare) { + if (event == null) { + return true; + } + final Date value = attribute.getValue(event); + if (value == null) { + return true; + } else { + return value.before(toCompare); + } + } + + /** + * Checks if enough plan entries are available and loads them from the backing {@link TimetablesV1Api} if required. + */ + private void updatePlan(final Date currentTime) throws IOException { + // If enough stops are available in cache do nothing. + if (this.cachedStopsPerId.size() >= this.stopCount) { + return; + } + + // start requesting at last request time. + final GregorianCalendar requestTime = new GregorianCalendar(); + if (this.lastRequestedPlan != null) { + requestTime.setTime(this.lastRequestedPlan); + requestTime.set(Calendar.HOUR_OF_DAY, requestTime.get(Calendar.HOUR_OF_DAY) + 1); + } else { + requestTime.setTime(currentTime); + } + + // Determine the max. time for which an plan is available + final GregorianCalendar maxRequestTime = new GregorianCalendar(); + maxRequestTime.setTime(currentTime); + maxRequestTime.set(Calendar.HOUR_OF_DAY, maxRequestTime.get(Calendar.HOUR_OF_DAY) + MAX_ADVANCE_HOUR); + + // load until required amount of stops is present or no more data is available. + while ((this.cachedStopsPerId.size() < this.stopCount) && requestTime.before(maxRequestTime)) { + final Timetable timetable = this.api.getPlan(this.evaNo, requestTime.getTime()); + this.lastRequestedPlan = requestTime.getTime(); + + // Filter only stops that are selected by given filter + final List stops = timetable // + .getS() // + .stream() // + .filter(this.stopFilter) // + .collect(Collectors.toList()); + + // Merge the loaded stops with the cached changes and put them into the plan cache. + this.processLoadedPlan(stops, currentTime); + + // Move request time one hour ahead. + requestTime.set(Calendar.HOUR_OF_DAY, requestTime.get(Calendar.HOUR_OF_DAY) + 1); + } + } + + /** + * Merges the loaded plan stops with the previously cached changes. + * The result will be cached as plan data, if not in the past. + */ + private void processLoadedPlan(List stops, Date currentTime) { + for (final TimetableStop stop : stops) { + + // Check if an change for the stop was cached and apply it + final TimetableStop change = this.cachedChanges.remove(stop.getId()); + if (change != null) { + TimetableStopMerger.merge(stop, change); + } + + // Check if stop is in past after applying changes and put + // into cached plan if not. + if (!isInPast(stop, currentTime)) { + this.cachedStopsPerId.put(stop.getId(), stop); + } + } + } + + /** + * Loads the changes from the api and merges them into the cached plan entries. + */ + private void updateChanges(final Date currentTime) throws IOException { + final List changes = this.loadChanges(currentTime); + this.processChanges(changes); + } + + /** + * Merges the given {@link TimetableStop} into the cached plan. + * If no stop in the plan for the change exist it will be put into the changes cache. + */ + private void processChanges(final List changes) { + for (final TimetableStop change : changes) { + + final TimetableStop existingEntry = this.cachedStopsPerId.get(change.getId()); + if (existingEntry != null) { + TimetableStopMerger.merge(existingEntry, change); + } else { + this.cachedChanges.put(change.getId(), change); + } + } + } + + /** + * Loads the full or recent changes depending on last request time. + */ + private List loadChanges(final Date currentTime) throws IOException { + boolean fullChanges = false; + final long secondsSinceLastUpdate = this.getSecondsSinceLastRequestedChanges(currentTime); + + // The recent changes are updated every 30 seconds, so if last update is less than 30 seconds do nothing. + if (secondsSinceLastUpdate < MIN_RECENT_CHANGE_INTERVAL) { + return Collections.emptyList(); + } + + // The recent changes are only available for 120 seconds, so if last update is older perform an full update. + if (secondsSinceLastUpdate >= MAX_RECENT_CHANGE_UPDATE) { + fullChanges = true; + } + + Timetable changes; + if (fullChanges) { + changes = this.api.getFullChanges(this.evaNo); + } else { + changes = this.api.getRecentChanges(this.evaNo); + } + this.lastRequestedChanges = currentTime; + return changes.getS(); + } + + @SuppressWarnings("null") + private long getSecondsSinceLastRequestedChanges(final Date currentTime) { + if (this.lastRequestedChanges == null) { + return Long.MAX_VALUE; + } else { + return ChronoUnit.SECONDS.between(this.lastRequestedChanges.toInstant(), currentTime.toInstant()); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStopComparator.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStopComparator.java new file mode 100644 index 0000000000000..520430fb61534 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStopComparator.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.util.Comparator; +import java.util.Date; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.deutschebahn.internal.EventAttribute; +import org.openhab.binding.deutschebahn.internal.EventType; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; + +/** + * {@link Comparator} that sorts the {@link TimetableStop} according planned date and time. + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public class TimetableStopComparator implements Comparator { + + private final EventType eventSelection; + + /** + * Creates an new {@link TimetableStopComparator} that sorts {@link TimetableStop} according the Event selected + * selected by the given {@link EventType}. + */ + public TimetableStopComparator(EventType eventSelection) { + this.eventSelection = eventSelection; + } + + @Override + public int compare(TimetableStop o1, TimetableStop o2) { + return determinePlannedDate(o1, this.eventSelection).compareTo(determinePlannedDate(o2, this.eventSelection)); + } + + /** + * Returns the planned-Time for the given {@link TimetableStop}. + * The time will be returned from the {@link Event} selected by the given {@link EventType}. + * If the {@link TimetableStop} has no according {@link Event} the other Event will be used. + */ + private static Date determinePlannedDate(TimetableStop stop, EventType eventSelection) { + Event selectedEvent = eventSelection.getEvent(stop); + if (selectedEvent == null) { + selectedEvent = eventSelection.getOppositeEvent(stop); + } + if (selectedEvent == null) { + throw new AssertionError("one event is always present"); + } + final Date value = EventAttribute.PT.getValue(selectedEvent); + if (value == null) { + throw new AssertionError("planned time cannot be null"); + } + return value; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStopMerger.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStopMerger.java new file mode 100644 index 0000000000000..e5ca984b8be64 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStopMerger.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.EventAttribute; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; + +/** + * Utility for merging timetable stops. + * This is required, thus first only the plan is returned from the API and afterwards the loaded timetable-stops must be + * merged with the fetched changes. + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +final class TimetableStopMerger { + + /** + * Merges the {@link TimetableStop} inplace to the first TimetableStop. + */ + public static void merge(final TimetableStop first, final TimetableStop second) { + mergeStopAttributes(first, second); + } + + /** + * Updates all values from the second {@link TimetableStop} into the first one. + */ + private static void mergeStopAttributes(final TimetableStop first, final TimetableStop second) { + mergeEventAttributes(first.getAr(), second.getAr()); + mergeEventAttributes(first.getDp(), second.getDp()); + } + + /** + * Updates all values from the second Event into the first one. + */ + private static void mergeEventAttributes(@Nullable final Event first, @Nullable final Event second) { + if ((first == null) || (second == null)) { + return; + } + + for (final EventAttribute attribute : EventAttribute.ALL_ATTRIBUTES) { + updateAttribute(attribute, first, second); + } + } + + /** + * Sets the value of the given {@link EventAttribute} from the second Event in the first event, if not + * null. + */ + private static void updateAttribute(final EventAttribute attribute, final Event first, + final Event second) { + final @Nullable VALUE_TYPE value = attribute.getValue(second); + if (value != null) { + attribute.setValue(first, value); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1Api.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1Api.java new file mode 100644 index 0000000000000..fa5ec52ddda45 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1Api.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.io.IOException; +import java.util.Date; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable; + +/** + * Interface for timetables API in V1. + * + * @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public interface TimetablesV1Api { + + /** + * Requests the timetable with the planned data for the given station and time. + * Calls the "/plan" endpoint of the rest-service. + * + * REST-endpoint documentation: (from + * {@see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData}). + * Returns a Timetable object (see Timetable) that contains planned data for the specified station (evaNo) + * within the hourly time slice given by date (format YYMMDD) and hour (format HH). The data includes stops + * for all trips that arrive or depart within that slice. There is a small overlap between slices since some + * trips arrive in one slice and depart in another. + * + * Planned data does never contain messages. On event level, planned data contains the 'plannned' attributes pt, pp, + * ps and ppth + * while the 'changed' attributes ct, cp, cs and cpth are absent. + * + * Planned data is generated many hours in advance and is static, i.e. it does never change. + * It should be cached by web caches.public interface allows access to information about a station. + * + * @param evaNo The Station EVA-number. + * @param time The time for which the timetable is requested. It will be requested for the given day and hour. + * + * @return The {@link Timetable} containing the planned arrivals and departues. + */ + public abstract Timetable getPlan(String evaNo, Date time) throws IOException; + + /** + * Requests all known changes in the timetable for the given station. + * Calls the "/fchg" endpoint of the rest-service. + * + * REST-endpoint documentation: (from + * {@see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData}). + * Returns a Timetable object (see Timetable) that contains all known changes for the station given by evaNo. + * + * The data includes all known changes from now on until undefinitely into the future. Once changes become obsolete + * (because their trip departs from the station) they are removed from this resource. + * + * Changes may include messages. On event level, they usually contain one or more of the 'changed' attributes ct, + * cp, cs or cpth. + * Changes may also include 'planned' attributes if there is no associated planned data for the change (e.g. an + * unplanned stop or trip). + * + * Full changes are updated every 30s and should be cached for that period by web caches. + * + * @param evaNo The Station EVA-number. + * + * @return The {@link Timetable} containing all known changes for the given station. + */ + public abstract Timetable getFullChanges(String evaNo) throws IOException; + + /** + * Requests the timetable with the planned data for the given station and time. + * Calls the "/plan" endpoint of the rest-service. + * + * REST-endpoint documentation: (from + * {@see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData}). + * Returns a Timetable object (see Timetable) that contains all recent changes for the station given by evaNo. + * Recent changes are always a subset of the full changes. They may equal full changes but are typically much + * smaller. + * Data includes only those changes that became known within the last 2 minutes. + * + * A client that updates its state in intervals of less than 2 minutes should load full changes initially and then + * proceed to periodically load only the recent changes in order to save bandwidth. + * + * Recent changes are updated every 30s as well and should be cached for that period by web caches. + * + * @param evaNo The Station EVA-number. + * + * @return The {@link Timetable} containing recent changes (from last two minutes) for the given station. + */ + public abstract Timetable getRecentChanges(String evaNo) throws IOException; +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ApiFactory.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ApiFactory.java new file mode 100644 index 0000000000000..5eaa552029aba --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ApiFactory.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.net.URISyntaxException; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Impl.HttpCallable; +import org.xml.sax.SAXException; + +/** + * Factory for {@link TimetablesV1Api}. + * + * @author Sönke Küper - Initial contribution. + */ +@NonNullByDefault +public interface TimetablesV1ApiFactory { + + /** + * Creates an new instance of the {@link TimetablesV1Api}. + */ + public abstract TimetablesV1Api create(final String authToken, final HttpCallable httpCallable) + throws JAXBException, SAXException, URISyntaxException; +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1Impl.java b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1Impl.java new file mode 100644 index 0000000000000..e4eccc5370b67 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1Impl.java @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.net.URISyntaxException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.validation.Schema; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpHeader; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +/** + * Default Implementation of {@link TimetablesV1Api}. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public final class TimetablesV1Impl implements TimetablesV1Api { + + /** + * Interface for stubbing HTTP-Calls in jUnit tests. + */ + public interface HttpCallable { + + /** + * Executes the given url with the given httpMethod. + * Furthermore the http.proxyXXX System variables are read and + * set into the {@link org.eclipse.jetty.client.HttpClient}. + * + * @param httpMethod the HTTP method to use + * @param url the url to execute + * @param httpHeaders optional http request headers which has to be sent within request + * @param content the content to be sent to the given url or null if no content should + * be sent. + * @param contentType the content type of the given content + * @param timeout the socket timeout in milliseconds to wait for data + * @return the response body or NULL when the request went wrong + * @throws IOException when the request execution failed, timed out or it was interrupted + */ + public abstract String executeUrl(String httpMethod, String url, Properties httpHeaders, + @Nullable InputStream content, @Nullable String contentType, int timeout) throws IOException; + } + + private static final String PLAN_URL = "https://api.deutschebahn.com/timetables/v1/plan/%evaNo%/%date%/%hour%"; + private static final String FCHG_URL = "https://api.deutschebahn.com/timetables/v1/fchg/%evaNo%"; + private static final String RCHG_URL = "https://api.deutschebahn.com/timetables/v1/rchg/%evaNo%"; + + private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd"); + private static final SimpleDateFormat HOUR_FORMAT = new SimpleDateFormat("HH"); + + private final String authToken; + private final HttpCallable httpCallable; + + private final Logger logger = LoggerFactory.getLogger(TimetablesV1Impl.class); + private JAXBContext jaxbContext; + // private Schema schema; + + /** + * Creates an new {@link TimetablesV1Impl}. + * + * @param authToken The authentication token for timetable api on developer.deutschebahn.com. + */ + public TimetablesV1Impl(final String authToken, final HttpCallable httpCallable) + throws JAXBException, SAXException, URISyntaxException { + this.authToken = authToken; + this.httpCallable = httpCallable; + + // The results from webservice does not conform to the schema provided. The triplabel-Element (tl) is expected + // to occour as + // last Element within an timetableStop (s) element. But it is the first element when requesting the plan. + // When requesting the changes it is the last element, so the schema can't just be corrected. + // If written to developer support, but got no response yet, so schema validation is disabled at the moment. + + // final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + // final URL schemaURL = getClass().getResource("/xsd/Timetables_REST.xsd"); + // assert schemaURL != null; + // this.schema = schemaFactory.newSchema(schemaURL); + this.jaxbContext = JAXBContext.newInstance(Timetable.class.getPackageName(), Timetable.class.getClassLoader()); + } + + @Override + public Timetable getPlan(final String evaNo, final Date time) throws IOException { + return this.performHttpApiRequest(buildPlanRequestURL(evaNo, time)); + } + + @Override + public Timetable getFullChanges(final String evaNo) throws IOException { + return this.performHttpApiRequest(buildFchgRequestURL(evaNo)); + } + + @Override + public Timetable getRecentChanges(final String evaNo) throws IOException { + return this.performHttpApiRequest(buildRchgRequestURL(evaNo)); + } + + private Timetable performHttpApiRequest(final String url) throws IOException { + this.logger.debug("Performing http request to timetable api with url {}", url); + + String response; + try { + response = this.httpCallable.executeUrl( // + "GET", // + url, // + this.createHeaders(), // + null, // + null, // + REQUEST_TIMEOUT_MS); + return this.mapResponseToTimetable(response); + } catch (IOException e) { + logger.debug("Error getting data from webservice.", e); + throw e; + } + } + + /** + * Parses and creates the {@link Timetable} from the response or + * returns an empty {@link Timetable} if response was empty. + */ + private Timetable mapResponseToTimetable(final String response) throws IOException { + if (response.isEmpty()) { + return new Timetable(); + } + + try { + return unmarshal(response, Timetable.class); + } catch (JAXBException | SAXException e) { + this.logger.error("Error parsing response from timetable api.", e); + throw new IOException(e); + } + } + + /** + * Creates the HTTP-Headers required for http requests. + */ + private Properties createHeaders() { + final Properties headers = new Properties(); + headers.put(HttpHeader.ACCEPT.asString(), "application/xml"); + headers.put(HttpHeader.AUTHORIZATION.asString(), "Bearer " + this.authToken); + return headers; + } + + private T unmarshal(final String xmlContent, final Class clazz) throws JAXBException, SAXException { + return unmarshal( // + jaxbContext, // + null, // Provide no schema, due webservice results are not schema-valid. + xmlContent, // + clazz // + ); + } + + @SuppressWarnings("unchecked") + private static T unmarshal(final JAXBContext jaxbContext, @Nullable final Schema schema, + final String xmlContent, final Class clss) throws JAXBException { + final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + unmarshaller.setSchema(schema); + final JAXBElement resultObject = (JAXBElement) unmarshaller.unmarshal(new StringReader(xmlContent)); + return resultObject.getValue(); + } + + /** + * Build rest endpoint URL for request the planned timetable. + */ + private String buildPlanRequestURL(final String evaNr, final Date date) { + synchronized (this) { + final String dateParam = DATE_FORMAT.format(date); + final String hourParam = HOUR_FORMAT.format(date); + + return PLAN_URL // + .replace("%evaNo%", evaNr) // + .replace("%date%", dateParam) // + .replace("%hour%", hourParam); + } + } + + /** + * Build rest endpoint URL for request all known changes in the timetable. + */ + private static String buildFchgRequestURL(final String evaNr) { + return FCHG_URL.replace("%evaNo%", evaNr); + } + + /** + * Build rest endpoint URL for request all known changes in the timetable. + */ + private static String buildRchgRequestURL(final String evaNr) { + return RCHG_URL.replace("%evaNo%", evaNr); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..7deb3797ee87c --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Deutsche Bahn Binding + This binding provides timetable information for train stations of Deutsche Bahn. + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/i18n/deutschebahn_de.properties b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/i18n/deutschebahn_de.properties new file mode 100644 index 0000000000000..80181986adecd --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/i18n/deutschebahn_de.properties @@ -0,0 +1,85 @@ +# binding +binding.deutschebahn.name = DeutscheBahn +binding.deutschebahn.description = Anbindung an die OpenData Schnittstelle der DeutschenBahn f�r den Abruf von Fahrplaninformationen. + +# thing type timetable +thing-type.deutschebahn.timetable.label = DeutscheBahn Fahrplan +thing-type.deutschebahn.timetable.description = Verbindung zur Webserivce-API der DeutschenBahn f�r den Abruf des Fahrplans. Die bereitgestellten Daten k�nnen dann �ber ein Thing "Zug" dargestellt werden. + +# thing type timetable config description +thing-type.config.deutschebahn.timetable.accessToken.label = Zugriffsschl�ssel +thing-type.config.deutschebahn.timetable.accessToken.description = Zugriffsschl�ssel f�r die Timetable V1 API aus dem Developer-Portal der DeutschenBahn. +thing-type.config.deutschebahn.timetable.evaNo.label = eva Nr des Bahnhofs +thing-type.config.deutschebahn.timetable.evaNo.description = evaNr des Bahnhofs, f�r den der Fahrplan abgerufen wird. Siehe https://data.deutschebahn.com/dataset.tags.EVA-Nr..html. +thing-type.config.deutschebahn.timetable.trainFilter.label = Zugfilter +thing-type.config.deutschebahn.timetable.trainFilter.description = Selektiert die Z�ge (Ank�nfte / Abfahrten), die in dem Fahrplan enthalten sein sollen. Wenn nicht angegeben werden nur die Abfahrten angezeigt. + +# thing type train +thing-type.deutschebahn.train.label = Zug +thing-type.deutschebahn.train.description = Stellt einen Zug im Fahrplan dar, der an dem konfigurierten Bahnhof ankommt oder abf�hrt. +thing-type.deutschebahn.train.group.trip.label = Fahrtinformationen +thing-type.deutschebahn.train.group.trip.description = Enth�lt alle Informationen �ber die Fahrt des Zuges. +thing-type.deutschebahn.train.group.arrival.label = Ankunft +thing-type.deutschebahn.train.group.arrival.description = Enth�lt alle Informationen �ber die Ankunft des Zuges. +thing-type.deutschebahn.train.group.departure.label = Abfahrt +thing-type.deutschebahn.train.group.departure.description = Enth�lt alle Informationen �ber die Abfahrt des Zuges. + +# thing type train config description +thing-type.config.deutschebahn.train.position.label = Position +thing-type.config.deutschebahn.train.position.description = Gibt die Position des Zuges im Fahrplan an. z.B. wird mit 1 der erste Zug im Fahrplan selektiert, mit 2 der Zweite usw. + +# trip information channel types +channel-type.deutschebahn.category.label = Kateogrie +channel-type.deutschebahn.category.description = Die Kategorie des Zuges, z.B. "ICE" oder "RE". +channel-type.deutschebahn.number.label = Zugnummer +channel-type.deutschebahn.number.description = Die Zugnummer, z.B. "4523". +channel-type.deutschebahn.filter-flags.label = Filter +channel-type.deutschebahn.filter-flags.description = Filter f�r die Fahrt. +channel-type.deutschebahn.trip-type.label = Fahrttyp +channel-type.deutschebahn.trip-type.description = Gibt den Typ der Fahrt an. +channel-type.deutschebahn.owner.label = Eigent�mer +channel-type.deutschebahn.owner.description = Gibt die eindeutige Kurzbezeichnung des EisenbahnVerkehrsUnternehmen des Zuges an. + +# event channel types +channel-type.deutschebahn.planned-path.label = Geplante Route +channel-type.deutschebahn.planned-path.description = Gibt die geplante Route des Zuges an, dabei werden die Stationen mit | getrennt aufgelistet. F�r Ank�nfte besteht der Pfad aus den Halten, die vor der aktuellen Station kamen, das erste Element ist der Startbahnhof. F�r Abfahrten werden die Stationen aufgelistet, die nach der aktuellen Station kommen. Das letzte Element ist der Zielbahnhof. +channel-type.deutschebahn.changed-path.label = Ge�ndert Route +channel-type.deutschebahn.changed-path.description = Gibt die ge�nderte Route des Zuges an, dabei werden die Stationen mit | getrennt aufgelistet. Ist nicht gesetzt, falls keine �nderungen vorliegen. +channel-type.deutschebahn.planned-platform.label = Geplantes Gleis +channel-type.deutschebahn.planned-platform.description = Gibt das geplante Gleis an, auf dem der Zug ankommt/abf�hrt. +channel-type.deutschebahn.changed-platform.label = Ge�ndertes Gleis +channel-type.deutschebahn.changed-platform.description = Gibt das ge�ndert Gleis an, auf dem der Zug ankommt/abf�hrt. Ist nicht gesetzt, falls keine �nderungen vorliegen. +channel-type.deutschebahn.planned-time.label = Geplante Zeit +channel-type.deutschebahn.planned-time.description = Gibt die geplante Zeit f�r die Ankunft/Abfahrt des Zuges an. +channel-type.deutschebahn.changed-time.label = Ge�nderte Zeit +channel-type.deutschebahn.changed-time.description = Gibt die ge�nder Zeit f�r die Ankunft/Abfahrt des Zuges an. Ist nicht gesetzt, falls keine �nderungen vorliegen. +channel-type.deutschebahn.planned-status.label = Geplanter Status +channel-type.deutschebahn.planned-status.description = Gibt den Stauts des Fahrplaneintrags an. +channel-type.deutschebahn.changed-status.label = Ge�nderter Status +channel-type.deutschebahn.changed-status.description = Gibt den ge�nderten Status des Fahrplaneintrags an. Ist nicht gesetzt, falls keine �nderungen vorliegen. +channel-type.deutschebahn.cancellation-time.label = Stornierungs-Zeitpunkt +channel-type.deutschebahn.cancellation-time.description = Gibt den Zeitpunkt an, an dem der Halt storniert wurde. +channel-type.deutschebahn.line.label = Linie +channel-type.deutschebahn.line.description = Gibt die Linie des Zuges an. +channel-type.deutschebahn.messages.label = Meldungen +channel-type.deutschebahn.messages.description = Textmeldungen, die f�r diese Ankunft/Abfahrt des Zuges vorliegen. Mehrere Meldungen werden mit einem Strich getrennt ausgegeben. +channel-type.deutschebahn.hidden.label = Versteckt +channel-type.deutschebahn.hidden.description = Gibt an, ob die Ankunft/Abfahrt im Fahrplan nicht angezeigt werden soll, da ein Ein-/Aussteigen nicht m�glich ist. +channel-type.deutschebahn.wings.label = Wing +channel-type.deutschebahn.wings.description = Gibt eine Folge | separierten "Trip-IDs"an. +channel-type.deutschebahn.transition.label = �bergang +channel-type.deutschebahn.transition.description = Gibt bei Z�gen, die zusmmengef�hrt oder getrennt werden die Trip-ID des vorherigen oder nachfolgenden Zuges an. +channel-type.deutschebahn.planned-distant-endpoint.label = Geplanter entfernter Endpunkt +channel-type.deutschebahn.planned-distant-endpoint.description = Gibt den geplanten entfernten Endpunkt des Zuges an. +channel-type.deutschebahn.changed-distant-endpoint.label = Ge�nderter entfernter Endpunkt +channel-type.deutschebahn.changed-distant-endpoint.description = Gibt den ge�nderten entfernten Endpunkt des Zuges an. Ist nicht gesetzt, falls keine �nderungen vorliegen. +channel-type.deutschebahn.distant-change.label = Ge�nderter Zielbahnhof +channel-type.deutschebahn.distant-change.description = Gibt den ge�nderten Zielbahnhof des Zuges an. +channel-type.deutschebahn.planned-final-station.label = Geplanter Start-/Zielbahnhof +channel-type.deutschebahn.planned-final-station.description = Gibt den geplanten Startbahnhof (f�r Ank�nfte) bzw. Zielbahnhof (f�r Abfahrten) an. +channel-type.deutschebahn.planned-intermediate-stations.label = Geplante Halte +channel-type.deutschebahn.planned-intermediate-stations.description = Gibt die geplanten Halte des Zuges auf dem Weg zum aktuellen Bahnhof an (f�r Ank�nfte) bzw. die folgenden Halte (f�r Abfahrten). +channel-type.deutschebahn.changed-final-station.label = Ge�nderter Start-/Zielbahnhof +channel-type.deutschebahn.changed-final-station.description = Gibt den ge�nderten Startbahnhof (f�r Ank�nfte) bzw. Zielbahnhof (f�r Abfahrten) an. Ist nicht gesetzt, falls keine �nderungen vorliegen. +channel-type.deutschebahn.changed-intermediate-stations.label = Ge�nderte Halte +channel-type.deutschebahn.changed-intermediate-stations.description = Gibt die ge�nderten Halte des Zuges auf dem Weg zum aktuellen Bahnhof an (f�r Ank�nfte) bzw. die folgenden Halte (f�r Abfahrten). Ist nicht gesetzt, falls keine �nderungen vorliegen. diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..d85a7c028ebac --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,342 @@ + + + + + + + Connection to the timetable API of Deutsche Bahn. Provides timetable data that can be displayed using the + train things. + + + + + Access Token from Deutsche Bahn developer portal for accessing the webservice api. + + + + evaNo of the station, for which the timetable should be requested. see + https://data.deutschebahn.com/dataset.tags.EVA-Nr..html + + + true + departures + + Selects the trains that will be be displayed in this timetable. If not set only departures will be + provided. + + + + + + + + + + + + + + + Displays informations about an train within the given timetable at one station. + + + + Contains all informations about the trip of the train. + + + + + Contains all informations about the arrival of the train at the station. + Channels may be empty, if the + trains starts at this station. + + + + + Contains all informations about the departure of the train at the station. + Channels may be empty, if the + trains ends at this station. + + + + + + + Selects the position of the train in the timetable. + + + + + + + Contains all informations about the trip of the train. + + + + + + + + + + + + Contains all attributes for an event (arrival / departure) of an train at the station. + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Provides the category of the trip, e.g. "ICE" or "RE". + + + + + String + + Provides the trip/train number, e.g. "4523". + + + + + String + + Provides the filter flags. + + + + + String + + Provides the type of the trip. + + + + + + + + String + + Provides the owner of the train. A unique short-form and only intended to map a trip to specific evu + (EisenbahnVerkehrsUnternehmen). + + + + + + String + + Provides the planned platform of a train. + + + + + String + + Provides the changed platform of a train. + + + + + DateTime + + Provides the planned time of a train. + + + + + DateTime + + Provides the changed time of a train. + + + + + String + + Provides the planned status of a train. + + + + + + + + + + + String + + Provides the changed status of a train. + + + + + + + + + + + String + + The line indicator. + + + + + String + + Messages for this train. Contains all translated codes from the messages of the selected train stop. + Multiple messages will be separated with an single dash. + + + + + + DateTime + + Time when the cancellation of this stop was created. + + + + + String + + Provides the planned path of a train. + For arrival, the path indicates the stations that come before the + current station. The first element then is the trip’s + start station. For departure, the path indicates the stations + that come after the current station. The last ele-ment + in the path then is the trip’s destination station. Note that + the current station is never included in the path + (neither for arrival nor for departure). + + + + + String + + Provides the planned path of a train. + For arrival, the path indicates the stations that come before the + current station. The first element then is the trip’s + start station. For departure, the path indicates the stations + that come after the current station. The last ele-ment + in the path then is the trip’s destination station. Note that + the current station is never included in the path + (neither for arrival nor for departure). + + + + + Switch + + On if the event should not be shown, because travellers are not supposed to enter or exit the train + at + this stop. + + + + + String + + A sequence of trip id separated by the pipe symbols (“|”). + + + + + String + + Trip id of the next or previous train of a shared train. At the start stop this references the previous + trip, at the last stop it references the next trip. + + + + + String + + Planned distant endpoint. + + + + + String + + Changed distant endpoint. + + + + + Number + + distant change + + + + + + String + + Planned final station of the train. For arrivals the starting station is returned, for departures the + target station is returned. + + + + String + + Returns the planned stations this train came from (for arrivals) or the stations this train will go to + (for departures). Stations will be separated by single dash. + + + + + String + + Changed final station of the train. For arrivals the starting station is returned, for departures the + target station is returned. + + + + + String + + Returns the changed stations this train came from (for arrivals) or the stations this train will go to + (for departures). Stations will be separated by single dash. + + + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/main/resources/xsd/Timetables_REST.xsd b/bundles/org.openhab.binding.deutschebahn/src/main/resources/xsd/Timetables_REST.xsd new file mode 100644 index 0000000000000..c0091341a79be --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/main/resources/xsd/Timetables_REST.xsd @@ -0,0 +1,441 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandlerTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandlerTest.java new file mode 100644 index 0000000000000..5209ad9d283e9 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTimetableHandlerTest.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.deutschebahn.internal.timetable.TimeproviderStub; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ApiFactory; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ApiStub; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Impl.HttpCallable; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ImplTestHelper; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Tests for {@link DeutscheBahnTimetableHandler}. + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +public class DeutscheBahnTimetableHandlerTest implements TimetablesV1ImplTestHelper { + + private static Configuration createConfig() { + final Configuration config = new Configuration(); + config.put("accessToken", "letMeIn"); + config.put("evaNo", "8000226"); + config.put("trainFilter", "all"); + return config; + } + + private static Bridge mockBridge() { + final Bridge bridge = mock(Bridge.class); + when(bridge.getUID()).thenReturn(new ThingUID(DeutscheBahnBindingConstants.TIMETABLE_TYPE, "timetable")); + when(bridge.getConfiguration()).thenReturn(createConfig()); + + final List things = new ArrayList<>(); + things.add(DeutscheBahnTrainHandlerTest.mockThing(1)); + things.add(DeutscheBahnTrainHandlerTest.mockThing(2)); + things.add(DeutscheBahnTrainHandlerTest.mockThing(3)); + when(things.get(0).getHandler()).thenReturn(mock(DeutscheBahnTrainHandler.class)); + when(things.get(1).getHandler()).thenReturn(mock(DeutscheBahnTrainHandler.class)); + when(things.get(2).getHandler()).thenReturn(mock(DeutscheBahnTrainHandler.class)); + + when(bridge.getThings()).thenReturn(things); + + return bridge; + } + + private DeutscheBahnTimetableHandler createAndInitHandler(final ThingHandlerCallback callback, final Bridge bridge) + throws Exception { + return createAndInitHandler(callback, bridge, createApiWithTestdata().getApiFactory()); + } + + private DeutscheBahnTimetableHandler createAndInitHandler( // + final ThingHandlerCallback callback, // + final Bridge bridge, // + final TimetablesV1ApiFactory apiFactory) throws Exception { // + final TimeproviderStub timeProvider = new TimeproviderStub(); + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 30); + + final DeutscheBahnTimetableHandler handler = new DeutscheBahnTimetableHandler(bridge, apiFactory, timeProvider); + handler.setCallback(callback); + handler.initialize(); + return handler; + } + + @Test + public void testUpdateChannels() throws Exception { + final Bridge bridge = mockBridge(); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + + final DeutscheBahnTimetableHandler handler = createAndInitHandler(callback, bridge); + + try { + verify(callback).statusUpdated(eq(bridge), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(bridge), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + verifyThingUpdated(bridge, 0, "-5296516961807204721-2108160906-5"); + verifyThingUpdated(bridge, 1, "-8364795265993682073-2108160911-6"); + verifyThingUpdated(bridge, 2, "-2949440726131702047-2108160858-10"); + } finally { + handler.dispose(); + } + } + + private void verifyThingUpdated(final Bridge bridge, int offset, String stopId) { + final Thing train = bridge.getThings().get(offset); + final DeutscheBahnTrainHandler childHandler = (DeutscheBahnTrainHandler) train.getHandler(); + verify(childHandler, timeout(1000)) + .updateChannels(argThat((TimetableStop stop) -> stop.getId().equals(stopId))); + } + + @Test + public void testUpdateTrainsToUndefinedIfNoDataWasProvided() throws Exception { + final Bridge bridge = mockBridge(); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + + final TimetablesV1ApiStub stubWithError = TimetablesV1ApiStub.createWithException(); + + final DeutscheBahnTimetableHandler handler = createAndInitHandler(callback, bridge, + (String authToken, HttpCallable httpCallable) -> stubWithError); + + try { + verify(callback).statusUpdated(eq(bridge), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(bridge), + argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE))); + + verifyChannelsUpdatedToUndef(bridge, 0, callback, UnDefType.UNDEF); + verifyChannelsUpdatedToUndef(bridge, 1, callback, UnDefType.UNDEF); + verifyChannelsUpdatedToUndef(bridge, 2, callback, UnDefType.UNDEF); + + } finally { + handler.dispose(); + } + } + + private static void verifyChannelsUpdatedToUndef(Bridge bridge, int offset, ThingHandlerCallback callback, + State expectedState) { + final Thing thing = bridge.getThings().get(offset); + for (Channel channel : thing.getChannels()) { + verify(callback).stateUpdated(eq(channel.getUID()), eq(expectedState)); + } + } + + @Test + public void testUpdateTrainsToUndefinedIfNotEnoughDataWasProvided() throws Exception { + final Bridge bridge = mockBridge(); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + + // Bridge contains 3 trains, but Timetable contains only 1 items, so two trains has to be updated to undef + // value. + final Timetable timetable = new Timetable(); + TimetableStop stop01 = new TimetableStop(); + stop01.setId("stop01id"); + Event dp = new Event(); + dp.setPt("2108161000"); + stop01.setDp(dp); + timetable.getS().add(stop01); + + final TimetablesV1ApiStub stubWithData = TimetablesV1ApiStub.createWithResult(timetable); + + final DeutscheBahnTimetableHandler handler = createAndInitHandler(callback, bridge, + (String authToken, HttpCallable httpCallable) -> stubWithData); + + try { + verify(callback).statusUpdated(eq(bridge), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(bridge), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + verifyThingUpdated(bridge, 0, stop01.getId()); + verifyChannelsUpdatedToUndef(bridge, 1, callback, UnDefType.UNDEF); + verifyChannelsUpdatedToUndef(bridge, 2, callback, UnDefType.UNDEF); + + } finally { + handler.dispose(); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainHandlerTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainHandlerTest.java new file mode 100644 index 0000000000000..627e53d3f5f3d --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/DeutscheBahnTrainHandlerTest.java @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.internal.BridgeImpl; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Tests for {@link DeutscheBahnTrainHandler}. + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +public class DeutscheBahnTrainHandlerTest { + + private static final String SAMPLE_PATH = "Bielefeld Hbf|Herford|Löhne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|Bückeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf|Lehrte"; + + private static Configuration createConfig(int position) { + final Configuration config = new Configuration(); + config.put("position", String.valueOf(position)); + return config; + } + + static Thing mockThing(int id) { + final Thing thing = mock(Thing.class); + when(thing.getUID()).thenReturn(new ThingUID(DeutscheBahnBindingConstants.TRAIN_TYPE, "train-" + id)); + when(thing.getThingTypeUID()).thenReturn(DeutscheBahnBindingConstants.TRAIN_TYPE); + when(thing.getConfiguration()).thenReturn(createConfig(id)); + ThingUID bridgeId = new ThingUID(DeutscheBahnBindingConstants.TIMETABLE_TYPE, "timetable"); + when(thing.getBridgeUID()).thenReturn(bridgeId); + + final Channel tripLabelCategory = mockChannel(thing.getUID(), "trip#category"); + + final Channel arrivalPlannedTime = mockChannel(thing.getUID(), "arrival#planned-time"); + final Channel arrivalLine = mockChannel(thing.getUID(), "arrival#line"); + final Channel arrivalChangedTime = mockChannel(thing.getUID(), "arrival#changed-time"); + + final Channel departurePlannedTime = mockChannel(thing.getUID(), "departure#planned-time"); + final Channel departurePlannedPlatform = mockChannel(thing.getUID(), "departure#planned-platform"); + final Channel departureTargetStation = mockChannel(thing.getUID(), "departure#planned-final-station"); + + when(thing.getChannelsOfGroup("trip")).thenReturn(Arrays.asList(tripLabelCategory)); + when(thing.getChannelsOfGroup("arrival")) + .thenReturn(Arrays.asList(arrivalPlannedTime, arrivalLine, arrivalChangedTime)); + when(thing.getChannelsOfGroup("departure")) + .thenReturn(Arrays.asList(departurePlannedTime, departurePlannedPlatform, departureTargetStation)); + when(thing.getChannels()).thenReturn(Arrays.asList( // + tripLabelCategory, // + arrivalPlannedTime, arrivalLine, arrivalChangedTime, // + departurePlannedTime, departurePlannedPlatform, departureTargetStation)); + + return thing; + } + + private static Channel mockChannel(final ThingUID thingId, final String channelId) { + final Channel channel = Mockito.mock(Channel.class); + when(channel.getUID()).thenReturn(new ChannelUID(thingId, channelId)); + return channel; + } + + private static DeutscheBahnTrainHandler createAndInitHandler(final ThingHandlerCallback callback, + final Thing thing) { + final DeutscheBahnTrainHandler handler = new DeutscheBahnTrainHandler(thing); + handler.setCallback(callback); + handler.initialize(); + return handler; + } + + private static State getDateTime(final Date day) { + final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(day.toInstant(), ZoneId.systemDefault()); + return new DateTimeType(zonedDateTime); + } + + @Test + public void testUpdateChannels() { + final Thing thing = mockThing(1); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + ThingUID bridgeId = new ThingUID(DeutscheBahnBindingConstants.TIMETABLE_TYPE, "timetable"); + when(callback.getBridge(bridgeId)) + .thenReturn(new BridgeImpl(DeutscheBahnBindingConstants.TIMETABLE_TYPE, bridgeId)); + final DeutscheBahnTrainHandler handler = createAndInitHandler(callback, thing); + + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + // Provide data that will update the channels + TimetableStop stop = new TimetableStop(); + + TripLabel label = new TripLabel(); + label.setC("WFB"); + stop.setTl(label); + + Event arrival = new Event(); + arrival.setPt("2108161434"); + arrival.setL("RE60"); + stop.setAr(arrival); + Event departure = new Event(); + departure.setPt("2108161435"); + departure.setPp("2"); + departure.setPpth(SAMPLE_PATH); + stop.setDp(departure); + + handler.updateChannels(stop); + + final Date arrivalTime = new GregorianCalendar(2021, 7, 16, 14, 34).getTime(); + final Date departureTime = new GregorianCalendar(2021, 7, 16, 14, 35).getTime(); + + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "trip#category"), + new StringType("WFB")); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "arrival#planned-time"), + getDateTime(arrivalTime)); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "arrival#line"), + new StringType("RE60")); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "arrival#changed-time"), + UnDefType.NULL); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "departure#planned-time"), + getDateTime(departureTime)); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "departure#planned-platform"), + new StringType("2")); + verify(callback, timeout(1000)).stateUpdated( + new ChannelUID(thing.getUID(), "departure#planned-final-station"), new StringType("Lehrte")); + } finally { + handler.dispose(); + } + } + + @Test + public void testUpdateChannelsWithEventNotPresent() { + final Thing thing = mockThing(1); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + ThingUID bridgeId = new ThingUID(DeutscheBahnBindingConstants.TIMETABLE_TYPE, "timetable"); + when(callback.getBridge(bridgeId)) + .thenReturn(new BridgeImpl(DeutscheBahnBindingConstants.TIMETABLE_TYPE, bridgeId)); + final DeutscheBahnTrainHandler handler = createAndInitHandler(callback, thing); + + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + // Provide data that will update the channels + TimetableStop stop = new TimetableStop(); + + Event arrival = new Event(); + arrival.setPt("2108161434"); + arrival.setL("RE60"); + stop.setAr(arrival); + + handler.updateChannels(stop); + + final Date arrivalTime = new GregorianCalendar(2021, 7, 16, 14, 34).getTime(); + + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "trip#category"), + UnDefType.UNDEF); + + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "arrival#planned-time"), + getDateTime(arrivalTime)); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "arrival#line"), + new StringType("RE60")); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "arrival#changed-time"), + UnDefType.NULL); + + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "departure#planned-time"), + UnDefType.UNDEF); + verify(callback, timeout(1000)).stateUpdated(new ChannelUID(thing.getUID(), "departure#planned-platform"), + UnDefType.UNDEF); + verify(callback, timeout(1000)) + .stateUpdated(new ChannelUID(thing.getUID(), "departure#planned-final-station"), UnDefType.UNDEF); + } finally { + handler.dispose(); + } + } + + @Test + public void testWithoutBridgeStateUpdatesToOffline() { + final Thing thing = mockThing(1); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + final DeutscheBahnTrainHandler handler = createAndInitHandler(callback, thing); + + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE))); + } finally { + handler.dispose(); + } + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java new file mode 100644 index 0000000000000..1f11a0891b56c --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/EventAttributeTest.java @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Event; +import org.openhab.binding.deutschebahn.internal.timetable.dto.EventStatus; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Message; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; + +/** + * Tests Mapping from {@link Event} attribute values to openhab state values. + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +@SuppressWarnings("unchecked") +public class EventAttributeTest { + + private static final String SAMPLE_PATH = "Bielefeld Hbf|Herford|Löhne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|Bückeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf|Lehrte"; + + private void doTestEventAttribute( // + String channelName, // + @Nullable String expectedChannelName, // + Consumer setValue, // + VALUE_TYPE expectedValue, // + @Nullable STATE_TYPE expectedState, // + EventType eventType, // + boolean performSetterTest) { // + final EventAttribute attribute = (EventAttribute) EventAttribute + .getByChannelName(channelName, eventType); + assertThat(attribute, is(not(nullValue()))); + assertThat(attribute.getChannelTypeName(), is(expectedChannelName == null ? channelName : expectedChannelName)); + assertThat(attribute.getValue(new Event()), is(nullValue())); + assertThat(attribute.getState(new Event()), is(nullValue())); + + // Create an event and set the attribute value. + final Event eventWithValueSet = new Event(); + setValue.accept(eventWithValueSet); + + // then try get value and state. + assertThat(attribute.getValue(eventWithValueSet), is(expectedValue)); + assertThat(attribute.getState(eventWithValueSet), is(expectedState)); + + // Try set Value in new Event + final Event copyTarget = new Event(); + attribute.setValue(copyTarget, expectedValue); + if (performSetterTest) { + assertThat(attribute.getValue(copyTarget), is(expectedValue)); + } + } + + @Test + public void testGetNonExistingChannel() { + assertThat(EventAttribute.getByChannelName("unkownChannel", EventType.ARRIVAL), is(nullValue())); + } + + @Test + public void testPlannedPath() { + doTestEventAttribute("planned-path", null, (Event e) -> e.setPpth(SAMPLE_PATH), SAMPLE_PATH, + new StringType(SAMPLE_PATH), EventType.DEPARTURE, true); + } + + @Test + public void testChangedPath() { + doTestEventAttribute("changed-path", null, (Event e) -> e.setCpth(SAMPLE_PATH), SAMPLE_PATH, + new StringType(SAMPLE_PATH), EventType.DEPARTURE, true); + } + + @Test + public void testPlannedPlatform() { + String platform = "2"; + doTestEventAttribute("planned-platform", null, (Event e) -> e.setPp(platform), platform, + new StringType(platform), EventType.DEPARTURE, true); + } + + @Test + public void testChangedPlatform() { + String platform = "2"; + doTestEventAttribute("changed-platform", null, (Event e) -> e.setCp(platform), platform, + new StringType(platform), EventType.DEPARTURE, true); + } + + @Test + public void testWings() { + String wings = "-906407760000782942-1403311431"; + doTestEventAttribute("wings", null, (Event e) -> e.setWings(wings), wings, new StringType(wings), + EventType.DEPARTURE, true); + } + + @Test + public void testTransition() { + String transition = "2016448009055686515-1403311438-1"; + doTestEventAttribute("transition", null, (Event e) -> e.setTra(transition), transition, + new StringType(transition), EventType.DEPARTURE, true); + } + + @Test + public void testPlannedDistantEndpoint() { + String endpoint = "Hannover Hbf"; + doTestEventAttribute("planned-distant-endpoint", null, (Event e) -> e.setPde(endpoint), endpoint, + new StringType(endpoint), EventType.DEPARTURE, true); + } + + @Test + public void testChangedDistantEndpoint() { + String endpoint = "Hannover Hbf"; + doTestEventAttribute("changed-distant-endpoint", null, (Event e) -> e.setCde(endpoint), endpoint, + new StringType(endpoint), EventType.DEPARTURE, true); + } + + @Test + public void testLine() { + String line = "RE60"; + doTestEventAttribute("line", null, (Event e) -> e.setL(line), line, new StringType(line), EventType.DEPARTURE, + true); + } + + @Test + public void testPlannedTime() { + String time = "2109111825"; + GregorianCalendar expectedValue = new GregorianCalendar(2021, 8, 11, 18, 25, 0); + DateTimeType expectedState = new DateTimeType( + ZonedDateTime.ofInstant(expectedValue.toInstant(), ZoneId.systemDefault())); + doTestEventAttribute("planned-time", null, (Event e) -> e.setPt(time), expectedValue.getTime(), expectedState, + EventType.DEPARTURE, true); + } + + @Test + public void testChangedTime() { + String time = "2109111825"; + GregorianCalendar expectedValue = new GregorianCalendar(2021, 8, 11, 18, 25, 0); + DateTimeType expectedState = new DateTimeType( + ZonedDateTime.ofInstant(expectedValue.toInstant(), ZoneId.systemDefault())); + doTestEventAttribute("changed-time", null, (Event e) -> e.setCt(time), expectedValue.getTime(), expectedState, + EventType.DEPARTURE, true); + } + + @Test + public void testCancellationTime() { + String time = "2109111825"; + GregorianCalendar expectedValue = new GregorianCalendar(2021, 8, 11, 18, 25, 0); + DateTimeType expectedState = new DateTimeType( + ZonedDateTime.ofInstant(expectedValue.toInstant(), ZoneId.systemDefault())); + doTestEventAttribute("cancellation-time", null, (Event e) -> e.setClt(time), expectedValue.getTime(), + expectedState, EventType.DEPARTURE, true); + } + + @Test + public void testPlannedStatus() { + EventStatus expectedValue = EventStatus.A; + doTestEventAttribute("planned-status", null, (Event e) -> e.setPs(expectedValue), expectedValue, + new StringType(expectedValue.name().toLowerCase()), EventType.DEPARTURE, true); + } + + @Test + public void testChangedStatus() { + EventStatus expectedValue = EventStatus.C; + doTestEventAttribute("changed-status", null, (Event e) -> e.setCs(expectedValue), expectedValue, + new StringType(expectedValue.name().toLowerCase()), EventType.DEPARTURE, true); + } + + @Test + public void testHidden() { + doTestEventAttribute("hidden", null, (Event e) -> e.setHi(0), 0, OnOffType.OFF, EventType.DEPARTURE, true); + doTestEventAttribute("hidden", null, (Event e) -> e.setHi(1), 1, OnOffType.ON, EventType.DEPARTURE, true); + } + + @Test + public void testDistantChange() { + doTestEventAttribute("distant-change", null, (Event e) -> e.setDc(42), 42, new DecimalType(42), + EventType.DEPARTURE, true); + } + + @Test + public void testPlannedFinalStation() { + doTestEventAttribute("planned-final-station", "planned-target-station", (Event e) -> e.setPpth(SAMPLE_PATH), + "Lehrte", new StringType("Lehrte"), EventType.DEPARTURE, false); + doTestEventAttribute("planned-final-station", "planned-start-station", (Event e) -> e.setPpth(SAMPLE_PATH), + "Bielefeld Hbf", new StringType("Bielefeld Hbf"), EventType.ARRIVAL, false); + } + + @Test + public void testChangedFinalStation() { + doTestEventAttribute("changed-final-station", "changed-target-station", (Event e) -> e.setCpth(SAMPLE_PATH), + "Lehrte", new StringType("Lehrte"), EventType.DEPARTURE, false); + doTestEventAttribute("changed-final-station", "changed-start-station", (Event e) -> e.setCpth(SAMPLE_PATH), + "Bielefeld Hbf", new StringType("Bielefeld Hbf"), EventType.ARRIVAL, false); + } + + @Test + public void testPlannedIntermediateStations() { + String expectedFollowing = "Bielefeld Hbf - Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf"; + doTestEventAttribute("planned-intermediate-stations", "planned-following-stations", + (Event e) -> e.setPpth(SAMPLE_PATH), expectedFollowing, new StringType(expectedFollowing), + EventType.DEPARTURE, false); + String expectedPrevious = "Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf - Lehrte"; + doTestEventAttribute("planned-intermediate-stations", "planned-previous-stations", + (Event e) -> e.setPpth(SAMPLE_PATH), expectedPrevious, new StringType(expectedPrevious), + EventType.ARRIVAL, false); + } + + @Test + public void testChangedIntermediateStations() { + String expectedFollowing = "Bielefeld Hbf - Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf"; + doTestEventAttribute("changed-intermediate-stations", "changed-following-stations", + (Event e) -> e.setCpth(SAMPLE_PATH), expectedFollowing, new StringType(expectedFollowing), + EventType.DEPARTURE, false); + String expectedPrevious = "Herford - Löhne(Westf) - Bad Oeynhausen - Porta Westfalica - Minden(Westf) - Bückeburg - Stadthagen - Haste - Wunstorf - Hannover Hbf - Lehrte"; + doTestEventAttribute("changed-intermediate-stations", "changed-previous-stations", + (Event e) -> e.setCpth(SAMPLE_PATH), expectedPrevious, new StringType(expectedPrevious), + EventType.ARRIVAL, false); + } + + @Test + public void testMessages() { + String expectedOneMessage = "Verzögerungen im Betriebsablauf"; + List messages = new ArrayList<>(); + Message m1 = new Message(); + m1.setC(99); + messages.add(m1); + doTestEventAttribute("messages", null, (Event e) -> e.getM().addAll(messages), messages, + new StringType(expectedOneMessage), EventType.DEPARTURE, true); + + String expectedTwoMessages = "Verzögerungen im Betriebsablauf - keine Qualitätsmängel"; + Message m2 = new Message(); + m2.setC(88); + messages.add(m2); + doTestEventAttribute("messages", null, (Event e) -> e.getM().addAll(messages), messages, + new StringType(expectedTwoMessages), EventType.DEPARTURE, true); + } + + @Test + public void testFilterDuplicateMessages() { + String expectedOneMessage = "andere Reihenfolge der Wagen - technische Störung am Zug - Zug verkehrt richtig gereiht"; + List messages = new ArrayList<>(); + Message m1 = new Message(); + m1.setC(80); + messages.add(m1); + Message m2 = new Message(); + m2.setC(80); + messages.add(m2); + Message m3 = new Message(); + m3.setC(36); + messages.add(m3); + Message m4 = new Message(); + m4.setC(80); + messages.add(m4); + Message m5 = new Message(); + m5.setC(84); + messages.add(m5); + + doTestEventAttribute("messages", null, (Event e) -> e.getM().addAll(messages), messages, + new StringType(expectedOneMessage), EventType.DEPARTURE, true); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/TripLabelAttributeTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/TripLabelAttributeTest.java new file mode 100644 index 0000000000000..191378a57b3bf --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/TripLabelAttributeTest.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TripType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; + +/** + * Tests Mapping from {@link TripLabel} attribute values to openhab state values. + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +@SuppressWarnings("unchecked") +public class TripLabelAttributeTest { + + private void doTestTripAttribute( // + String channelName, // + @Nullable String expectedChannelName, // + Consumer setValue, // + VALUE_TYPE expectedValue, // + @Nullable STATE_TYPE expectedState, // + boolean performSetterTest) { // + final TripLabelAttribute attribute = (TripLabelAttribute) TripLabelAttribute + .getByChannelName(channelName); + assertThat(attribute, is(not(nullValue()))); + assertThat(attribute.getChannelTypeName(), is(expectedChannelName == null ? channelName : expectedChannelName)); + assertThat(attribute.getValue(new TripLabel()), is(nullValue())); + assertThat(attribute.getState(new TripLabel()), is(nullValue())); + + // Create an trip label and set the attribute value. + final TripLabel labelWithValueSet = new TripLabel(); + setValue.accept(labelWithValueSet); + + // then try get value and state. + assertThat(attribute.getValue(labelWithValueSet), is(expectedValue)); + assertThat(attribute.getState(labelWithValueSet), is(expectedState)); + + // Try set Value in new Event + final TripLabel copyTarget = new TripLabel(); + attribute.setValue(copyTarget, expectedValue); + if (performSetterTest) { + assertThat(attribute.getValue(copyTarget), is(expectedValue)); + } + } + + @Test + public void testGetNonExistingChannel() { + assertThat(TripLabelAttribute.getByChannelName("unkownChannel"), is(nullValue())); + } + + @Test + public void testCategory() { + final String category = "ICE"; + doTestTripAttribute("category", null, (TripLabel e) -> e.setC(category), category, new StringType(category), + true); + } + + @Test + public void testNumber() { + final String number = "4567"; + doTestTripAttribute("number", null, (TripLabel e) -> e.setN(number), number, new StringType(number), true); + } + + @Test + public void testOwner() { + final String owner = "W3"; + doTestTripAttribute("owner", null, (TripLabel e) -> e.setO(owner), owner, new StringType(owner), true); + } + + @Test + public void testFilterFlages() { + final String filter = "a"; + doTestTripAttribute("filter-flags", null, (TripLabel e) -> e.setF(filter), filter, new StringType(filter), + true); + } + + @Test + public void testTripType() { + final TripType type = TripType.E; + doTestTripAttribute("trip-type", null, (TripLabel e) -> e.setT(type), type, new StringType("e"), true); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimeproviderStub.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimeproviderStub.java new file mode 100644 index 0000000000000..0bf7072e48d56 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimeproviderStub.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Stub time provider. + * + * @author Sönke Küper - Initial contribution. + */ +@NonNullByDefault +public final class TimeproviderStub implements Supplier { + + public GregorianCalendar time = new GregorianCalendar(); + + @Override + public Date get() { + return this.time.getTime(); + } + + public void moveAhead(int seconds) { + this.time.set(Calendar.SECOND, time.get(Calendar.SECOND) + seconds); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoaderTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoaderTest.java new file mode 100644 index 0000000000000..6a25c8cf51b2b --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableLoaderTest.java @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.deutschebahn.internal.EventType; +import org.openhab.binding.deutschebahn.internal.TimetableStopFilter; +import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop; + +/** + * Tests for the {@link TimetableLoader}. + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public class TimetableLoaderTest implements TimetablesV1ImplTestHelper { + + @Test + public void testLoadRequiredStopCount() throws Exception { + final TimetablesApiTestModule timeTableTestModule = this.createApiWithTestdata(); + final TimeproviderStub timeProvider = new TimeproviderStub(); + final TimetableLoader loader = new TimetableLoader(timeTableTestModule.getApi(), TimetableStopFilter.ALL, + EventType.DEPARTURE, timeProvider, EVA_LEHRTE, 20); + + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 30); + + final List stops = loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09", + "https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/10", + "https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/11")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + + assertThat(stops, hasSize(21)); + assertEquals("-5296516961807204721-2108160906-5", stops.get(0).getId()); + assertEquals("-3222259045572671319-2108161155-1", stops.get(20).getId()); + + // when requesting again no further call to plan is made, because required stops are available. + final List stops02 = loader.getTimetableStops(); + assertThat(stops02, hasSize(21)); + assertThat(timeTableTestModule.getRequestedPlanUrls(), hasSize(3)); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), hasSize(1)); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + } + + @Test + public void testLoadNewDataIfRequired() throws Exception { + final TimetablesApiTestModule timeTableTestModule = this.createApiWithTestdata(); + final TimeproviderStub timeProvider = new TimeproviderStub(); + final TimetableLoader loader = new TimetableLoader(timeTableTestModule.getApi(), TimetableStopFilter.ALL, + EventType.DEPARTURE, timeProvider, EVA_LEHRTE, 8); + + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 0); + + final List stops = loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + + assertThat(stops, hasSize(8)); + assertEquals("1763676552526687479-2108160847-6", stops.get(0).getId()); + assertEquals("8681599812964340829-2108160955-1", stops.get(7).getId()); + + // Move clock ahead for 30 minutes, so that some of the fetched data is in past and new plan data must be + // requested + timeProvider.moveAhead(30 * 60); + + final List stops02 = loader.getTimetableStops(); + assertThat(stops02, hasSize(13)); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09", + "https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/10")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226", + "https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + + assertEquals("-5296516961807204721-2108160906-5", stops02.get(0).getId()); + assertEquals("-3376513334056532423-2108161055-1", stops02.get(12).getId()); + } + + @Test + public void testRequestUpdates() throws Exception { + final TimetablesApiTestModule timeTableTestModule = this.createApiWithTestdata(); + final TimeproviderStub timeProvider = new TimeproviderStub(); + final TimetableLoader loader = new TimetableLoader(timeTableTestModule.getApi(), TimetableStopFilter.ALL, + EventType.DEPARTURE, timeProvider, EVA_LEHRTE, 1); + + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 30); + + // First call - plan and full changes are requested. + loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + + // Changes are updated only every 30 seconds, so move clock ahead 20 seconds, no request is made + timeProvider.moveAhead(20); + loader.getTimetableStops(); + + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + + // Move ahead 10 seconds, so recent changes are fetched + timeProvider.moveAhead(10); + loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/rchg/8000226")); + + // Move again ahead 30 seconds, recent changes are fetched again + timeProvider.moveAhead(30); + loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/rchg/8000226", + "https://api.deutschebahn.com/timetables/v1/rchg/8000226")); + + // If recent change were not updated last 120 seconds the full changes must be requested + timeProvider.moveAhead(120); + loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226", + "https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/rchg/8000226", + "https://api.deutschebahn.com/timetables/v1/rchg/8000226")); + } + + @Test + public void testReturnOnlyArrivals() throws Exception { + final TimetablesApiTestModule timeTableTestModule = this.createApiWithTestdata(); + final TimeproviderStub timeProvider = new TimeproviderStub(); + final TimetableLoader loader = new TimetableLoader(timeTableTestModule.getApi(), TimetableStopFilter.ARRIVALS, + EventType.ARRIVAL, timeProvider, EVA_LEHRTE, 20); + + // Simulate that only one url is available + timeTableTestModule.addAvailableUrl("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09"); + + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 0); + + final List stops = loader.getTimetableStops(); + + // File contains 8 stops, but 2 are only departures + assertThat(stops, hasSize(6)); + assertEquals("1763676552526687479-2108160847-6", stops.get(0).getId()); + assertEquals("-735649762452915464-2108160912-6", stops.get(5).getId()); + } + + @Test + public void testReturnOnlyDepartures() throws Exception { + final TimetablesApiTestModule timeTableTestModule = this.createApiWithTestdata(); + final TimeproviderStub timeProvider = new TimeproviderStub(); + final TimetableLoader loader = new TimetableLoader(timeTableTestModule.getApi(), TimetableStopFilter.DEPARTURES, + EventType.DEPARTURE, timeProvider, EVA_LEHRTE, 20); + + // Simulate that only one url is available + timeTableTestModule.addAvailableUrl("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09"); + + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 0); + + final List stops = loader.getTimetableStops(); + + // File contains 8 stops, but 2 are only arrivals + assertThat(stops, hasSize(6)); + assertEquals("-94442819435724762-2108160916-1", stops.get(0).getId()); + assertEquals("8681599812964340829-2108160955-1", stops.get(5).getId()); + } + + @Test + public void testRemoveEntryOnlyIfChangedTimeIsInPast() throws Exception { + final TimetablesApiTestModule timeTableTestModule = this.createApiWithTestdata(); + final TimeproviderStub timeProvider = new TimeproviderStub(); + final TimetableLoader loader = new TimetableLoader(timeTableTestModule.getApi(), TimetableStopFilter.DEPARTURES, + EventType.DEPARTURE, timeProvider, EVA_LEHRTE, 1); + + timeProvider.time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 35); + + final List stops = loader.getTimetableStops(); + assertThat(timeTableTestModule.getRequestedPlanUrls(), + contains("https://api.deutschebahn.com/timetables/v1/plan/8000226/210816/09")); + assertThat(timeTableTestModule.getRequestedFullChangesUrls(), + contains("https://api.deutschebahn.com/timetables/v1/fchg/8000226")); + assertThat(timeTableTestModule.getRequestedRecentChangesUrls(), empty()); + + // Stop -5296516961807204721-2108160906-5 has its planned time at 9:34, but its included because its changed + // time is 9:42 + assertThat(stops, hasSize(4)); + assertEquals("-5296516961807204721-2108160906-5", stops.get(0).getId()); + assertEquals("2108160942", stops.get(0).getDp().getCt()); + assertEquals("8681599812964340829-2108160955-1", stops.get(3).getId()); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStubHttpCallable.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStubHttpCallable.java new file mode 100644 index 0000000000000..44aab0cfbe9f0 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetableStubHttpCallable.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Impl.HttpCallable; + +/** + * Stub Implementation for {@link HttpCallable}, that provides Data for the selected station, date and hour from file + * system. + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +public class TimetableStubHttpCallable implements HttpCallable { + + private static final Pattern PLAN_URL_PATTERN = Pattern + .compile("https://api.deutschebahn.com/timetables/v1/plan/(\\d+)/(\\d+)/(\\d+)"); + private static final Pattern FULL_CHANGES_URL_PATTERN = Pattern + .compile("https://api.deutschebahn.com/timetables/v1/fchg/(\\d+)"); + private static final Pattern RECENT_CHANGES_URL_PATTERN = Pattern + .compile("https://api.deutschebahn.com/timetables/v1/rchg/(\\d+)"); + + private final File testdataDir; + private final List requestedPlanUrls; + private final List requestedFullChangesUrls; + private final List requestedRecentChangesUrls; + + // Allows simulation of available data. + // if not set all available files will be served. + private @Nullable Set availableUrls = null; + + public TimetableStubHttpCallable(File testdataDir) { + this.testdataDir = testdataDir; + this.requestedPlanUrls = new ArrayList<>(); + this.requestedFullChangesUrls = new ArrayList(); + this.requestedRecentChangesUrls = new ArrayList(); + } + + public void addAvailableUrl(String url) { + if (this.availableUrls == null) { + availableUrls = new HashSet<>(); + } + this.availableUrls.add(url); + } + + @Override + public String executeUrl( // + String httpMethod, // + String url, // + Properties httpHeaders, // + @Nullable InputStream content, // + @Nullable String contentType, // + int timeout) throws IOException { + final Matcher planMatcher = PLAN_URL_PATTERN.matcher(url); + if (planMatcher.matches()) { + requestedPlanUrls.add(url); + return processRequest(url, planMatcher, this::getPlanData); + } + + final Matcher fullChangesMatcher = FULL_CHANGES_URL_PATTERN.matcher(url); + if (fullChangesMatcher.matches()) { + requestedFullChangesUrls.add(url); + return processRequest(url, fullChangesMatcher, this::getFullChanges); + } + + final Matcher recentChangesMatcher = RECENT_CHANGES_URL_PATTERN.matcher(url); + if (recentChangesMatcher.matches()) { + requestedRecentChangesUrls.add(url); + return processRequest(url, recentChangesMatcher, this::getRecentChanges); + } + return ""; + } + + private String processRequest(String url, Matcher matcher, Function responseSupplier) { + if (availableUrls != null && !availableUrls.contains(url)) { + return ""; + } else { + return responseSupplier.apply(matcher); + } + } + + private String getPlanData(final Matcher planMatcher) { + final String evaNo = planMatcher.group(1); + final String day = planMatcher.group(2); + final String hour = planMatcher.group(3); + + final File responseFile = new File(this.testdataDir, "plan/" + evaNo + "/" + day + "/" + hour + ".xml"); + return serveFileContentIfExists(responseFile); + } + + private String serveFileContentIfExists(File responseFile) { + if (!responseFile.exists()) { + return ""; + } + + try { + return Files.readString(responseFile.toPath()); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private String getRecentChanges(Matcher recentChangesMatcher) { + final String evaNo = recentChangesMatcher.group(1); + File responseFile = new File(this.testdataDir, "rchg/" + evaNo + ".xml"); + return serveFileContentIfExists(responseFile); + } + + private String getFullChanges(Matcher fullChangesMatcher) { + final String evaNo = fullChangesMatcher.group(1); + File responseFile = new File(this.testdataDir, "fchg/" + evaNo + ".xml"); + return serveFileContentIfExists(responseFile); + } + + public List getRequestedPlanUrls() { + return requestedPlanUrls; + } + + public List getRequestedFullChangesUrls() { + return requestedFullChangesUrls; + } + + public List getRequestedRecentChangesUrls() { + return requestedRecentChangesUrls; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesApiTestModule.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesApiTestModule.java new file mode 100644 index 0000000000000..5c90d602d502b --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesApiTestModule.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.net.URISyntaxException; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Impl.HttpCallable; +import org.xml.sax.SAXException; + +/** + * Testmodule that contains the {@link TimetablesV1Api} and {@link TimetableStubHttpCallable}. + * Used in tests to check which http calls have been made. + * + * @author Sönke Küper - Initial contribution. + */ +@NonNullByDefault +public final class TimetablesApiTestModule { + + private final TimetablesV1Api api; + private final TimetableStubHttpCallable httpStub; + + public TimetablesApiTestModule(TimetablesV1Api api, TimetableStubHttpCallable httpStub) { + this.api = api; + this.httpStub = httpStub; + } + + public TimetablesV1Api getApi() { + return api; + } + + public void addAvailableUrl(String url) { + this.httpStub.addAvailableUrl(url); + } + + public List getRequestedPlanUrls() { + return httpStub.getRequestedPlanUrls(); + } + + public List getRequestedFullChangesUrls() { + return httpStub.getRequestedFullChangesUrls(); + } + + public List getRequestedRecentChangesUrls() { + return httpStub.getRequestedRecentChangesUrls(); + } + + public TimetablesV1ApiFactory getApiFactory() { + return new TimetablesV1ApiFactory() { + + @Override + public TimetablesV1Api create(String authToken, HttpCallable httpCallable) + throws JAXBException, SAXException, URISyntaxException { + return api; + } + }; + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ApiStub.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ApiStub.java new file mode 100644 index 0000000000000..3b14cdfdc5824 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ApiStub.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import java.io.IOException; +import java.util.Date; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable; + +/** + * Stub Implementation of {@link TimetablesV1Api}, that may return an preconfigured Timetable or + * throws an {@link IOException} if not data has been set. + * + * @author Sönke Küper - initial contribution + */ +@NonNullByDefault +public final class TimetablesV1ApiStub implements TimetablesV1Api { + + @Nullable + private final Timetable result; + + private TimetablesV1ApiStub(@Nullable Timetable result) { + this.result = result; + } + + /** + * Creates an new {@link TimetablesV1ApiStub}, that returns the given result. + */ + public static TimetablesV1ApiStub createWithResult(Timetable timetable) { + return new TimetablesV1ApiStub(timetable); + } + + /** + * Creates an new {@link TimetablesV1ApiStub} that throws an Exception. + */ + public static TimetablesV1ApiStub createWithException() { + return new TimetablesV1ApiStub(null); + } + + @Override + public Timetable getPlan(String evaNo, Date time) throws IOException { + final Timetable currentResult = this.result; + if (currentResult == null) { + throw new IOException("No timetable data is available"); + } else { + return currentResult; + } + } + + @Override + public Timetable getFullChanges(String evaNo) throws IOException { + return new Timetable(); + } + + @Override + public Timetable getRecentChanges(String evaNo) throws IOException { + return new Timetable(); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ImplTest.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ImplTest.java new file mode 100644 index 0000000000000..9e5ae39276040 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ImplTest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable; + +/** + * Tests for {@link TimetablesV1Impl} + * + * @author Sönke Küper - Initial contribution. + */ +@NonNullByDefault +public class TimetablesV1ImplTest implements TimetablesV1ImplTestHelper { + + @Test + public void testGetDataForLehrte() throws Exception { + TimetablesV1Api timeTableApi = createApiWithTestdata().getApi(); + + Date time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 22).getTime(); + + Timetable timeTable = timeTableApi.getPlan(EVA_LEHRTE, time); + assertNotNull(timeTable); + assertEquals(8, timeTable.getS().size()); + } + + @Test + public void testGetNonExistingData() throws Exception { + TimetablesV1Api timeTableApi = createApiWithTestdata().getApi(); + + Date time = new GregorianCalendar(2021, Calendar.AUGUST, 16, 9, 22).getTime(); + + Timetable timeTable = timeTableApi.getPlan("ABCDEF", time); + assertNotNull(timeTable); + assertEquals(0, timeTable.getS().size()); + } + + @Test + public void testGetDataForHannoverHBF() throws Exception { + TimetablesV1Api timeTableApi = createApiWithTestdata().getApi(); + + Date time = new GregorianCalendar(2021, Calendar.OCTOBER, 14, 11, 00).getTime(); + + Timetable timeTable = timeTableApi.getPlan(EVA_HANNOVER_HBF, time); + assertNotNull(timeTable); + assertEquals(50, timeTable.getS().size()); + + Timetable changes = timeTableApi.getFullChanges(EVA_HANNOVER_HBF); + assertNotNull(changes); + assertEquals(730, changes.getS().size()); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ImplTestHelper.java b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ImplTestHelper.java new file mode 100644 index 0000000000000..2a92393279619 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/java/org/openhab/binding/deutschebahn/internal/timetable/TimetablesV1ImplTestHelper.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deutschebahn.internal.timetable; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.net.URL; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Helper interface for jUnit Tests to provide an {@link TimetablesApiTestModule}. + * + * @author Sönke Küper - initial contribution. + */ +@NonNullByDefault +public interface TimetablesV1ImplTestHelper { + + public static final String EVA_LEHRTE = "8000226"; + public static final String EVA_HANNOVER_HBF = "8000152"; + public static final String AUTH_TOKEN = "354c8161cd7fb0936c840240280c131e"; + + /** + * Creates an {@link TimetablesApiTestModule} that uses http response data from file system. + */ + public default TimetablesApiTestModule createApiWithTestdata() throws Exception { + final URL timetablesData = getClass().getResource("/timetablesData"); + assertNotNull(timetablesData); + final File testDataDir = new File(timetablesData.toURI()); + final TimetableStubHttpCallable httpStub = new TimetableStubHttpCallable(testDataDir); + final TimetablesV1Impl timeTableApi = new TimetablesV1Impl(AUTH_TOKEN, httpStub); + return new TimetablesApiTestModule(timeTableApi, httpStub); + } +} diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/fchg/8000152.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/fchg/8000152.xml new file mode 100644 index 0000000000000..b95ec6d5eb10c --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/fchg/8000152.xml @@ -0,0 +1,5401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/fchg/8000226.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/fchg/8000226.xml new file mode 100644 index 0000000000000..b4783dbe27c03 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/fchg/8000226.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000152/211014/11.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000152/211014/11.xml new file mode 100644 index 0000000000000..52bb29f7e56dd --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000152/211014/11.xml @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/07.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/07.xml new file mode 100644 index 0000000000000..4f00d86594ccf --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/07.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/08.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/08.xml new file mode 100644 index 0000000000000..c2413f0139472 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/08.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/09.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/09.xml new file mode 100644 index 0000000000000..0613399d0f9f6 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/09.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/10.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/10.xml new file mode 100644 index 0000000000000..e2d7fe24c89a8 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/10.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/11.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/11.xml new file mode 100644 index 0000000000000..494e57b898e8b --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/11.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/12.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/12.xml new file mode 100644 index 0000000000000..f1b3ee40f6bd7 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/12.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/13.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/13.xml new file mode 100644 index 0000000000000..e4212807bf6d3 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/13.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/14.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/14.xml new file mode 100644 index 0000000000000..682e6e2413f1c --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/14.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/15.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/15.xml new file mode 100644 index 0000000000000..f65a2880434b8 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/15.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/16.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/16.xml new file mode 100644 index 0000000000000..68e29297f1407 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/16.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/17.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/17.xml new file mode 100644 index 0000000000000..6fb571efa3533 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/17.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/18.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/18.xml new file mode 100644 index 0000000000000..0f143706b5c1d --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/18.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/19.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/19.xml new file mode 100644 index 0000000000000..ce466a72562bc --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/19.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/20.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/20.xml new file mode 100644 index 0000000000000..1ba8e2629e637 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/20.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/21.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/21.xml new file mode 100644 index 0000000000000..5714e711df55f --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/21.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/22.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/22.xml new file mode 100644 index 0000000000000..63fc2808f2ae0 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/22.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/23.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/23.xml new file mode 100644 index 0000000000000..d30ab2d780e48 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210816/23.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/00.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/00.xml new file mode 100644 index 0000000000000..6acd438713d8e --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/00.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/01.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/01.xml new file mode 100644 index 0000000000000..5a5adda84e890 --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/01.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/02.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/02.xml new file mode 100644 index 0000000000000..5f341233944ce --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/02.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/03.xml b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/03.xml new file mode 100644 index 0000000000000..5f341233944ce --- /dev/null +++ b/bundles/org.openhab.binding.deutschebahn/src/test/resources/timetablesData/plan/8000226/210817/03.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index ec2376b590d0c..aafd24a7171a1 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -96,6 +96,7 @@ org.openhab.binding.dbquery org.openhab.binding.deconz org.openhab.binding.denonmarantz + org.openhab.binding.deutschebahn org.openhab.binding.digiplex org.openhab.binding.digitalstrom org.openhab.binding.dlinksmarthome