[deutschebahn] Initial contribution: New binding for DeutscheBahn Fahrplan (#11384)

* Created binding for DeutscheBahn Timetable API.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Disabled schema validation and used original schema. Added tests for hannover hbf which has non schema conforming responses.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Added information about UNDEF and NULL channel values.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Added sample widget and screenshot

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Filtering duplicate messages

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Fixed some typos.

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Updated to jUnit5

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Applied review remarks in Readme

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* Applied some review remarks

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

* 0000: Fixed compile warnings

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>

Co-authored-by: Sönke Küper <soenkekueper@gmx.de>
This commit is contained in:
Sönke Küper 2021-11-28 18:34:30 +01:00 committed by GitHub
parent 95e1479c5d
commit 50d5622e79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 11615 additions and 0 deletions

View File

@ -64,6 +64,7 @@
/bundles/org.openhab.binding.dbquery/ @lujop /bundles/org.openhab.binding.dbquery/ @lujop
/bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.denonmarantz/ @jwveldhuis /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis
/bundles/org.openhab.binding.deutschebahn/ @soenkekueper
/bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digiplex/ @rmichalak
/bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele /bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele
/bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor /bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor

View File

@ -311,6 +311,11 @@
<artifactId>org.openhab.binding.denonmarantz</artifactId> <artifactId>org.openhab.binding.denonmarantz</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.deutschebahn</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.digiplex</artifactId> <artifactId>org.openhab.binding.digiplex</artifactId>

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.deutschebahn</artifactId>
<name>openHAB Add-ons :: Bundles :: Deutsche Bahn Binding</name>
<build>
<plugins>
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.14.0</version>
<executions>
<execution>
<id>generate-jaxb-sources</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<generatePackage>org.openhab.binding.deutschebahn.internal.timetable.dto</generatePackage>
<schemaDirectory>src/main/resources/xsd</schemaDirectory>
<noFileHeader>true</noFileHeader>
<locale>en</locale>
<episode>false</episode>
<extension>true</extension>
<args>
<arg>-Xxew</arg>
<arg>-Xxew:instantiate early</arg>
</args>
<plugins>
<plugin>
<groupId>com.github.jaxb-xew-plugin</groupId>
<artifactId>jaxb-xew-plugin</artifactId>
<version>1.10</version>
</plugin>
</plugins>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.deutschebahn-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-deutschebahn" description="DeutscheBahn Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.deutschebahn/${project.version}</bundle>
</feature>
</features>

View File

@ -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 <DTO_TYPE> type of value in Bean.
* @param <VALUE_TYPE> type of value in Bean.
* @param <STATE_TYPE> type of state.
*/
@NonNullByDefault
public abstract class AbstractDtoAttributeSelector<DTO_TYPE extends JaxbEntity, @Nullable VALUE_TYPE, STATE_TYPE extends State> {
private final Function<DTO_TYPE, @Nullable VALUE_TYPE> getter;
private final BiConsumer<DTO_TYPE, VALUE_TYPE> setter;
private final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState;
private final String channelTypeName;
private final Class<STATE_TYPE> 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<DTO_TYPE, @Nullable VALUE_TYPE> getter, //
final BiConsumer<DTO_TYPE, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
final Class<STATE_TYPE> 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<STATE_TYPE> 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 <code>null</code> if the value is <code>null</code>.
*/
@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);
}
}

View File

@ -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);
}

View File

@ -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");
}

View File

@ -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<ThingTypeUID> 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;
}
}

View File

@ -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());
}
}

View File

@ -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<Integer, List<Thing>> 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<Thing> 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<Thing> 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<Date> currentTimeProvider;
/**
* Creates an new {@link DeutscheBahnTimetableHandler}.
*/
public DeutscheBahnTimetableHandler( //
final Bridge bridge, //
final TimetablesV1ApiFactory timetablesV1ApiFactory, //
final Supplier<Date> currentTimeProvider) {
super(bridge);
this.timetablesV1ApiFactory = timetablesV1ApiFactory;
this.currentTimeProvider = currentTimeProvider;
}
private List<TimetableStop> loadTimetable() {
final TimetableLoader currentLoader = this.loader;
if (currentLoader == null) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
return Collections.emptyList();
}
try {
final List<TimetableStop> 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<TimetableStop> 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<Thing> 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<TimetableStop> timetableStops) {
int position = 1;
for (final TimetableStop stop : timetableStops) {
final List<Thing> 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<Thing> 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);
}
}

View File

@ -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;
}

View File

@ -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<ChannelWithConfig> 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);
}
}
}

View File

@ -0,0 +1,427 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deutschebahn.internal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
import org.openhab.binding.deutschebahn.internal.timetable.dto.EventStatus;
import org.openhab.binding.deutschebahn.internal.timetable.dto.Message;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
/**
* Selector for the Attribute of an {@link Event}.
*
* chapter "1.2.11 Event" in Technical Interface Description for external Developers
*
* @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&#tab1
*
* @author Sönke Küper - initial contribution
*
* @param <VALUE_TYPE> type of value in Bean.
* @param <STATE_TYPE> type of state.
*/
@NonNullByDefault
public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
extends AbstractDtoAttributeSelector<Event, @Nullable VALUE_TYPE, STATE_TYPE> {
/**
* Planned Path.
*/
public static final EventAttribute<String, StringType> PPTH = new EventAttribute<>("planned-path", Event::getPpth,
Event::setPpth, StringType::new, StringType.class);
/**
* Changed Path.
*/
public static final EventAttribute<String, StringType> CPTH = new EventAttribute<>("changed-path", Event::getCpth,
Event::setCpth, StringType::new, StringType.class);
/**
* Planned platform.
*/
public static final EventAttribute<String, StringType> PP = new EventAttribute<>("planned-platform", Event::getPp,
Event::setPp, StringType::new, StringType.class);
/**
* Changed platform.
*/
public static final EventAttribute<String, StringType> CP = new EventAttribute<>("changed-platform", Event::getCp,
Event::setCp, StringType::new, StringType.class);
/**
* Planned time.
*/
public static final EventAttribute<Date, DateTimeType> PT = new EventAttribute<>("planned-time",
getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType, DateTimeType.class);
/**
* Changed time.
*/
public static final EventAttribute<Date, DateTimeType> CT = new EventAttribute<>("changed-time",
getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType, DateTimeType.class);
/**
* Planned status.
*/
public static final EventAttribute<EventStatus, StringType> PS = new EventAttribute<>("planned-status",
Event::getPs, Event::setPs, EventAttribute::fromEventStatus, StringType.class);
/**
* Changed status.
*/
public static final EventAttribute<EventStatus, StringType> CS = new EventAttribute<>("changed-status",
Event::getCs, Event::setCs, EventAttribute::fromEventStatus, StringType.class);
/**
* Hidden.
*/
public static final EventAttribute<Integer, OnOffType> HI = new EventAttribute<>("hidden", Event::getHi,
Event::setHi, EventAttribute::parseHidden, OnOffType.class);
/**
* Cancellation time.
*/
public static final EventAttribute<Date, DateTimeType> CLT = new EventAttribute<>("cancellation-time",
getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType, DateTimeType.class);
/**
* Wing.
*/
public static final EventAttribute<String, StringType> WINGS = new EventAttribute<>("wings", Event::getWings,
Event::setWings, StringType::new, StringType.class);
/**
* Transition.
*/
public static final EventAttribute<String, StringType> TRA = new EventAttribute<>("transition", Event::getTra,
Event::setTra, StringType::new, StringType.class);
/**
* Planned distant endpoint.
*/
public static final EventAttribute<String, StringType> PDE = new EventAttribute<>("planned-distant-endpoint",
Event::getPde, Event::setPde, StringType::new, StringType.class);
/**
* Changed distant endpoint.
*/
public static final EventAttribute<String, StringType> CDE = new EventAttribute<>("changed-distant-endpoint",
Event::getCde, Event::setCde, StringType::new, StringType.class);
/**
* Distant change.
*/
public static final EventAttribute<Integer, DecimalType> DC = new EventAttribute<>("distant-change", Event::getDc,
Event::setDc, DecimalType::new, DecimalType.class);
/**
* Line.
*/
public static final EventAttribute<String, StringType> L = new EventAttribute<>("line", Event::getL, Event::setL,
StringType::new, StringType.class);
/**
* Messages.
*/
public static final EventAttribute<List<Message>, StringType> MESSAGES = new EventAttribute<>("messages",
EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages, StringType.class);
/**
* Planned Start station.
*/
public static final EventAttribute<String, StringType> 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<String, StringType> 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<String, StringType> 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<String, StringType> 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<String, StringType> 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<String, StringType> 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<String, StringType> 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<String, StringType> 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<EventAttribute<?, ?>> 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<Event, @Nullable VALUE_TYPE> getter, //
final BiConsumer<Event, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
final Class<STATE_TYPE> 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<Event, @Nullable Date> getDate(final Function<Event, @Nullable String> getValue) {
return (final Event event) -> {
return parseDate(getValue.apply(event));
};
}
private static BiConsumer<Event, Date> setDate(final BiConsumer<Event, String> 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<Message> 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<Message> 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<Event, @Nullable List<Message>> getMessages() {
return new Function<Event, @Nullable List<Message>>() {
@Override
public @Nullable List<Message> 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 <code>true</code> the first value will be returned, <code>false</code> will return the last
* value.
*/
private static Function<Event, @Nullable String> getSingleStationFromPath(
final Function<Event, @Nullable String> 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 <code>true</code> the first value will be removed, <code>false</code> will remove the last
* value.
*/
private static Function<Event, @Nullable String> getIntermediateStationsFromPath(
final Function<Event, @Nullable String> 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<String> 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 <VALUE_TYPE> BiConsumer<Event, VALUE_TYPE> voidSetter() {
return new BiConsumer<Event, VALUE_TYPE>() {
@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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<TimetableStop, @Nullable Event> getter;
private final Function<TimetableStop, @Nullable Event> oppositeGetter;
private EventType(Function<TimetableStop, @Nullable Event> getter,
Function<TimetableStop, @Nullable Event> 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);
}
}

View File

@ -0,0 +1,134 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deutschebahn.internal;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Class containing the mappings for all message status codes.
*
* chapter "2 List of all codes" in Technical Interface Description for external Developers
*
* @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&#tab1
*
* @author Sönke Küper - initial contribution
*/
@NonNullByDefault
public final class MessageCodes {
private static Map<Integer, String> 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;
}
}
}

View File

@ -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<TimetableStop> {
/**
* 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;
}
};
}

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deutschebahn.internal;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TripLabel;
import org.openhab.binding.deutschebahn.internal.timetable.dto.TripType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Selection that returns the value of an {@link TripLabel}.
*
* chapter "1.2.7 TripLabel" in Technical Interface Description for external Developers
*
* @see https://developer.deutschebahn.com/store/apis/info?name=Timetables&version=v1&provider=DBOpenData&#tab1
*
* @author Sönke Küper - Initial contribution.
*
* @param <VALUE_TYPE> type of value in Bean.
* @param <STATE_TYPE> type of state.
*/
@NonNullByDefault
public final class TripLabelAttribute<VALUE_TYPE, STATE_TYPE extends State> extends
AbstractDtoAttributeSelector<TripLabel, @Nullable VALUE_TYPE, STATE_TYPE> implements AttributeSelection {
/**
* Trip category.
*/
public static final TripLabelAttribute<String, StringType> C = new TripLabelAttribute<>("category", TripLabel::getC,
TripLabel::setC, StringType::new, StringType.class);
/**
* Number.
*/
public static final TripLabelAttribute<String, StringType> N = new TripLabelAttribute<>("number", TripLabel::getN,
TripLabel::setN, StringType::new, StringType.class);
/**
* Filter flags.
*/
public static final TripLabelAttribute<String, StringType> F = new TripLabelAttribute<>("filter-flags",
TripLabel::getF, TripLabel::setF, StringType::new, StringType.class);
/**
* Trip Type.
*/
public static final TripLabelAttribute<TripType, StringType> T = new TripLabelAttribute<>("trip-type",
TripLabel::getT, TripLabel::setT, TripLabelAttribute::fromTripType, StringType.class);
/**
* Owner.
*/
public static final TripLabelAttribute<String, StringType> 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<TripLabel, @Nullable VALUE_TYPE> getter, //
final BiConsumer<TripLabel, VALUE_TYPE> setter, //
final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
final Class<STATE_TYPE> 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;
}
}
}

View File

@ -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<String, TimetableStop> cachedStopsPerId;
private final Map<String, TimetableStop> cachedChanges;
private final TimetablesV1Api api;
private final TimetableStopFilter stopFilter;
private final TimetableStopComparator comparator;
private final Supplier<Date> 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<Date> 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<TimetableStop> getTimetableStops() throws IOException {
this.updateCache();
final List<TimetableStop> 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<Entry<String, TimetableStop>> it = this.cachedStopsPerId.entrySet().iterator();
while (it.hasNext()) {
final Entry<String, TimetableStop> 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 <code>true</code> 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 <code>null</code> or before
* the given compareTime.
* If the {@link Event} is <code>null</code> it will return <code>true</code>.
*/
private static boolean isBefore( //
final EventAttribute<Date, DateTimeType> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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());
}
}
}

View File

@ -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<TimetableStop> {
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;
}
}

View File

@ -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
* <code>null</code>.
*/
private static <VALUE_TYPE> void updateAttribute(final EventAttribute<VALUE_TYPE, ?> attribute, final Event first,
final Event second) {
final @Nullable VALUE_TYPE value = attribute.getValue(second);
if (value != null) {
attribute.setValue(first, value);
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 <code>url</code> with the given <code>httpMethod</code>.
* Furthermore the <code>http.proxyXXX</code> 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 <code>url</code> or <code>null</code> if no content should
* be sent.
* @param contentType the content type of the given <code>content</code>
* @param timeout the socket timeout in milliseconds to wait for data
* @return the response body or <code>NULL</code> 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> T unmarshal(final String xmlContent, final Class<T> 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> T unmarshal(final JAXBContext jaxbContext, @Nullable final Schema schema,
final String xmlContent, final Class<T> clss) throws JAXBException {
final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setSchema(schema);
final JAXBElement<T> resultObject = (JAXBElement<T>) 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);
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="deutschebahn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Deutsche Bahn Binding</name>
<description>This binding provides timetable information for train stations of Deutsche Bahn.</description>
</binding:binding>

View File

@ -0,0 +1,85 @@
# binding
binding.deutschebahn.name = DeutscheBahn
binding.deutschebahn.description = Anbindung an die OpenData Schnittstelle der DeutschenBahn für den Abruf von Fahrplaninformationen.
# thing type timetable
thing-type.deutschebahn.timetable.label = DeutscheBahn Fahrplan
thing-type.deutschebahn.timetable.description = Verbindung zur Webserivce-API der DeutschenBahn für den Abruf des Fahrplans. Die bereitgestellten Daten können dann über ein Thing "Zug" dargestellt werden.
# thing type timetable config description
thing-type.config.deutschebahn.timetable.accessToken.label = Zugriffsschlüssel
thing-type.config.deutschebahn.timetable.accessToken.description = Zugriffsschlüssel für die Timetable V1 API aus dem Developer-Portal der DeutschenBahn.
thing-type.config.deutschebahn.timetable.evaNo.label = eva Nr des Bahnhofs
thing-type.config.deutschebahn.timetable.evaNo.description = evaNr des Bahnhofs, für den der Fahrplan abgerufen wird. Siehe https://data.deutschebahn.com/dataset.tags.EVA-Nr..html.
thing-type.config.deutschebahn.timetable.trainFilter.label = Zugfilter
thing-type.config.deutschebahn.timetable.trainFilter.description = Selektiert die Züge (Ankünfte / Abfahrten), die in dem Fahrplan enthalten sein sollen. Wenn nicht angegeben werden nur die Abfahrten angezeigt.
# thing type train
thing-type.deutschebahn.train.label = Zug
thing-type.deutschebahn.train.description = Stellt einen Zug im Fahrplan dar, der an dem konfigurierten Bahnhof ankommt oder abfährt.
thing-type.deutschebahn.train.group.trip.label = Fahrtinformationen
thing-type.deutschebahn.train.group.trip.description = Enthält alle Informationen über die Fahrt des Zuges.
thing-type.deutschebahn.train.group.arrival.label = Ankunft
thing-type.deutschebahn.train.group.arrival.description = Enthält alle Informationen über die Ankunft des Zuges.
thing-type.deutschebahn.train.group.departure.label = Abfahrt
thing-type.deutschebahn.train.group.departure.description = Enthält alle Informationen über die Abfahrt des Zuges.
# thing type train config description
thing-type.config.deutschebahn.train.position.label = Position
thing-type.config.deutschebahn.train.position.description = Gibt die Position des Zuges im Fahrplan an. z.B. wird mit 1 der erste Zug im Fahrplan selektiert, mit 2 der Zweite usw.
# trip information channel types
channel-type.deutschebahn.category.label = Kateogrie
channel-type.deutschebahn.category.description = Die Kategorie des Zuges, z.B. "ICE" oder "RE".
channel-type.deutschebahn.number.label = Zugnummer
channel-type.deutschebahn.number.description = Die Zugnummer, z.B. "4523".
channel-type.deutschebahn.filter-flags.label = Filter
channel-type.deutschebahn.filter-flags.description = Filter für die Fahrt.
channel-type.deutschebahn.trip-type.label = Fahrttyp
channel-type.deutschebahn.trip-type.description = Gibt den Typ der Fahrt an.
channel-type.deutschebahn.owner.label = Eigentümer
channel-type.deutschebahn.owner.description = Gibt die eindeutige Kurzbezeichnung des EisenbahnVerkehrsUnternehmen des Zuges an.
# event channel types
channel-type.deutschebahn.planned-path.label = Geplante Route
channel-type.deutschebahn.planned-path.description = Gibt die geplante Route des Zuges an, dabei werden die Stationen mit | getrennt aufgelistet. Für Ankünfte besteht der Pfad aus den Halten, die vor der aktuellen Station kamen, das erste Element ist der Startbahnhof. Für Abfahrten werden die Stationen aufgelistet, die nach der aktuellen Station kommen. Das letzte Element ist der Zielbahnhof.
channel-type.deutschebahn.changed-path.label = Geändert Route
channel-type.deutschebahn.changed-path.description = Gibt die geänderte Route des Zuges an, dabei werden die Stationen mit | getrennt aufgelistet. Ist nicht gesetzt, falls keine Änderungen vorliegen.
channel-type.deutschebahn.planned-platform.label = Geplantes Gleis
channel-type.deutschebahn.planned-platform.description = Gibt das geplante Gleis an, auf dem der Zug ankommt/abfährt.
channel-type.deutschebahn.changed-platform.label = Geändertes Gleis
channel-type.deutschebahn.changed-platform.description = Gibt das geändert Gleis an, auf dem der Zug ankommt/abfährt. Ist nicht gesetzt, falls keine Änderungen vorliegen.
channel-type.deutschebahn.planned-time.label = Geplante Zeit
channel-type.deutschebahn.planned-time.description = Gibt die geplante Zeit für die Ankunft/Abfahrt des Zuges an.
channel-type.deutschebahn.changed-time.label = Geänderte Zeit
channel-type.deutschebahn.changed-time.description = Gibt die geänder Zeit für die Ankunft/Abfahrt des Zuges an. Ist nicht gesetzt, falls keine Änderungen vorliegen.
channel-type.deutschebahn.planned-status.label = Geplanter Status
channel-type.deutschebahn.planned-status.description = Gibt den Stauts des Fahrplaneintrags an.
channel-type.deutschebahn.changed-status.label = Geänderter Status
channel-type.deutschebahn.changed-status.description = Gibt den geänderten Status des Fahrplaneintrags an. Ist nicht gesetzt, falls keine Änderungen vorliegen.
channel-type.deutschebahn.cancellation-time.label = Stornierungs-Zeitpunkt
channel-type.deutschebahn.cancellation-time.description = Gibt den Zeitpunkt an, an dem der Halt storniert wurde.
channel-type.deutschebahn.line.label = Linie
channel-type.deutschebahn.line.description = Gibt die Linie des Zuges an.
channel-type.deutschebahn.messages.label = Meldungen
channel-type.deutschebahn.messages.description = Textmeldungen, die für diese Ankunft/Abfahrt des Zuges vorliegen. Mehrere Meldungen werden mit einem Strich getrennt ausgegeben.
channel-type.deutschebahn.hidden.label = Versteckt
channel-type.deutschebahn.hidden.description = Gibt an, ob die Ankunft/Abfahrt im Fahrplan nicht angezeigt werden soll, da ein Ein-/Aussteigen nicht möglich ist.
channel-type.deutschebahn.wings.label = Wing
channel-type.deutschebahn.wings.description = Gibt eine Folge | separierten "Trip-IDs"an.
channel-type.deutschebahn.transition.label = Übergang
channel-type.deutschebahn.transition.description = Gibt bei Zügen, die zusmmengeführt oder getrennt werden die Trip-ID des vorherigen oder nachfolgenden Zuges an.
channel-type.deutschebahn.planned-distant-endpoint.label = Geplanter entfernter Endpunkt
channel-type.deutschebahn.planned-distant-endpoint.description = Gibt den geplanten entfernten Endpunkt des Zuges an.
channel-type.deutschebahn.changed-distant-endpoint.label = Geänderter entfernter Endpunkt
channel-type.deutschebahn.changed-distant-endpoint.description = Gibt den geänderten entfernten Endpunkt des Zuges an. Ist nicht gesetzt, falls keine Änderungen vorliegen.
channel-type.deutschebahn.distant-change.label = Geänderter Zielbahnhof
channel-type.deutschebahn.distant-change.description = Gibt den geänderten Zielbahnhof des Zuges an.
channel-type.deutschebahn.planned-final-station.label = Geplanter Start-/Zielbahnhof
channel-type.deutschebahn.planned-final-station.description = Gibt den geplanten Startbahnhof (für Ankünfte) bzw. Zielbahnhof (für Abfahrten) an.
channel-type.deutschebahn.planned-intermediate-stations.label = Geplante Halte
channel-type.deutschebahn.planned-intermediate-stations.description = Gibt die geplanten Halte des Zuges auf dem Weg zum aktuellen Bahnhof an (für Ankünfte) bzw. die folgenden Halte (für Abfahrten).
channel-type.deutschebahn.changed-final-station.label = Geänderter Start-/Zielbahnhof
channel-type.deutschebahn.changed-final-station.description = Gibt den geänderten Startbahnhof (für Ankünfte) bzw. Zielbahnhof (für Abfahrten) an. Ist nicht gesetzt, falls keine Änderungen vorliegen.
channel-type.deutschebahn.changed-intermediate-stations.label = Geänderte Halte
channel-type.deutschebahn.changed-intermediate-stations.description = Gibt die geänderten Halte des Zuges auf dem Weg zum aktuellen Bahnhof an (für Ankünfte) bzw. die folgenden Halte (für Abfahrten). Ist nicht gesetzt, falls keine Änderungen vorliegen.

View File

@ -0,0 +1,342 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="deutschebahn"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="timetable">
<label>Deutsche Bahn Timetable</label>
<description>Connection to the timetable API of Deutsche Bahn. Provides timetable data that can be displayed using the
train things.</description>
<config-description>
<parameter name="accessToken" type="text" required="true">
<label>Access Token</label>
<description>Access Token from Deutsche Bahn developer portal for accessing the webservice api.</description>
</parameter>
<parameter name="evaNo" type="text" required="true" pattern="80\d{5,5}">
<label>EvaNo of Station</label>
<description>evaNo of the station, for which the timetable should be requested. see
https://data.deutschebahn.com/dataset.tags.EVA-Nr..html</description>
</parameter>
<parameter name="trainFilter" type="text" readOnly="false">
<advanced>true</advanced>
<default>departures</default>
<label>Train Filter</label>
<description>Selects the trains that will be be displayed in this timetable. If not set only departures will be
provided.</description>
<options>
<option value="all">All</option>
<option value="arrivals">Arrivals</option>
<option value="departures">Departures</option>
</options>
</parameter>
</config-description>
</bridge-type>
<thing-type id="train">
<supported-bridge-type-refs>
<bridge-type-ref id="timetable"/>
</supported-bridge-type-refs>
<label>Train</label>
<description>Displays informations about an train within the given timetable at one station.</description>
<channel-groups>
<channel-group typeId="tripAttributes" id="trip">
<label>Trip</label>
<description>Contains all informations about the trip of the train.
</description>
</channel-group>
<channel-group typeId="eventAttributes" id="arrival">
<label>Arrival</label>
<description>Contains all informations about the arrival of the train at the station.
Channels may be empty, if the
trains starts at this station.
</description>
</channel-group>
<channel-group typeId="eventAttributes" id="departure">
<label>Departure</label>
<description>Contains all informations about the departure of the train at the station.
Channels may be empty, if the
trains ends at this station.
</description>
</channel-group>
</channel-groups>
<config-description>
<parameter name="position" type="decimal" min="1">
<label>Position</label>
<description>Selects the position of the train in the timetable.</description>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="tripAttributes">
<label>Trip Attributes</label>
<description>Contains all informations about the trip of the train.</description>
<channels>
<channel typeId="category" id="category"/>
<channel typeId="number" id="number"/>
<channel typeId="filter-flags" id="filter-flags"/>
<channel typeId="trip-type" id="trip-type"/>
<channel typeId="owner" id="owner"/>
</channels>
</channel-group-type>
<channel-group-type id="eventAttributes">
<label>Event Attributes</label>
<description>Contains all attributes for an event (arrival / departure) of an train at the station.</description>
<channels>
<channel typeId="planned-path" id="planned-path"/>
<channel typeId="changed-path" id="changed-path"/>
<channel typeId="planned-platform" id="planned-platform"/>
<channel typeId="changed-platform" id="changed-platform"/>
<channel typeId="planned-time" id="planned-time"/>
<channel typeId="changed-time" id="changed-time"/>
<channel typeId="planned-status" id="planned-status"/>
<channel typeId="changed-status" id="changed-status"/>
<channel typeId="cancellation-time" id="cancellation-time"/>
<channel typeId="line" id="line"/>
<channel typeId="messages" id="messages"/>
<channel typeId="hidden" id="hidden"/>
<channel typeId="wings" id="wings"/>
<channel typeId="transition" id="transition"/>
<channel typeId="planned-distant-endpoint" id="planned-distant-endpoint"/>
<channel typeId="changed-distant-endpoint" id="changed-distant-endpoint"/>
<channel typeId="distant-change" id="distant-change"/>
<channel typeId="planned-final-station" id="planned-final-station"/>
<channel typeId="planned-intermediate-stations" id="planned-intermediate-stations"/>
<channel typeId="changed-final-station" id="changed-final-station"/>
<channel typeId="changed-intermediate-stations" id="changed-intermediate-stations"/>
</channels>
</channel-group-type>
<!-- Channel Types for trip label attributes. -->
<channel-type id="category">
<item-type>String</item-type>
<label>Category</label>
<description>Provides the category of the trip, e.g. "ICE" or "RE".</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="number">
<item-type>String</item-type>
<label>Number</label>
<description>Provides the trip/train number, e.g. "4523".</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="filter-flags" advanced="true">
<item-type>String</item-type>
<label>Filter Flags</label>
<description>Provides the filter flags.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="trip-type" advanced="true">
<item-type>String</item-type>
<label>Trip Type</label>
<description>Provides the type of the trip.</description>
<state readOnly="true">
<options>
<option value="p"/>
<option value="e"/>
<option value="z"/>
<option value="s"/>
<option value="h"/>
<option value="n"/>
</options>
</state>
</channel-type>
<channel-type id="owner" advanced="true">
<item-type>String</item-type>
<label>Owner</label>
<description>Provides the owner of the train. A unique short-form and only intended to map a trip to specific evu
(EisenbahnVerkehrsUnternehmen).</description>
<state readOnly="true"/>
</channel-type>
<!-- Channel types for event attributes. -->
<channel-type id="planned-platform">
<item-type>String</item-type>
<label>Planned Platform</label>
<description>Provides the planned platform of a train.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="changed-platform">
<item-type>String</item-type>
<label>Changed Platform</label>
<description>Provides the changed platform of a train.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="planned-time">
<item-type>DateTime</item-type>
<label>Planned Time</label>
<description>Provides the planned time of a train.</description>
<state readOnly="true" pattern="%1$tH:%1$tM"/>
</channel-type>
<channel-type id="changed-time">
<item-type>DateTime</item-type>
<label>Changed Time</label>
<description>Provides the changed time of a train.</description>
<state readOnly="true" pattern="%1$tH:%1$tM"/>
</channel-type>
<channel-type id="planned-status">
<item-type>String</item-type>
<label>Planned Status</label>
<description>Provides the planned status of a train.</description>
<state readOnly="true">
<options>
<option value="p">Planned</option>
<option value="a">Added</option>
<option value="c">Cancelled</option>
</options>
</state>
</channel-type>
<channel-type id="changed-status">
<item-type>String</item-type>
<label>Changed Status</label>
<description>Provides the changed status of a train.</description>
<state readOnly="true">
<options>
<option value="p">Planned</option>
<option value="a">Added</option>
<option value="c">Cancelled</option>
</options>
</state>
</channel-type>
<channel-type id="line">
<item-type>String</item-type>
<label>Line</label>
<description>The line indicator.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="messages">
<item-type>String</item-type>
<label>Messages</label>
<description>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.</description>
<state readOnly="true"/>
</channel-type>
<!-- Advanced channels -->
<channel-type id="cancellation-time" advanced="true">
<item-type>DateTime</item-type>
<label>Cancellation Time</label>
<description>Time when the cancellation of this stop was created.</description>
<state readOnly="true" pattern="%1$tH:%1$tM"/>
</channel-type>
<channel-type id="planned-path" advanced="true">
<item-type>String</item-type>
<label>Planned Path</label>
<description>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 trips
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 trips destination station. Note that
the current station is never included in the path
(neither for arrival nor for departure).</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="changed-path" advanced="true">
<item-type>String</item-type>
<label>Changed Path</label>
<description>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 trips
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 trips destination station. Note that
the current station is never included in the path
(neither for arrival nor for departure).</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="hidden" advanced="true">
<item-type>Switch</item-type>
<label>Hidden</label>
<description>On if the event should not be shown, because travellers are not supposed to enter or exit the train
at
this stop.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="wings" advanced="true">
<item-type>String</item-type>
<label>Wings</label>
<description>A sequence of trip id separated by the pipe symbols (“|”).</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="transition" advanced="true">
<item-type>String</item-type>
<label>Transition</label>
<description>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.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="planned-distant-endpoint" advanced="true">
<item-type>String</item-type>
<label>Planned Distant Endpoint</label>
<description>Planned distant endpoint.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="changed-distant-endpoint" advanced="true">
<item-type>String</item-type>
<label>Changed Distant Endpoint</label>
<description>Changed distant endpoint.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="distant-change" advanced="true">
<item-type>Number</item-type>
<label>Distant Change</label>
<description>distant change</description>
<state readOnly="true"/>
</channel-type>
<!-- Channels with derived values from other channels -->
<channel-type id="planned-final-station">
<item-type>String</item-type>
<label>Planned Final Station</label>
<description>Planned final station of the train. For arrivals the starting station is returned, for departures the
target station is returned.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="planned-intermediate-stations">
<item-type>String</item-type>
<label>Planned Intermediate Stations</label>
<description>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.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="changed-final-station">
<item-type>String</item-type>
<label>Changed Final Station</label>
<description>Changed final station of the train. For arrivals the starting station is returned, for departures the
target station is returned.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="changed-intermediate-stations">
<item-type>String</item-type>
<label>Changed Intermediate Stations</label>
<description>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.</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,441 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="jaxbEntity">
<xs:sequence/>
</xs:complexType>
<xs:element name="conn" type="connection"/>
<xs:element name="details" type="stopDetails"/>
<xs:element name="event" type="event"/>
<xs:element name="m" type="message"/>
<xs:element name="s" type="timetableStop"/>
<xs:element name="station" type="stationData"/>
<xs:element name="station-details" type="stationDetails"/>
<xs:element name="stations" type="multipleStationData"/>
<xs:element name="timetable" type="timetable"/>
<xs:element name="timetables" type="multipleTimetables"/>
<xs:element name="tl" type="tripLabel"/>
<xs:element name="trip" type="trip"/>
<xs:element name="trips" type="multipleTrips"/>
<xs:complexType name="multipleStationData">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="station" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="multipleTimetables">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="timetable" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="timetable">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="s" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="m" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="station" type="xs:string"/>
<xs:attribute name="eva" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="timetableStop">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="conn" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="m" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="ar" type="event" minOccurs="0"/>
<xs:element name="dp" type="event" minOccurs="0"/>
<xs:element name="hd" type="historicDelay" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="hpc" type="historicPlatformChange" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="rtr" type="referenceTripRelation" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="tl" minOccurs="0"/>
<xs:element name="ref" type="tripReference" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="eva" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="stop" abstract="true">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="conn" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="m" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="ar" type="event" minOccurs="0"/>
<xs:element name="dp" type="event" minOccurs="0"/>
<xs:element name="hd" type="historicDelay" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="hpc" type="historicPlatformChange" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="rtr" type="referenceTripRelation" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tripLabel">
<xs:complexContent>
<xs:extension base="referenceTripLabel">
<xs:sequence/>
<xs:attribute name="f" type="xs:string"/>
<xs:attribute name="t" type="tripType"/>
<xs:attribute name="o" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="connection">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element name="ref" type="timetableStop" minOccurs="0"/>
<xs:element ref="s"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="ts" type="xs:string" use="required"/>
<xs:attribute name="eva" type="xs:string"/>
<xs:attribute name="cs" type="connectionStatus" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="message">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element name="dm" type="distributorMessage" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="tl" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="t" type="messageType" use="required"/>
<xs:attribute name="from" type="xs:string"/>
<xs:attribute name="to" type="xs:string"/>
<xs:attribute name="c" type="xs:int"/>
<xs:attribute name="int" type="xs:string"/>
<xs:attribute name="ext" type="xs:string"/>
<xs:attribute name="elnk" type="xs:string"/>
<xs:attribute name="cat" type="xs:string"/>
<xs:attribute name="ec" type="xs:int"/>
<xs:attribute name="ts" type="xs:string" use="required"/>
<xs:attribute name="pr" type="priority"/>
<xs:attribute name="o" type="xs:string"/>
<xs:attribute name="del" type="xs:int"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="distributorMessage">
<xs:sequence/>
<xs:attribute name="t" type="distributorType"/>
<xs:attribute name="n" type="xs:string"/>
<xs:attribute name="int" type="xs:string"/>
<xs:attribute name="ts" type="xs:string"/>
</xs:complexType>
<xs:complexType name="event">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="m" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="cpth" type="xs:string"/>
<xs:attribute name="ppth" type="xs:string"/>
<xs:attribute name="cp" type="xs:string"/>
<xs:attribute name="pp" type="xs:string"/>
<xs:attribute name="ct" type="xs:string"/>
<xs:attribute name="pt" type="xs:string"/>
<xs:attribute name="cs" type="eventStatus"/>
<xs:attribute name="ps" type="eventStatus"/>
<xs:attribute name="hi" type="xs:int"/>
<xs:attribute name="clt" type="xs:string"/>
<xs:attribute name="wings" type="xs:string"/>
<xs:attribute name="tra" type="xs:string"/>
<xs:attribute name="pde" type="xs:string"/>
<xs:attribute name="cde" type="xs:string"/>
<xs:attribute name="dc" type="xs:int"/>
<xs:attribute name="l" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="historicChange">
<xs:sequence/>
<xs:attribute name="ts" type="xs:string"/>
</xs:complexType>
<xs:complexType name="historicDelay">
<xs:complexContent>
<xs:extension base="historicChange">
<xs:sequence/>
<xs:attribute name="ar" type="xs:string"/>
<xs:attribute name="dp" type="xs:string"/>
<xs:attribute name="src" type="delaySource"/>
<xs:attribute name="cod" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="historicPlatformChange">
<xs:complexContent>
<xs:extension base="historicChange">
<xs:sequence/>
<xs:attribute name="ar" type="xs:string"/>
<xs:attribute name="dp" type="xs:string"/>
<xs:attribute name="cot" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="stationDetails">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="m" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="eva" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="multipleTrips">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="trip" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="trip">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="tl"/>
<xs:element ref="s" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tripStop">
<xs:complexContent>
<xs:extension base="stop">
<xs:sequence/>
<xs:attribute name="i" type="xs:int" use="required"/>
<xs:attribute name="eva" type="xs:string" use="required"/>
<xs:attribute name="jt" type="junctionType"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="stopDetails">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="m" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="conn" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="connectionStatus">
<xs:restriction base="xs:string">
<xs:enumeration value="w"/>
<xs:enumeration value="n"/>
<xs:enumeration value="a"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="messageType">
<xs:restriction base="xs:string">
<xs:enumeration value="h"/>
<xs:enumeration value="q"/>
<xs:enumeration value="f"/>
<xs:enumeration value="d"/>
<xs:enumeration value="i"/>
<xs:enumeration value="u"/>
<xs:enumeration value="r"/>
<xs:enumeration value="c"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="priority">
<xs:restriction base="xs:string">
<xs:enumeration value="1"/>
<xs:enumeration value="2"/>
<xs:enumeration value="3"/>
<xs:enumeration value="4"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="distributorType">
<xs:restriction base="xs:string">
<xs:enumeration value="s"/>
<xs:enumeration value="r"/>
<xs:enumeration value="f"/>
<xs:enumeration value="x"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="eventStatus">
<xs:restriction base="xs:string">
<xs:enumeration value="p"/>
<xs:enumeration value="a"/>
<xs:enumeration value="c"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="delaySource">
<xs:restriction base="xs:string">
<xs:enumeration value="L"/>
<xs:enumeration value="NA"/>
<xs:enumeration value="NM"/>
<xs:enumeration value="V"/>
<xs:enumeration value="IA"/>
<xs:enumeration value="IM"/>
<xs:enumeration value="A"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="filterFlag">
<xs:restriction base="xs:string">
<xs:enumeration value="EXTERNAL"/>
<xs:enumeration value="LONG_DISTANCE"/>
<xs:enumeration value="REGIONAL"/>
<xs:enumeration value="SBAHN"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="junctionType">
<xs:restriction base="xs:string">
<xs:enumeration value="t"/>
<xs:enumeration value="s"/>
<xs:enumeration value="j"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="referenceTripRelation">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element name="rt" type="referenceTrip" minOccurs="0"/>
<xs:element name="rts" type="referenceTripRelationToStop" minOccurs="0"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="referenceTrip">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element name="rtl" type="referenceTripLabel" minOccurs="0"/>
<xs:element name="sd" type="referenceTripStopLabel" minOccurs="0"/>
<xs:element name="ea" type="referenceTripStopLabel" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="c" type="xs:boolean" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="referenceTripLabel">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence/>
<xs:attribute name="c" type="xs:string" use="required"/>
<xs:attribute name="n" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="referenceTripStopLabel">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence/>
<xs:attribute name="eva" type="xs:string" use="required"/>
<xs:attribute name="n" type="xs:string" use="required"/>
<xs:attribute name="i" type="xs:int" use="required"/>
<xs:attribute name="pt" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="referenceTripRelationToStop">
<xs:restriction base="xs:string">
<xs:enumeration value="b"/>
<xs:enumeration value="e"/>
<xs:enumeration value="c"/>
<xs:enumeration value="s"/>
<xs:enumeration value="a"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="tripReference">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence>
<xs:element ref="tl" minOccurs="0"/>
<xs:element name="rt" type="referenceTrip" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="stationData">
<xs:complexContent>
<xs:extension base="jaxbEntity">
<xs:sequence/>
<xs:attribute name="p" type="xs:string"/>
<xs:attribute name="meta" type="xs:string"/>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="eva" type="xs:string" use="required"/>
<xs:attribute name="ds100" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="tripType">
<xs:restriction base="xs:string">
<xs:enumeration value="p"/>
<xs:enumeration value="e"/>
<xs:enumeration value="z"/>
<xs:enumeration value="s"/>
<xs:enumeration value="h"/>
<xs:enumeration value="n"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@ -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<Thing> 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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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 <VALUE_TYPE, STATE_TYPE extends State> void doTestEventAttribute( //
String channelName, //
@Nullable String expectedChannelName, //
Consumer<Event> setValue, //
VALUE_TYPE expectedValue, //
@Nullable STATE_TYPE expectedState, //
EventType eventType, //
boolean performSetterTest) { //
final EventAttribute<VALUE_TYPE, STATE_TYPE> attribute = (EventAttribute<VALUE_TYPE, STATE_TYPE>) 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<Message> 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<Message> 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);
}
}

View File

@ -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 <VALUE_TYPE, STATE_TYPE extends State> void doTestTripAttribute( //
String channelName, //
@Nullable String expectedChannelName, //
Consumer<TripLabel> setValue, //
VALUE_TYPE expectedValue, //
@Nullable STATE_TYPE expectedState, //
boolean performSetterTest) { //
final TripLabelAttribute<VALUE_TYPE, STATE_TYPE> attribute = (TripLabelAttribute<VALUE_TYPE, STATE_TYPE>) 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);
}
}

View File

@ -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<Date> {
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);
}
}

View File

@ -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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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<TimetableStop> 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());
}
}

View File

@ -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<String> requestedPlanUrls;
private final List<String> requestedFullChangesUrls;
private final List<String> requestedRecentChangesUrls;
// Allows simulation of available data.
// if not set all available files will be served.
private @Nullable Set<String> availableUrls = null;
public TimetableStubHttpCallable(File testdataDir) {
this.testdataDir = testdataDir;
this.requestedPlanUrls = new ArrayList<>();
this.requestedFullChangesUrls = new ArrayList<String>();
this.requestedRecentChangesUrls = new ArrayList<String>();
}
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<Matcher, String> 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<String> getRequestedPlanUrls() {
return requestedPlanUrls;
}
public List<String> getRequestedFullChangesUrls() {
return requestedFullChangesUrls;
}
public List<String> getRequestedRecentChangesUrls() {
return requestedRecentChangesUrls;
}
}

View File

@ -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<String> getRequestedPlanUrls() {
return httpStub.getRequestedPlanUrls();
}
public List<String> getRequestedFullChangesUrls() {
return httpStub.getRequestedFullChangesUrls();
}
public List<String> getRequestedRecentChangesUrls() {
return httpStub.getRequestedRecentChangesUrls();
}
public TimetablesV1ApiFactory getApiFactory() {
return new TimetablesV1ApiFactory() {
@Override
public TimetablesV1Api create(String authToken, HttpCallable httpCallable)
throws JAXBException, SAXException, URISyntaxException {
return api;
}
};
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
<timetable station="Lehrte" eva="8000226">
<s id="-5296516961807204721-2108160906-5">
<ar ct="2108160940" l="3"/>
<dp ct="2108160942" l="3"/>
</s>
</timetable>

View File

@ -0,0 +1,282 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Hannover Hbf'>
<s id="-368596468835976260-2110141151-1">
<tl f="D" t="p" o="X1" c="erx" n="83718" />
<dp pt="2110141151" pp="2" l="RB38"
ppth="Langenhagen Mitte|Mellendorf|Lindwedel|Schwarmstedt|Hodenhagen|Walsrode|Bad Fallingbostel|Dorfmark|Soltau(Han)|Soltau Nord|Wolterdingen(Han)|Schneverdingen|Wintermoor|Handeloh|B&#252;senbachtal|Holm-Seppensen|Suerhop|Buchholz(Nordheide)" />
</s>
<s id="2538467373743306803-2110140832-6">
<tl f="F" t="p" o="80" c="ICE" n="682" />
<ar pt="2110141132" pp="8" wings="-9101728246304835337-2110140832"
ppth="N&#252;rnberg Hbf|W&#252;rzburg Hbf|Fulda|Kassel-Wilhelmsh&#246;he|G&#246;ttingen" />
<dp pt="2110141136" pp="8" ppth="Hamburg-Harburg|Hamburg Hbf|L&#252;beck Hbf" />
</s>
<s id="-1279242321187232732-2110141007-3">
<tl f="F" t="p" o="80" c="ICE" n="75" />
<ar pt="2110141138" pp="3" ppth="Hamburg-Altona|Hamburg Hbf" />
<dp pt="2110141141" pp="3"
ppth="G&#246;ttingen|Kassel-Wilhelmsh&#246;he|Frankfurt(Main)Hbf|Mannheim Hbf|Karlsruhe Hbf|Baden-Baden|Freiburg(Breisgau) Hbf|Basel Bad Bf|Basel SBB|Z&#252;rich HB|Sargans|Landquart|Chur" />
</s>
<s id="-6167522321062055942-2110141136-1">
<tl f="D" t="p" o="R1" c="ME" n="82815" />
<dp pt="2110141136" pp="7" l="RE2"
ppth="Sarstedt|Nordstemmen|Elze(Han)|Banteln|Alfeld(Leine)|Freden(Leine)|Kreiensen|Einbeck-Salzderhelden|Northeim(Han)|N&#246;rten-Hardenberg|G&#246;ttingen" />
</s>
<s id="5703989328735164692-2110140935-5">
<tl f="F" t="p" o="80" c="ICE" n="942" />
<ar pt="2110141128" pp="12" wings="1341469202977470214-2110140935"
ppth="Berlin Ostbahnhof|Berlin Hbf|Berlin-Spandau|Wolfsburg Hbf" />
<dp pt="2110141131" pp="12" wings="1341469202977470214-2110140935"
ppth="Bielefeld Hbf|Hamm(Westf)Hbf|Dortmund Hbf|Bochum Hbf|Essen Hbf|Duisburg Hbf|D&#252;sseldorf Flughafen|D&#252;sseldorf Hbf" />
</s>
<s id="5687381020734438255-2110141034-16">
<tl f="S" t="p" o="800244" c="S" n="34170" />
<ar pt="2110141125" pp="2" l="1" tra="8235987991787718627-2110141128-1"
ppth="Haste|Bad Nenndorf|Bantorf|Winninghausen|Barsinghausen|Kirchdorf(Deister)|Egestorf(Deister)|Wennigsen(Deister)|Lemmie|Weetzen|Ronnenberg|Empelde|Hannover-Bornum|Hannover-Linden/Fischerhof|Hannover Bismarckstr." />
</s>
<s id="-4382382001083802173-2110141106-7">
<tl f="S" t="p" o="800244" c="S" n="34545" />
<ar pt="2110141123" pp="1" l="5"
ppth="Hannover Flughafen|Langenhagen Pferdemarkt|Langenhagen Mitte|Hannover-Vinnhorst|Hannover-Ledeburg|Hannover-Nordstadt" />
<dp pt="2110141125" pp="1" l="5"
ppth="Hannover Bismarckstr.|Hannover-Linden/Fischerhof|Weetzen|Holtensen/Linderte|Bennigsen|V&#246;lksen/Eldagsen|Springe|Bad M&#252;nder(Deister)|Hameln" />
</s>
<s id="-4789704478638952900-2110141050-10">
<tl f="S" t="p" o="800244" c="S" n="34546" />
<ar pt="2110141133" pp="2" l="5"
ppth="Hameln|Bad M&#252;nder(Deister)|Springe|V&#246;lksen/Eldagsen|Bennigsen|Holtensen/Linderte|Weetzen|Hannover-Linden/Fischerhof|Hannover Bismarckstr." />
<dp pt="2110141135" pp="2" l="5"
ppth="Hannover-Nordstadt|Hannover-Ledeburg|Hannover-Vinnhorst|Langenhagen Mitte|Langenhagen Pferdemarkt|Hannover Flughafen" />
</s>
<s id="7017358269185663424-2110141040-7">
<tl f="S" t="p" o="800244" c="S" n="34611" />
<ar pt="2110141115" pp="14" l="6" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse|Hannover Karl-Wiechert-Allee" />
</s>
<s id="2220256233596246487-2110141009-9">
<tl f="D" t="p" o="R1" c="ME" n="82865" />
<ar pt="2110141115" pp="4" l="RE3" pde="Hamburg Hbf"
ppth="Uelzen|Suderburg|Unterl&#252;&#223;|Eschede|Celle|Gro&#223;burgwedel|Isernhagen|Langenhagen Mitte" />
</s>
<s id="-3862718574500346031-2110141120-1">
<tl f="N" t="p" o="800295" c="RE" n="4414" />
<dp pt="2110141120" pp="12" l="1"
ppth="Wunstorf|Neustadt am R&#252;benberge|Nienburg(Weser)|Eystrup|D&#246;rverden|Verden(Aller)|Achim|Bremen-Mahndorf|Bremen Hbf|Delmenhorst|Hude|Oldenburg(Oldb)|Bad Zwischenahn|Westerstede-Ocholt|Augustfehn|Leer(Ostfriesl)|Emden Hbf|Marienhafe|Norden|Norddeich|Norddeich Mole" />
</s>
<s id="4458426124091069798-2110140700-13">
<tl f="F" t="p" o="84" c="IC" n="141" />
<ar pt="2110141119" pp="9"
ppth="Amsterdam Centraal|Hilversum|Amersfoort Centraal|Apeldoorn|Deventer|Almelo|Hengelo|Bad Bentheim|Rheine|Osnabr&#252;ck Hbf|Bad Oeynhausen|Minden(Westf)" />
<dp pt="2110141122" pp="9" ppth="Wolfsburg Hbf|Stendal Hbf|Berlin-Spandau|Berlin Hbf" />
</s>
<s id="-5299413353021537464-2110140711-10">
<tl f="F" t="p" o="80" c="IC" n="2046" />
<ar pt="2110141123" pp="11"
ppth="Dresden Hbf|Dresden-Neustadt|Riesa|Leipzig Hbf|Leipzig/Halle Flughafen|Halle(Saale)Hbf|K&#246;then|Magdeburg Hbf|Braunschweig Hbf" />
<dp pt="2110141140" pp="11"
ppth="Minden(Westf)|Herford|Bielefeld Hbf|G&#252;tersloh Hbf|Hamm(Westf)Hbf|Hagen Hbf|Wuppertal Hbf|Solingen Hbf|K&#246;ln Hbf" />
</s>
<s id="1181392732754916068-2110140736-16">
<tl f="F" t="p" o="80" c="IC" n="2037" />
<ar pt="2110141113" pp="10"
ppth="Norddeich Mole|Norddeich|Norden|Marienhafe|Emden Hbf|Leer(Ostfriesl)|Augustfehn|Westerstede-Ocholt|Bad Zwischenahn|Oldenburg(Oldb)|Hude|Delmenhorst|Bremen Hbf|Verden(Aller)|Nienburg(Weser)" />
<dp pt="2110141136" pp="10" ppth="Braunschweig Hbf|Helmstedt|Magdeburg Hbf|K&#246;then|Halle(Saale)Hbf|Leipzig Hbf" />
</s>
<s id="-7716809412790479831-2110141104-1">
<tl f="S" t="p" o="800244" c="S" n="34722" />
<dp pt="2110141104" pp="14" l="7"
ppth="Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten|Lehrte|Aligse|Burgdorf|Otze|Ehlershausen|Celle" />
</s>
<s id="8235987991787718627-2110141128-1">
<tl f="S" t="p" o="800244" c="S" n="34120" />
<dp pt="2110141128" pp="2" l="1" tra="5687381020734438255-2110141034-16"
ppth="Hannover-Nordstadt|Hannover-Leinhausen|Letter|Seelze|Dedensen-G&#252;mmer|Wunstorf|Haste|Lindhorst(Schaumb-Lippe)|Stadthagen|Kirchhorsten|B&#252;ckeburg|Minden(Westf)" />
</s>
<s id="-5592824990971072116-2110141011-7">
<tl f="F" t="p" o="80" c="ICE" n="1579" />
<ar pt="2110141158" pp="4" ppth="Hamburg-Altona|Hamburg Hbf|Hamburg-Harburg|L&#252;neburg|Uelzen|Celle" />
<dp pt="2110141201" pp="4"
ppth="G&#246;ttingen|Kassel-Wilhelmsh&#246;he|Treysa|Marburg(Lahn)|Gie&#223;en|Friedberg(Hess)|Frankfurt(Main)Hbf|Darmstadt Hbf|Bensheim|Weinheim(Bergstr)Hbf|Heidelberg Hbf|Wiesloch-Walldorf|Bruchsal|Karlsruhe Hbf" />
</s>
<s id="-2259513983107179971-2110141110-1">
<tl f="S" t="p" o="800244" c="S" n="34428" />
<dp pt="2110141110" pp="2" l="4"
ppth="Hannover-Nordstadt|Hannover-Ledeburg|Hannover-Vinnhorst|Langenhagen Mitte|Langenhagen Pferdemarkt|Langenhagen-Kaltenweide|Bissendorf|Mellendorf|Bennem&#252;hlen" />
</s>
<s id="7270360564156475774-2110141009-12">
<tl f="D" t="p" o="R1" c="ME" n="82822" />
<ar pt="2110141126" pp="7" l="RE2"
ppth="G&#246;ttingen|N&#246;rten-Hardenberg|Northeim(Han)|Einbeck-Salzderhelden|Kreiensen|Freden(Leine)|Alfeld(Leine)|Banteln|Elze(Han)|Nordstemmen|Sarstedt" />
</s>
<s id="-2379689238391834001-2110141023-10">
<tl f="D" t="p" o="W3" c="WFB" n="95827" />
<ar pt="2110141150" pp="9" l="RE70"
ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf" />
<dp pt="2110141155" pp="9" l="RE70" ppth="Lehrte|H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf" />
</s>
<s id="8260041120522098561-2110141035-13">
<tl f="S" t="p" o="800244" c="S" n="34121" />
<ar pt="2110141130" pp="1" l="1" tra="-4278327689275423766-2110141133-1"
ppth="Minden(Westf)|B&#252;ckeburg|Kirchhorsten|Stadthagen|Lindhorst(Schaumb-Lippe)|Haste|Wunstorf|Dedensen-G&#252;mmer|Seelze|Letter|Hannover-Leinhausen|Hannover-Nordstadt" />
</s>
<s id="-503063856107069988-2110140943-5">
<tl f="F" t="p" o="80" c="ICE" n="787" />
<ar pt="2110141120" pp="3" ppth="Hamburg-Altona|Hamburg Dammtor|Hamburg Hbf|Hamburg-Harburg" />
<dp pt="2110141126" pp="3"
ppth="G&#246;ttingen|Kassel-Wilhelmsh&#246;he|Fulda|W&#252;rzburg Hbf|Augsburg Hbf|M&#252;nchen Hbf" />
</s>
<s id="-4054196780072035175-2110141054-7">
<tl f="D" t="p" o="W3" c="WFB" n="95762" />
<ar pt="2110141141" pp="12" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald|Lehrte" />
</s>
<s id="2546282974817998243-2110141051-10">
<tl f="S" t="p" o="800244" c="S" n="34427" />
<ar pt="2110141117" pp="1" l="4"
ppth="Bennem&#252;hlen|Mellendorf|Bissendorf|Langenhagen-Kaltenweide|Langenhagen Pferdemarkt|Langenhagen Mitte|Hannover-Vinnhorst|Hannover-Ledeburg|Hannover-Nordstadt" />
<dp pt="2110141119" pp="1" l="4"
ppth="Hannover Bismarckstr.|Hannover Messe/Laatzen|Rethen(Leine)|Sarstedt|Barnten|Emmerke|Hildesheim Hbf" />
</s>
<s id="8659063088626413270-2110141136-7">
<tl f="S" t="p" o="800244" c="S" n="34547" />
<ar pt="2110141153" pp="1" l="5"
ppth="Hannover Flughafen|Langenhagen Pferdemarkt|Langenhagen Mitte|Hannover-Vinnhorst|Hannover-Ledeburg|Hannover-Nordstadt" />
<dp pt="2110141155" pp="1" l="5"
ppth="Hannover Bismarckstr.|Hannover-Linden/Fischerhof|Weetzen|Holtensen/Linderte|Bennigsen|V&#246;lksen/Eldagsen|Springe|Bad M&#252;nder(Deister)|Hameln|Emmerthal|Bad Pyrmont|L&#252;gde|Schieder|Steinheim(Westf)|Altenbeken|Paderborn Hbf" />
</s>
<s id="-2569164296520890912-2110141141-1">
<tl f="S" t="p" o="800244" c="S" n="34612" />
<dp pt="2110141141" pp="14" l="6" ppth="Hannover Karl-Wiechert-Allee|Aligse|Burgdorf|Otze|Ehlershausen|Celle" />
</s>
<s id="-9101728246304835337-2110140832-6">
<tl f="F" t="p" o="80" c="ICE" n="632" />
<ar pt="2110141132" pp="8" ppth="N&#252;rnberg Hbf|W&#252;rzburg Hbf|Fulda|Kassel-Wilhelmsh&#246;he|G&#246;ttingen" />
<dp pt="2110141145" pp="8" ppth="Bremen Hbf" />
</s>
<s id="-1618433914809757000-2110140725-7">
<tl f="F" t="p" o="80" c="ICE" n="772" />
<ar pt="2110141117" pp="7"
ppth="Stuttgart Hbf|Mannheim Hbf|Frankfurt(M) Flughafen Fernbf|Frankfurt(Main)Hbf|Kassel-Wilhelmsh&#246;he|G&#246;ttingen" />
<dp pt="2110141120" pp="7" ppth="Hamburg Hbf|Hamburg-Altona" />
</s>
<s id="-7530588623595067801-2110141108-11">
<tl f="S" t="p" o="800244" c="S" n="34723" />
<ar pt="2110141155" pp="14" l="7"
ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse|Lehrte|Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld" />
</s>
<s id="6957029625618749347-2110140923-13">
<tl f="N" t="p" o="800295" c="RE" n="4413" />
<ar pt="2110141138" pp="9" l="8"
ppth="Bremerhaven-Lehe|Bremerhaven Hbf|Osterholz-Scharmbeck|Bremen Hbf|Bremen-Mahndorf|Achim|Verden(Aller)|D&#246;rverden|Eystrup|Nienburg(Weser)|Neustadt am R&#252;benberge|Wunstorf" />
</s>
<s id="1698974169433520451-2110140702-17">
<tl f="F" t="p" o="80" c="ICE" n="1672" />
<ar pt="2110141156" pp="8"
ppth="Karlsruhe Hbf|Karlsruhe-Durlach|Bruchsal|Wiesloch-Walldorf|Heidelberg Hbf|Weinheim(Bergstr)Hbf|Bensheim|Darmstadt Hbf|Frankfurt(Main)Hbf|Friedberg(Hess)|Gie&#223;en|Marburg(Lahn)|Treysa|Wabern(Bz Kassel)|Kassel-Wilhelmsh&#246;he|G&#246;ttingen" />
<dp pt="2110141159" pp="8" ppth="Celle|Uelzen|L&#252;neburg|Hamburg-Harburg|Hamburg Hbf|Hamburg-Altona" />
</s>
<s id="244929573019057867-2110141020-7">
<tl f="D" t="p" o="W3" c="WFB" n="95776" />
<ar pt="2110141105" pp="12" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald|Lehrte" />
<dp pt="2110141109" pp="12" l="RE60"
ppth="Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine" />
</s>
<s id="-2294735978985476965-2110141148-1">
<tl t="p" o="R0" c="ENO" n="83515" />
<dp pt="2110141148" pp="13" l="RE30"
ppth="Lehrte|Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf" />
</s>
<s id="4685591131365771688-2110141134-1">
<tl f="S" t="p" o="800244" c="S" n="34315" />
<dp pt="2110141134" pp="13" l="3"
ppth="Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten|Lehrte|Sehnde|Algermissen|Harsum|Hildesheim Hbf" />
</s>
<s id="2498470733363920454-2110141106-8">
<tl f="S" t="p" o="800244" c="S" n="34430" />
<ar pt="2110141138" pp="2" l="4"
ppth="Hildesheim Hbf|Emmerke|Barnten|Sarstedt|Rethen(Leine)|Hannover Messe/Laatzen|Hannover Bismarckstr." />
<dp pt="2110141140" pp="2" l="4"
ppth="Hannover-Nordstadt|Hannover-Ledeburg|Hannover-Vinnhorst|Langenhagen Mitte|Langenhagen Pferdemarkt|Langenhagen-Kaltenweide|Bissendorf|Mellendorf|Bennem&#252;hlen" />
</s>
<s id="-886919495737015388-2110141148-1">
<tl f="D" t="p" o="X2" c="erx" n="83465" />
<dp pt="2110141148" pp="7" l="RE10"
ppth="Sarstedt|Hildesheim Hbf|Hildesheim Ost|Derneburg(Han)|Baddeckenstedt|Salzgitter-Ringelheim|Goslar|Bad Harzburg" />
</s>
<s id="-16771105683292222-2110140913-19">
<tl f="D" t="p" o="X1" c="erx" n="83713" />
<ar pt="2110141108" pp="1" l="RB38"
ppth="Buchholz(Nordheide)|Suerhop|Holm-Seppensen|B&#252;senbachtal|Handeloh|Wintermoor|Schneverdingen|Wolterdingen(Han)|Soltau Nord|Soltau(Han)|Dorfmark|Bad Fallingbostel|Walsrode|Hodenhagen|Schwarmstedt|Lindwedel|Mellendorf|Langenhagen Mitte" />
</s>
<s id="9177503933658188673-2110141116-1">
<tl f="D" t="p" o="W3" c="WFB" n="95761" />
<dp pt="2110141116" pp="11" l="RE70" ppth="Lehrte|H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf" />
</s>
<s id="-2837041410263484254-2110140822-7">
<tl f="F" t="p" o="80" c="ICE" n="555" />
<ar pt="2110141132" pp="9" wings="-3319510478025615006-2110140814"
ppth="Bonn Hbf|K&#246;ln Hbf|Wuppertal Hbf|Hagen Hbf|Hamm(Westf)Hbf|Bielefeld Hbf" />
<dp pt="2110141135" pp="9" wings="-3319510478025615006-2110140814"
ppth="Berlin-Spandau|Berlin Hbf|Berlin Ostbahnhof" />
</s>
<s id="-4278327689275423766-2110141133-1">
<tl f="S" t="p" o="800244" c="S" n="34171" />
<dp pt="2110141133" pp="1" l="1" tra="8260041120522098561-2110141035-13"
ppth="Hannover Bismarckstr.|Hannover-Linden/Fischerhof|Hannover-Bornum|Empelde|Ronnenberg|Weetzen|Lemmie|Wennigsen(Deister)|Egestorf(Deister)|Kirchdorf(Deister)|Barsinghausen|Winninghausen|Bantorf|Bad Nenndorf|Haste" />
</s>
<s id="1341469202977470214-2110140935-5">
<tl f="F" t="p" o="80" c="ICE" n="952" />
<ar pt="2110141128" pp="12" ppth="Berlin Ostbahnhof|Berlin Hbf|Berlin-Spandau|Wolfsburg Hbf" />
<dp pt="2110141131" pp="12" ppth="Bielefeld Hbf|Hamm(Westf)Hbf|Hagen Hbf|Wuppertal Hbf|K&#246;ln Hbf" />
</s>
<s id="3959040045726460106-2110141121-10">
<tl f="S" t="p" o="800244" c="S" n="34429" />
<ar pt="2110141147" pp="1" l="4"
ppth="Bennem&#252;hlen|Mellendorf|Bissendorf|Langenhagen-Kaltenweide|Langenhagen Pferdemarkt|Langenhagen Mitte|Hannover-Vinnhorst|Hannover-Ledeburg|Hannover-Nordstadt" />
</s>
<s id="1493980582581603875-2110140915-17">
<tl f="S" t="p" o="800244" c="S" n="34544" />
<ar pt="2110141103" pp="2" l="5"
ppth="Paderborn Hbf|Altenbeken|Steinheim(Westf)|Schieder|L&#252;gde|Bad Pyrmont|Emmerthal|Hameln|Bad M&#252;nder(Deister)|Springe|V&#246;lksen/Eldagsen|Bennigsen|Holtensen/Linderte|Weetzen|Hannover-Linden/Fischerhof|Hannover Bismarckstr." />
<dp pt="2110141105" pp="2" l="5"
ppth="Hannover-Nordstadt|Hannover-Ledeburg|Hannover-Vinnhorst|Langenhagen Mitte|Langenhagen Pferdemarkt|Hannover Flughafen" />
</s>
<s id="-5967072892888193768-2110141104-16">
<tl f="S" t="p" o="800244" c="S" n="34216" />
<ar pt="2110141155" pp="2" l="2"
ppth="Haste|Bad Nenndorf|Bantorf|Winninghausen|Barsinghausen|Kirchdorf(Deister)|Egestorf(Deister)|Wennigsen(Deister)|Lemmie|Weetzen|Ronnenberg|Empelde|Hannover-Bornum|Hannover-Linden/Fischerhof|Hannover Bismarckstr." />
<dp pt="2110141158" pp="2" l="2"
ppth="Hannover-Nordstadt|Hannover-Leinhausen|Letter|Seelze|Dedensen-G&#252;mmer|Wunstorf|Poggenhagen|Neustadt am R&#252;benberge|Eilvese|Hagen(Han)|Linsburg|Nienburg(Weser)" />
</s>
<s id="-2642206968755664688-2110141012-13">
<tl f="S" t="p" o="800244" c="S" n="34213" />
<ar pt="2110141100" pp="1" l="2"
ppth="Nienburg(Weser)|Linsburg|Hagen(Han)|Eilvese|Neustadt am R&#252;benberge|Poggenhagen|Wunstorf|Dedensen-G&#252;mmer|Seelze|Letter|Hannover-Leinhausen|Hannover-Nordstadt" />
<dp pt="2110141103" pp="1" l="2"
ppth="Hannover Bismarckstr.|Hannover-Linden/Fischerhof|Hannover-Bornum|Empelde|Ronnenberg|Weetzen|Lemmie|Wennigsen(Deister)|Egestorf(Deister)|Kirchdorf(Deister)|Barsinghausen|Winninghausen|Bantorf|Bad Nenndorf|Haste" />
</s>
<s id="-3319510478025615006-2110140814-9">
<tl f="F" t="p" o="80" c="ICE" n="545" />
<ar pt="2110141132" pp="9"
ppth="D&#252;sseldorf Hbf|D&#252;sseldorf Flughafen|Duisburg Hbf|Essen Hbf|Bochum Hbf|Dortmund Hbf|Hamm(Westf)Hbf|Bielefeld Hbf" />
<dp pt="2110141135" pp="9" ppth="Berlin-Spandau|Berlin Hbf|Berlin Ostbahnhof" />
</s>
<s id="-9206541391658417235-2110141014-11">
<tl t="p" o="R0" c="ENO" n="83510" />
<ar pt="2110141111" pp="13" l="RE30"
ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke|Lehrte" />
</s>
<s id="-8400241610239571421-2110141140-1">
<tl f="D" t="p" o="R1" c="ME" n="82872" />
<dp pt="2110141140" pp="4" l="RE3" pde="Hamburg Hbf"
ppth="Langenhagen Mitte|Isernhagen|Gro&#223;burgwedel|Celle|Eschede|Unterl&#252;&#223;|Suderburg|Uelzen" />
</s>
<s id="-4563769839646635922-2110141043-10">
<tl f="S" t="p" o="800244" c="S" n="34312" />
<ar pt="2110141124" pp="13" l="3"
ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde|Lehrte|Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld" />
</s>
<s id="1962082432295475643-2110140948-9">
<tl f="D" t="p" o="X2" c="erx" n="83462" />
<ar pt="2110141110" pp="8" l="RE10"
ppth="Bad Harzburg|Goslar|Salzgitter-Ringelheim|Baddeckenstedt|Derneburg(Han)|Hildesheim Ost|Hildesheim Hbf|Sarstedt" />
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="2480917541885150474-2108160716-1">
<tl t="p" o="R0" c="ENO" n="83505"/>
<dp pt="2108160716" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="8503425137324890080-2108160706-5">
<tl f="S" t="p" o="800244" c="S" n="99026"/>
<ar pt="2108160729" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108160734" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="-1963042948543761461-2108160755-1">
<tl f="S" t="p" o="800244" c="S" n="99105"/>
<dp pt="2108160755" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-3028574470713369195-2108160711-6">
<tl f="S" t="p" o="800244" c="S" n="99027"/>
<ar pt="2108160734" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108160738" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="4315586671604650438-2108160647-6">
<tl f="S" t="p" o="800244" c="S" n="99104"/>
<ar pt="2108160710" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-6154367079560405690-2108160517-14">
<tl f="D" t="p" o="W3" c="WFB" n="95773"/>
<ar pt="2108160715" pp="14" l="RE60" ppth="Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108160718" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="7538934640418692029-2108160712-6">
<tl f="D" t="p" o="W3" c="WFB" n="95824"/>
<ar pt="2108160744" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108160747" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="4034387657741012323-2108160658-10">
<tl t="p" o="R0" c="ENO" n="83504"/>
<ar pt="2108160740" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="543425183571017441-2108160812-6">
<tl f="D" t="p" o="W3" c="WFB" n="95774"/>
<ar pt="2108160844" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108160847" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
<s id="7614604050911547623-2108160758-10">
<tl t="p" o="R0" c="ENO" n="83506"/>
<ar pt="2108160840" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="4020683816646788743-2108160816-1">
<tl t="p" o="R0" c="ENO" n="83507"/>
<dp pt="2108160816" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="8061249370456134776-2108160811-6">
<tl f="S" t="p" o="800244" c="S" n="99029"/>
<ar pt="2108160834" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108160838" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-1008963402532713716-2108160806-5">
<tl f="S" t="p" o="800244" c="S" n="99028"/>
<ar pt="2108160829" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108160834" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="8552544228981452549-2108160855-1">
<tl f="S" t="p" o="800244" c="S" n="99107"/>
<dp pt="2108160855" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-118775371347779303-2108160624-12">
<tl f="D" t="p" o="W3" c="WFB" n="95823"/>
<ar pt="2108160815" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108160818" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-8547167232507075495-2108160747-6">
<tl f="S" t="p" o="800244" c="S" n="99106"/>
<ar pt="2108160810" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-8364795265993682073-2108160911-6">
<tl f="S" t="p" o="800244" c="S" n="99031"/>
<ar pt="2108160934" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108160938" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-2949440726131702047-2108160858-10">
<tl t="p" o="R0" c="ENO" n="83508"/>
<ar pt="2108160940" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="1763676552526687479-2108160847-6">
<tl f="S" t="p" o="800244" c="S" n="99108"/>
<ar pt="2108160910" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-94442819435724762-2108160916-1">
<tl t="p" o="R0" c="ENO" n="83509"/>
<dp pt="2108160916" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-5296516961807204721-2108160906-5">
<tl f="S" t="p" o="800244" c="S" n="99030"/>
<ar pt="2108160929" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108160934" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="-5076826593111136189-2108160638-20">
<tl f="D" t="p" o="W3" c="WFB" n="95775"/>
<ar pt="2108160915" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108160918" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="8681599812964340829-2108160955-1">
<tl f="S" t="p" o="800244" c="S" n="99109"/>
<dp pt="2108160955" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-735649762452915464-2108160912-6">
<tl f="D" t="p" o="W3" c="WFB" n="95826"/>
<ar pt="2108160944" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108160947" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-2287077818259696863-2108160947-6">
<tl f="S" t="p" o="800244" c="S" n="99110"/>
<ar pt="2108161010" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-7727555432663336240-2108161011-6">
<tl f="S" t="p" o="800244" c="S" n="99033"/>
<ar pt="2108161034" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161038" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-9206541391658417235-2108160958-10">
<tl t="p" o="R0" c="ENO" n="83510"/>
<ar pt="2108161040" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-1174241606353260853-2108161016-1">
<tl t="p" o="R0" c="ENO" n="83511"/>
<dp pt="2108161016" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-7324985626915628890-2108160824-12">
<tl f="D" t="p" o="W3" c="WFB" n="95825"/>
<ar pt="2108161015" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161018" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="244929573019057867-2108161012-6">
<tl f="D" t="p" o="W3" c="WFB" n="95776"/>
<ar pt="2108161044" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161047" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
<s id="-3376513334056532423-2108161055-1">
<tl f="S" t="p" o="800244" c="S" n="99111"/>
<dp pt="2108161055" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-8744470103815834345-2108161006-5">
<tl f="S" t="p" o="800244" c="S" n="99032"/>
<ar pt="2108161029" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161034" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="7006118749138032199-2108160838-20">
<tl f="D" t="p" o="W3" c="WFB" n="95777"/>
<ar pt="2108161115" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161118" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-3599955674038290960-2108161047-6">
<tl f="S" t="p" o="800244" c="S" n="99112"/>
<ar pt="2108161110" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-6419712430263017490-2108161058-10">
<tl t="p" o="R0" c="ENO" n="83512"/>
<ar pt="2108161140" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-3222259045572671319-2108161155-1">
<tl f="S" t="p" o="800244" c="S" n="99113"/>
<dp pt="2108161155" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="5972407473007093240-2108161111-6">
<tl f="S" t="p" o="800244" c="S" n="99035"/>
<ar pt="2108161134" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161138" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="6533014172122516634-2108161112-6">
<tl f="D" t="p" o="W3" c="WFB" n="95828"/>
<ar pt="2108161144" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161147" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="4782237501505495785-2108161116-1">
<tl t="p" o="R0" c="ENO" n="83513"/>
<dp pt="2108161116" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="2158701616591342630-2108161106-5">
<tl f="S" t="p" o="800244" c="S" n="99034"/>
<ar pt="2108161129" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161134" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-2379689238391834001-2108161023-12">
<tl f="D" t="p" o="W3" c="WFB" n="95827"/>
<ar pt="2108161215" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161218" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-1121324065844325978-2108161212-6">
<tl f="D" t="p" o="W3" c="WFB" n="95778"/>
<ar pt="2108161244" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161247" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
<s id="7397997706155921797-2108161211-6">
<tl f="S" t="p" o="800244" c="S" n="99037"/>
<ar pt="2108161234" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161238" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="4131928373987407668-2108161158-10">
<tl t="p" o="R0" c="ENO" n="83514"/>
<ar pt="2108161240" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-942062361217487173-2108161216-1">
<tl t="p" o="R0" c="ENO" n="83515"/>
<dp pt="2108161216" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-6974622041562609560-2108161206-5">
<tl f="S" t="p" o="800244" c="S" n="99036"/>
<ar pt="2108161229" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161234" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="6022904892453359304-2108161147-6">
<tl f="S" t="p" o="800244" c="S" n="99114"/>
<ar pt="2108161210" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="3669677165128171197-2108161255-1">
<tl f="S" t="p" o="800244" c="S" n="99115"/>
<dp pt="2108161255" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="1839271107440507858-2108161258-10">
<tl t="p" o="R0" c="ENO" n="83516"/>
<ar pt="2108161340" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="6363484950342149752-2108161316-1">
<tl t="p" o="R0" c="ENO" n="83517"/>
<dp pt="2108161316" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="6972591298753273939-2108161306-5">
<tl f="S" t="p" o="800244" c="S" n="99038"/>
<ar pt="2108161329" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161334" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="8991753824982033162-2108161355-1">
<tl f="S" t="p" o="800244" c="S" n="99117"/>
<dp pt="2108161355" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-1273850417038046266-2108161312-6">
<tl f="D" t="p" o="W3" c="WFB" n="95830"/>
<ar pt="2108161344" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161347" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="8517329334682501948-2108161247-6">
<tl f="S" t="p" o="800244" c="S" n="99116"/>
<ar pt="2108161310" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-1057899669668012215-2108161311-6">
<tl f="S" t="p" o="800244" c="S" n="99039"/>
<ar pt="2108161334" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161338" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="1425903371853713152-2108161038-20">
<tl f="D" t="p" o="W3" c="WFB" n="95779"/>
<ar pt="2108161315" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161318" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="4611955095092960007-2108161224-12">
<tl f="D" t="p" o="W3" c="WFB" n="95829"/>
<ar pt="2108161415" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161418" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-3135014020714797396-2108161416-1">
<tl t="p" o="R0" c="ENO" n="83519"/>
<dp pt="2108161416" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-3293102965109218731-2108161358-10">
<tl t="p" o="R0" c="ENO" n="83518"/>
<ar pt="2108161440" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="2878791902132849173-2108161406-5">
<tl f="S" t="p" o="800244" c="S" n="99040"/>
<ar pt="2108161429" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161434" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="-2488337281148762091-2108161411-6">
<tl f="S" t="p" o="800244" c="S" n="99041"/>
<ar pt="2108161434" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161438" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-9040190761468977527-2108161455-1">
<tl f="S" t="p" o="800244" c="S" n="99119"/>
<dp pt="2108161455" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="2321786846951144736-2108161347-6">
<tl f="S" t="p" o="800244" c="S" n="99118"/>
<ar pt="2108161410" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="5842541839645946958-2108161412-6">
<tl f="D" t="p" o="W3" c="WFB" n="95780"/>
<ar pt="2108161443" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161447" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="1767689193739809195-2108161511-6">
<tl f="S" t="p" o="800244" c="S" n="99043"/>
<ar pt="2108161534" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161538" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-3802907293883913443-2108161447-6">
<tl f="S" t="p" o="800244" c="S" n="99120"/>
<ar pt="2108161510" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-4683929219734901736-2108161506-5">
<tl f="S" t="p" o="800244" c="S" n="99042"/>
<ar pt="2108161529" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161534" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="4988481656268886235-2108161238-20">
<tl f="D" t="p" o="W3" c="WFB" n="95781"/>
<ar pt="2108161515" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161518" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="4964097043140910843-2108161516-1">
<tl t="p" o="R0" c="ENO" n="83521"/>
<dp pt="2108161516" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-2721954907811785474-2108161555-1">
<tl f="S" t="p" o="800244" c="S" n="99121"/>
<dp pt="2108161555" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-6815601404651114984-2108161512-6">
<tl f="D" t="p" o="W3" c="WFB" n="95832"/>
<ar pt="2108161544" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161547" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="-1200910621123205769-2108161458-10">
<tl t="p" o="R0" c="ENO" n="83520"/>
<ar pt="2108161540" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="8375696493341006692-2108161612-6">
<tl f="D" t="p" o="W3" c="WFB" n="95782"/>
<ar pt="2108161644" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161647" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
<s id="-6844033562588306961-2108161611-6">
<tl f="S" t="p" o="800244" c="S" n="99045"/>
<ar pt="2108161634" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161638" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-8550636124222837939-2108161547-6">
<tl f="S" t="p" o="800244" c="S" n="99122"/>
<ar pt="2108161610" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="3563379797796138412-2108161424-12">
<tl f="D" t="p" o="W3" c="WFB" n="95831"/>
<ar pt="2108161615" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161618" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-1046209332688096315-2108161655-1">
<tl f="S" t="p" o="800244" c="S" n="99123"/>
<dp pt="2108161655" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-2054302951360180473-2108161606-5">
<tl f="S" t="p" o="800244" c="S" n="99044"/>
<ar pt="2108161629" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161634" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="3894388062931543290-2108161616-1">
<tl t="p" o="R0" c="ENO" n="83523"/>
<dp pt="2108161616" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-4732092021760003985-2108161558-10">
<tl t="p" o="R0" c="ENO" n="83522"/>
<ar pt="2108161640" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="9112265633718338636-2108161755-1">
<tl f="S" t="p" o="800244" c="S" n="99125"/>
<dp pt="2108161755" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-1457168968992517383-2108161658-10">
<tl t="p" o="R0" c="ENO" n="83524"/>
<ar pt="2108161740" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-3340822240933892496-2108161706-5">
<tl f="S" t="p" o="800244" c="S" n="99046"/>
<ar pt="2108161729" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161734" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="-6036386918811435216-2108161438-20">
<tl f="D" t="p" o="W3" c="WFB" n="95783"/>
<ar pt="2108161715" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161718" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-7424629400664744183-2108161647-6">
<tl f="S" t="p" o="800244" c="S" n="99124"/>
<ar pt="2108161710" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="4263401729039526569-2108161712-6">
<tl f="D" t="p" o="W3" c="WFB" n="95834"/>
<ar pt="2108161744" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161747" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="4641247587225026942-2108161716-1">
<tl t="p" o="R0" c="ENO" n="83525"/>
<dp pt="2108161716" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="4353911451161478332-2108161711-6">
<tl f="S" t="p" o="800244" c="S" n="99047"/>
<ar pt="2108161734" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161738" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-4161432970801748050-2108161816-1">
<tl t="p" o="R0" c="ENO" n="83527"/>
<dp pt="2108161816" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="3595734209843238192-2108161806-5">
<tl f="S" t="p" o="800244" c="S" n="99048"/>
<ar pt="2108161829" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161834" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="2981307361181102496-2108161811-6">
<tl f="S" t="p" o="800244" c="S" n="99049"/>
<ar pt="2108161834" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161838" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="7606526371432167075-2108161758-10">
<tl t="p" o="R0" c="ENO" n="83526"/>
<ar pt="2108161840" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="3158263065549666682-2108161747-6">
<tl f="S" t="p" o="800244" c="S" n="99126"/>
<ar pt="2108161810" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-7429012022861016920-2108161855-1">
<tl f="S" t="p" o="800244" c="S" n="99127"/>
<dp pt="2108161855" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="502654172913099951-2108161812-6">
<tl f="D" t="p" o="W3" c="WFB" n="95784"/>
<ar pt="2108161844" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161847" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
<s id="2683252994559519477-2108161624-12">
<tl f="D" t="p" o="W3" c="WFB" n="95833"/>
<ar pt="2108161815" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161818" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="7793181434569715846-2108161858-10">
<tl t="p" o="R0" c="ENO" n="83528"/>
<ar pt="2108161940" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-8155319653599274990-2108161916-1">
<tl t="p" o="R0" c="ENO" n="83529"/>
<dp pt="2108161916" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="4983472827407508037-2108161955-1">
<tl f="S" t="p" o="800244" c="S" n="99129"/>
<dp pt="2108161955" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="3639552795299979246-2108161638-20">
<tl f="D" t="p" o="W3" c="WFB" n="95785"/>
<ar pt="2108161915" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108161918" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="2332778806583235977-2108161847-6">
<tl f="S" t="p" o="800244" c="S" n="99128"/>
<ar pt="2108161910" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="1148856555558896584-2108161912-6">
<tl f="D" t="p" o="W3" c="WFB" n="95836"/>
<ar pt="2108161944" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108161947" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="-5532477049445256271-2108161906-5">
<tl f="S" t="p" o="800244" c="S" n="99050"/>
<ar pt="2108161929" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108161934" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="4273450929425816477-2108161911-6">
<tl f="S" t="p" o="800244" c="S" n="99051"/>
<ar pt="2108161934" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108161938" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="5006636560290563224-2108162011-6">
<tl f="S" t="p" o="800244" c="S" n="99053"/>
<ar pt="2108162034" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108162038" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="4036630279678935031-2108162055-1">
<tl f="S" t="p" o="800244" c="S" n="99131"/>
<dp pt="2108162055" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="3382247501636548579-2108161958-10">
<tl t="p" o="R0" c="ENO" n="83530"/>
<ar pt="2108162040" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-6990129281608112740-2108161824-12">
<tl f="D" t="p" o="W3" c="WFB" n="95835"/>
<ar pt="2108162015" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108162018" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-4937005316818951728-2108162016-1">
<tl t="p" o="R0" c="ENO" n="83531"/>
<dp pt="2108162016" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="7845084439898423766-2108162006-5">
<tl f="S" t="p" o="800244" c="S" n="99052"/>
<ar pt="2108162029" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108162034" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="601208318005647306-2108162012-6">
<tl f="D" t="p" o="W3" c="WFB" n="95786"/>
<ar pt="2108162044" pp="14" l="RE60" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108162047" pp="14" l="RE60" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf|Osnabr&#252;ck Altstadt|Ibbenb&#252;ren-Laggenbeck|Ibbenb&#252;ren|Ibbenb&#252;ren-Esch|H&#246;rstel|Rheine"/>
</s>
<s id="-5299438316210197432-2108161947-6">
<tl f="S" t="p" o="800244" c="S" n="99130"/>
<ar pt="2108162010" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-6365543450329300214-2108162058-10">
<tl t="p" o="R0" c="ENO" n="83532"/>
<ar pt="2108162140" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-876254985661224810-2108162112-6">
<tl f="D" t="p" o="W3" c="WFB" n="95838"/>
<ar pt="2108162144" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108162147" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Herford|Bielefeld Hbf"/>
</s>
<s id="1424956062307577167-2108162047-6">
<tl f="S" t="p" o="800244" c="S" n="99132"/>
<ar pt="2108162110" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-7229853211489462088-2108162155-1">
<tl f="S" t="p" o="800244" c="S" n="99133"/>
<dp pt="2108162155" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-1262495492364677508-2108162111-6">
<tl f="S" t="p" o="800244" c="S" n="99055"/>
<ar pt="2108162134" pp="14" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108162138" pp="14" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-2331021216215087320-2108162116-1">
<tl t="p" o="R0" c="ENO" n="83533"/>
<dp pt="2108162116" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-30458462377562293-2108162106-5">
<tl f="S" t="p" o="800244" c="S" n="99054"/>
<ar pt="2108162129" pp="13" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108162134" pp="13" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
<s id="7478530496367099178-2108161838-20">
<tl f="D" t="p" o="W3" c="WFB" n="95787"/>
<ar pt="2108162115" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108162118" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="4697330016557121579-2108162255-1">
<tl f="S" t="p" o="800244" c="S" n="99135"/>
<dp pt="2108162255" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="3132795657593053084-2108162147-6">
<tl f="S" t="p" o="800244" c="S" n="99134"/>
<ar pt="2108162210" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="4622464961095952906-2108162216-1">
<tl t="p" o="R0" c="ENO" n="83535"/>
<dp pt="2108162216" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="4456518626465571419-2108162212-6">
<tl f="D" t="p" o="W3" c="WFB" n="95788"/>
<ar pt="2108162244" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108162247" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)|Porta Westfalica|Bad Oeynhausen|L&#246;hne(Westf)|Kirchlengern|B&#252;nde(Westf)|Melle|Osnabr&#252;ck Hbf"/>
</s>
<s id="-6486755152464856584-2108162211-6">
<tl f="S" t="p" o="800244" c="S" n="99057"/>
<ar pt="2108162234" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108162238" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="-8624610893606997940-2108162024-12">
<tl f="D" t="p" o="W3" c="WFB" n="95837"/>
<ar pt="2108162215" pp="14" l="RE70" ppth="Bielefeld Hbf|Herford|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108162218" pp="14" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-4895514507913038239-2108162158-10">
<tl t="p" o="R0" c="ENO" n="83534"/>
<ar pt="2108162240" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="-7255513921487534258-2108162206-5">
<tl f="S" t="p" o="800244" c="S" n="99056"/>
<ar pt="2108162229" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108162234" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
</timetable>

View File

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-2338143139486100041-2108162316-1">
<tl t="p" o="R0" c="ENO" n="83537"/>
<dp pt="2108162316" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="225524803682367536-2108162355-1">
<tl f="S" t="p" o="800244" c="S" n="99137"/>
<dp pt="2108162355" pp="13" l="3" ppth="Ahlten|Hannover Anderten-Misburg|Hannover Karl-Wiechert-Allee|Hannover-Kleefeld|Hannover Hbf"/>
</s>
<s id="-2108753935575260460-2108162247-6">
<tl f="S" t="p" o="800244" c="S" n="99136"/>
<ar pt="2108162310" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="-3319200239513168009-2108162311-6">
<tl f="S" t="p" o="800244" c="S" n="99059"/>
<ar pt="2108162334" pp="13" l="3" ppth="Celle|Ehlershausen|Otze|Burgdorf|Aligse"/>
<dp pt="2108162338" pp="13" l="3" ppth="Sehnde|Algermissen|Harsum|Hildesheim Hbf"/>
</s>
<s id="2998651517533835490-2108162038-20">
<tl f="D" t="p" o="W3" c="WFB" n="95789"/>
<ar pt="2108162315" pp="14" l="RE60" ppth="Rheine|H&#246;rstel|Ibbenb&#252;ren-Esch|Ibbenb&#252;ren|Ibbenb&#252;ren-Laggenbeck|Osnabr&#252;ck Altstadt|Osnabr&#252;ck Hbf|Melle|B&#252;nde(Westf)|Kirchlengern|L&#246;hne(Westf)|Bad Oeynhausen|Porta Westfalica|Minden(Westf)|B&#252;ckeburg|Stadthagen|Haste|Wunstorf|Hannover Hbf"/>
<dp pt="2108162318" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="2495224407053131128-2108162258-10">
<tl t="p" o="R0" c="ENO" n="83536"/>
<ar pt="2108162340" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="4731596700926155152-2108162312-6">
<tl f="D" t="p" o="W3" c="WFB" n="95790"/>
<ar pt="2108162344" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108162347" pp="14" l="RE70" ppth="Hannover Hbf|Wunstorf|Haste|Stadthagen|B&#252;ckeburg|Minden(Westf)"/>
</s>
<s id="-451319054335837606-2108162306-5">
<tl f="S" t="p" o="800244" c="S" n="99058"/>
<ar pt="2108162329" pp="14" l="3" ppth="Hildesheim Hbf|Harsum|Algermissen|Sehnde"/>
<dp pt="2108162334" pp="14" l="3" ppth="Aligse|Burgdorf|Otze|Ehlershausen|Celle"/>
</s>
</timetable>

View File

@ -0,0 +1,25 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-2534056427099012540-2108170012-6">
<tl f="D" t="p" o="W3" c="WFB" n="95792"/>
<ar pt="2108170044" pp="14" l="RE70" ppth="Braunschweig Hbf|Vechelde|Peine|V&#246;hrum|H&#228;melerwald"/>
<dp pt="2108170047" pp="14" l="RE70" ppth="Hannover Hbf"/>
</s>
<s id="8693031459340504773-2108162347-6">
<tl f="S" t="p" o="800244" c="S" n="99138"/>
<ar pt="2108170010" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="5845049446990543970-2108170002-10">
<tl t="p" o="R0" c="ENO" n="83538"/>
<ar pt="2108170043" pp="3" l="RE30" ppth="Wolfsburg Hbf|Fallersleben|Calberlah|Gifhorn|Leiferde(b Gifhorn)|Meinersen|Dedenhausen|Dollbergen|Immensen-Arpke"/>
</s>
<s id="1872572738060974975-2108170016-1">
<tl t="p" o="R0" c="ENO" n="83539"/>
<dp pt="2108170016" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
<s id="-430306391697164748-2108170024-2">
<tl f="D" t="p" o="W3" c="WFB" n="95793"/>
<ar pt="2108170044" pp="13" l="RE70" ppth="Hannover Hbf"/>
<dp pt="2108170045" pp="13" l="RE70" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable station='Lehrte'>
<s id="-1781439465744467581-2108170124-2">
<tl f="D" t="p" o="W3" c="WFB" n="95769"/>
<ar pt="2108170144" pp="14" l="RE60" ppth="Hannover Hbf"/>
<dp pt="2108170145" pp="14" l="RE60" ppth="H&#228;melerwald|V&#246;hrum|Peine|Vechelde|Braunschweig Hbf"/>
</s>
<s id="-5193176993424262202-2108170047-6">
<tl f="S" t="p" o="800244" c="S" n="99098"/>
<ar pt="2108170110" pp="13" l="7" ppth="Hannover Hbf|Hannover-Kleefeld|Hannover Karl-Wiechert-Allee|Hannover Anderten-Misburg|Ahlten"/>
</s>
<s id="5996529048494327209-2108170116-1">
<tl t="p" o="R0" c="ENO" n="83541"/>
<dp pt="2108170116" pp="3" l="RE30" ppth="Immensen-Arpke|Dollbergen|Dedenhausen|Meinersen|Leiferde(b Gifhorn)|Gifhorn|Calberlah|Fallersleben|Wolfsburg Hbf"/>
</s>
</timetable>

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable/>

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<timetable/>

View File

@ -96,6 +96,7 @@
<module>org.openhab.binding.dbquery</module> <module>org.openhab.binding.dbquery</module>
<module>org.openhab.binding.deconz</module> <module>org.openhab.binding.deconz</module>
<module>org.openhab.binding.denonmarantz</module> <module>org.openhab.binding.denonmarantz</module>
<module>org.openhab.binding.deutschebahn</module>
<module>org.openhab.binding.digiplex</module> <module>org.openhab.binding.digiplex</module>
<module>org.openhab.binding.digitalstrom</module> <module>org.openhab.binding.digitalstrom</module>
<module>org.openhab.binding.dlinksmarthome</module> <module>org.openhab.binding.dlinksmarthome</module>