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=DBOpenDatatab1
+ *
+ * @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=DBOpenDatatab1
+ *
+ * @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=DBOpenDatatab1
+ *
+ * @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