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 d672adddd5321..9a6dd839a540b 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 fr 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 fr den Abruf des Fahrplans. Die bereitgestellten Daten knnen dann ber ein Thing "Zug" dargestellt werden.
+
+# thing type timetable config description
+thing-type.config.deutschebahn.timetable.accessToken.label = Zugriffsschlssel
+thing-type.config.deutschebahn.timetable.accessToken.description = Zugriffsschlssel fr 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, fr 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 Zge (Anknfte / 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 abfhrt.
+thing-type.deutschebahn.train.group.trip.label = Fahrtinformationen
+thing-type.deutschebahn.train.group.trip.description = Enthlt alle Informationen ber die Fahrt des Zuges.
+thing-type.deutschebahn.train.group.arrival.label = Ankunft
+thing-type.deutschebahn.train.group.arrival.description = Enthlt alle Informationen ber die Ankunft des Zuges.
+thing-type.deutschebahn.train.group.departure.label = Abfahrt
+thing-type.deutschebahn.train.group.departure.description = Enthlt 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 fr 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 = Eigentmer
+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. Fr Anknfte besteht der Pfad aus den Halten, die vor der aktuellen Station kamen, das erste Element ist der Startbahnhof. Fr Abfahrten werden die Stationen aufgelistet, die nach der aktuellen Station kommen. Das letzte Element ist der Zielbahnhof.
+channel-type.deutschebahn.changed-path.label = Gendert Route
+channel-type.deutschebahn.changed-path.description = Gibt die genderte 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/abfhrt.
+channel-type.deutschebahn.changed-platform.label = Gendertes Gleis
+channel-type.deutschebahn.changed-platform.description = Gibt das gendert Gleis an, auf dem der Zug ankommt/abfhrt. 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 fr die Ankunft/Abfahrt des Zuges an.
+channel-type.deutschebahn.changed-time.label = Genderte Zeit
+channel-type.deutschebahn.changed-time.description = Gibt die gender Zeit fr 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 = Genderter Status
+channel-type.deutschebahn.changed-status.description = Gibt den genderten 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 fr 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 mglich 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 Zgen, die zusmmengefhrt 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 = Genderter entfernter Endpunkt
+channel-type.deutschebahn.changed-distant-endpoint.description = Gibt den genderten entfernten Endpunkt des Zuges an. Ist nicht gesetzt, falls keine nderungen vorliegen.
+channel-type.deutschebahn.distant-change.label = Genderter Zielbahnhof
+channel-type.deutschebahn.distant-change.description = Gibt den genderten 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 (fr Anknfte) bzw. Zielbahnhof (fr 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 (fr Anknfte) bzw. die folgenden Halte (fr Abfahrten).
+channel-type.deutschebahn.changed-final-station.label = Genderter Start-/Zielbahnhof
+channel-type.deutschebahn.changed-final-station.description = Gibt den genderten Startbahnhof (fr Anknfte) bzw. Zielbahnhof (fr Abfahrten) an. Ist nicht gesetzt, falls keine nderungen vorliegen.
+channel-type.deutschebahn.changed-intermediate-stations.label = Genderte Halte
+channel-type.deutschebahn.changed-intermediate-stations.description = Gibt die genderten Halte des Zuges auf dem Weg zum aktuellen Bahnhof an (fr Anknfte) bzw. die folgenden Halte (fr 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 695088f236523..6ab04faa208be 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