[publictransportswitzerland] Public Transport Switzerland Initial contribution (#8540)
* [publictransportswitzerland] Initital commit Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add json stationboard Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add csv stationboard Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Encoding / Data usage Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update readme Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update binding.xml Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove code owner Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add more detailed thing status Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Make field filters a constant Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove json channel Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Stop scheduler on config error Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Work on i18n Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove TODO Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Re-order members Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update label Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Set status to unknown instead of initializing Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update version Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Log api response Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Mark CSV advanced Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add error message Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update readme Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Move members Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add dynamic channels Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Implement dispose Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove unnecessary translation Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Rename csv -> tsv Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update readme Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update train names Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Fix markdown table Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Use UNDEF Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Allow departures without platform Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Work on display logic Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Use null Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Make style checks happy Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Implement refresh command Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Avoid hitting API limits Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Use expiring cache Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update version Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Increase channel update interval Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Use lower cache expiration time Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Avoid glob import Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Make compiler happier Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add error explanation Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Skip check Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Print message instead of stacktrace Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Store date format as final field Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Cache configuration Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Work on exception handling Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update readme Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Make style checks happy Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Bump version Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Work on compiler warnings Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Add more detailed introduction Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Move tsv into group Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Bump version Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update thing description Co-authored-by: Fabian Wolter <github@fabian-wolter.de> Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update thing description Co-authored-by: Fabian Wolter <github@fabian-wolter.de> Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove explicit channel creation Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Run mvn spotless:apply Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Migrate to OH3 Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Work on OH3 migration Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Bump version Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Define channel-type for departures Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update copyright notice Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Move channel types below thing types Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove ignored files Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Remove author tag Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update feature description Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Make linter happy Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * [publictransportswitzerland] Update README Signed-off-by: Jeremy Stucki <dev@jeremystucki.ch> * Bump version Signed-off-by: Fabian Wolter <github@fabian-wolter.de> Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
parent
d38f37fa1a
commit
1cae863569
@ -245,6 +245,7 @@
|
|||||||
/bundles/org.openhab.binding.plugwiseha/ @lsiepel
|
/bundles/org.openhab.binding.plugwiseha/ @lsiepel
|
||||||
/bundles/org.openhab.binding.powermax/ @lolodomo
|
/bundles/org.openhab.binding.powermax/ @lolodomo
|
||||||
/bundles/org.openhab.binding.proteusecometer/ @2chilled
|
/bundles/org.openhab.binding.proteusecometer/ @2chilled
|
||||||
|
/bundles/org.openhab.binding.publictransportswitzerland/ @jeremystucki
|
||||||
/bundles/org.openhab.binding.pulseaudio/ @peuter
|
/bundles/org.openhab.binding.pulseaudio/ @peuter
|
||||||
/bundles/org.openhab.binding.pushbullet/ @hakan42
|
/bundles/org.openhab.binding.pushbullet/ @hakan42
|
||||||
/bundles/org.openhab.binding.pushover/ @cweitkamp
|
/bundles/org.openhab.binding.pushover/ @cweitkamp
|
||||||
|
|||||||
@ -1216,6 +1216,11 @@
|
|||||||
<artifactId>org.openhab.binding.proteusecometer</artifactId>
|
<artifactId>org.openhab.binding.proteusecometer</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.publictransportswitzerland</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.pulseaudio</artifactId>
|
<artifactId>org.openhab.binding.pulseaudio</artifactId>
|
||||||
|
|||||||
@ -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
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
# Public Transport Switzerland Binding
|
||||||
|
|
||||||
|
Connects to the "Swiss public transport API" to provide real-time public transport information. [Link to the API](https://transport.opendata.ch/)
|
||||||
|
|
||||||
|
For example, here is a station board in HABPanel. (Download [here](https://github.com/StefanieJaeger/HABPanel-departure-board))
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Supported Things
|
||||||
|
|
||||||
|
### Stationboard
|
||||||
|
|
||||||
|
Upcoming departures for a single station. This is what you would usually see displayed at the train station.
|
||||||
|
|
||||||
|
#### Channels
|
||||||
|
|
||||||
|
| channel | type | description |
|
||||||
|
|----------------|--------|----------------------------------------------------------------------------------------------|
|
||||||
|
| departures#n | String | A dynamic channel for each upcoming departure |
|
||||||
|
| tsv (advanced) | String | A tsv which contains the fields:<br />`identifier, departureTime, destination, track, delay` |
|
||||||
|
|
||||||
|
#### UI based Configuration
|
||||||
|
|
||||||
|
`station` is the station name for which to display departures.
|
||||||
|
The name has to be one that is used by the swiss federal railways.
|
||||||
|
Please consult their [website](https://sbb.ch/en).
|
||||||
|
|
||||||
|
#### Textual configuration
|
||||||
|
|
||||||
|
##### Thing
|
||||||
|
```
|
||||||
|
Thing publictransportswitzerland:stationboard:zurich [ station="Zürich HB" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Items
|
||||||
|
```
|
||||||
|
String Next_Departure "Next Departure" { channel="publictransportswitzerland:stationboard:zurich:departures#1" }
|
||||||
|
String Upcoming_Departures_TSV "Upcoming_Departures_TSV" { channel="publictransportswitzerland:stationboard:zurich:tsv" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
This binding does not support auto-discovery.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
@ -0,0 +1,17 @@
|
|||||||
|
<?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.3.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>org.openhab.binding.publictransportswitzerland</artifactId>
|
||||||
|
|
||||||
|
<name>openHAB Add-ons :: Bundles :: PublicTransportSwitzerland Binding</name>
|
||||||
|
|
||||||
|
</project>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<features name="org.openhab.binding.publictransportswitzerland-${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-publictransportswitzerland" description="Public Transport Switzerland Binding" version="${project.version}">
|
||||||
|
<feature>openhab-runtime-base</feature>
|
||||||
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.publictransportswitzerland/${project.version}</bundle>
|
||||||
|
</feature>
|
||||||
|
</features>
|
||||||
@ -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.publictransportswitzerland.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PublicTransportSwitzerlandBindingConstants} class defines common constants, which are
|
||||||
|
* used across the whole binding.
|
||||||
|
*
|
||||||
|
* @author Jeremy Stucki - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class PublicTransportSwitzerlandBindingConstants {
|
||||||
|
|
||||||
|
private static final String BINDING_ID = "publictransportswitzerland";
|
||||||
|
|
||||||
|
// List of all Thing Type UIDs
|
||||||
|
public static final ThingTypeUID THING_TYPE_STATIONBOARD = new ThingTypeUID(BINDING_ID, "stationboard");
|
||||||
|
|
||||||
|
public static final String BASE_URL = "https://transport.opendata.ch/v1/";
|
||||||
|
}
|
||||||
@ -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.publictransportswitzerland.internal;
|
||||||
|
|
||||||
|
import static org.openhab.binding.publictransportswitzerland.internal.PublicTransportSwitzerlandBindingConstants.*;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.publictransportswitzerland.internal.stationboard.PublicTransportSwitzerlandStationboardHandler;
|
||||||
|
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 PublicTransportSwitzerlandHandlerFactory} is responsible for creating things and thing
|
||||||
|
* handlers.
|
||||||
|
*
|
||||||
|
* @author Jeremy Stucki - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
@Component(configurationPid = "binding.publictransportswitzerland", service = ThingHandlerFactory.class)
|
||||||
|
public class PublicTransportSwitzerlandHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
|
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_STATIONBOARD);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||||
|
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||||
|
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||||
|
|
||||||
|
if (THING_TYPE_STATIONBOARD.equals(thingTypeUID)) {
|
||||||
|
return new PublicTransportSwitzerlandStationboardHandler(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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.publictransportswitzerland.internal.stationboard;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PublicTransportSwitzerlandStationboardConfiguration} class contains fields mapping thing configuration
|
||||||
|
* parameters.
|
||||||
|
*
|
||||||
|
* @author Jeremy Stucki - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class PublicTransportSwitzerlandStationboardConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The station name
|
||||||
|
*/
|
||||||
|
public @Nullable String station;
|
||||||
|
}
|
||||||
@ -0,0 +1,332 @@
|
|||||||
|
/**
|
||||||
|
* 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.publictransportswitzerland.internal.stationboard;
|
||||||
|
|
||||||
|
import static org.openhab.binding.publictransportswitzerland.internal.PublicTransportSwitzerlandBindingConstants.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.cache.ExpiringCache;
|
||||||
|
import org.openhab.core.io.net.http.HttpUtil;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
import org.openhab.core.thing.Channel;
|
||||||
|
import org.openhab.core.thing.ChannelGroupUID;
|
||||||
|
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.thing.binding.builder.ChannelBuilder;
|
||||||
|
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||||
|
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.RefreshType;
|
||||||
|
import org.openhab.core.types.UnDefType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PublicTransportSwitzerlandStationboardHandler} is responsible for handling commands, which are
|
||||||
|
* sent to one of the channels.
|
||||||
|
*
|
||||||
|
* @author Jeremy Stucki - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class PublicTransportSwitzerlandStationboardHandler extends BaseThingHandler {
|
||||||
|
|
||||||
|
// Limit the API response to the necessary fields
|
||||||
|
private static final String FIELD_FILTERS = createFilterForFields("stationboard/to", "stationboard/category",
|
||||||
|
"stationboard/number", "stationboard/stop/departureTimestamp", "stationboard/stop/delay",
|
||||||
|
"stationboard/stop/platform");
|
||||||
|
|
||||||
|
private static final String TSV_CHANNEL = "tsv";
|
||||||
|
|
||||||
|
private final ChannelGroupUID dynamicChannelGroupUID = new ChannelGroupUID(getThing().getUID(), "departures");
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(PublicTransportSwitzerlandStationboardHandler.class);
|
||||||
|
|
||||||
|
private final SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm");
|
||||||
|
|
||||||
|
private @Nullable ScheduledFuture<?> updateChannelsJob;
|
||||||
|
private @Nullable ExpiringCache<@Nullable JsonElement> cache;
|
||||||
|
private @Nullable PublicTransportSwitzerlandStationboardConfiguration configuration;
|
||||||
|
|
||||||
|
public PublicTransportSwitzerlandStationboardHandler(Thing thing) {
|
||||||
|
super(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
|
if (command instanceof RefreshType) {
|
||||||
|
updateChannels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
// Together with the 10 second timeout, this should be less than a minute
|
||||||
|
cache = new ExpiringCache<>(45_000, this::updateData);
|
||||||
|
|
||||||
|
PublicTransportSwitzerlandStationboardConfiguration configuration = getConfigAs(
|
||||||
|
PublicTransportSwitzerlandStationboardConfiguration.class);
|
||||||
|
this.configuration = configuration;
|
||||||
|
|
||||||
|
String configurationError = findConfigurationError(configuration);
|
||||||
|
if (configurationError != null) {
|
||||||
|
stopChannelUpdate();
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configurationError);
|
||||||
|
} else {
|
||||||
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
|
startChannelUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
stopChannelUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
|
||||||
|
super.handleConfigurationUpdate(configurationParameters);
|
||||||
|
|
||||||
|
PublicTransportSwitzerlandStationboardConfiguration configuration = getConfigAs(
|
||||||
|
PublicTransportSwitzerlandStationboardConfiguration.class);
|
||||||
|
this.configuration = configuration;
|
||||||
|
|
||||||
|
ScheduledFuture<?> updateJob = updateChannelsJob;
|
||||||
|
|
||||||
|
String configurationError = findConfigurationError(configuration);
|
||||||
|
if (configurationError != null) {
|
||||||
|
stopChannelUpdate();
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configurationError);
|
||||||
|
} else if (updateJob == null || updateJob.isCancelled()) {
|
||||||
|
startChannelUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String findConfigurationError(PublicTransportSwitzerlandStationboardConfiguration configuration) {
|
||||||
|
String station = configuration.station;
|
||||||
|
if (station == null || station.isEmpty()) {
|
||||||
|
return "The station is not set";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startChannelUpdate() {
|
||||||
|
updateChannelsJob = scheduler.scheduleWithFixedDelay(this::updateChannels, 0, 60, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopChannelUpdate() {
|
||||||
|
ScheduledFuture<?> updateJob = updateChannelsJob;
|
||||||
|
|
||||||
|
if (updateJob != null) {
|
||||||
|
updateJob.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable JsonElement updateData() {
|
||||||
|
PublicTransportSwitzerlandStationboardConfiguration config = configuration;
|
||||||
|
if (config == null) {
|
||||||
|
logger.warn("Unable to access configuration");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String station = config.station;
|
||||||
|
if (station == null) {
|
||||||
|
logger.warn("Station is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String escapedStation = URLEncoder.encode(station, StandardCharsets.UTF_8.name());
|
||||||
|
String requestUrl = BASE_URL + "stationboard?station=" + escapedStation + FIELD_FILTERS;
|
||||||
|
|
||||||
|
String response = HttpUtil.executeUrl("GET", requestUrl, 10_000);
|
||||||
|
logger.debug("Got response from API: {}", response);
|
||||||
|
|
||||||
|
return JsonParser.parseString(response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Unable to fetch stationboard data: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createFilterForFields(String... fields) {
|
||||||
|
return Arrays.stream(fields).map((field) -> "&fields[]=" + field).collect(Collectors.joining());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChannels() {
|
||||||
|
ExpiringCache<@Nullable JsonElement> expiringCache = cache;
|
||||||
|
|
||||||
|
if (expiringCache == null) {
|
||||||
|
logger.warn("Cache is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonElement jsonObject = expiringCache.getValue();
|
||||||
|
|
||||||
|
if (jsonObject == null) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||||
|
|
||||||
|
updateState(TSV_CHANNEL, UnDefType.UNDEF);
|
||||||
|
|
||||||
|
for (Channel channel : getThing().getChannelsOfGroup(dynamicChannelGroupUID.getId())) {
|
||||||
|
updateState(channel.getUID(), UnDefType.UNDEF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
|
||||||
|
JsonArray stationboard = jsonObject.getAsJsonObject().get("stationboard").getAsJsonArray();
|
||||||
|
|
||||||
|
createDynamicChannels(stationboard.size());
|
||||||
|
setUnusedDynamicChannelsToUndef(stationboard.size());
|
||||||
|
|
||||||
|
List<String> tsvRows = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < stationboard.size(); i++) {
|
||||||
|
JsonElement jsonElement = stationboard.get(i);
|
||||||
|
|
||||||
|
JsonObject departureObject = jsonElement.getAsJsonObject();
|
||||||
|
JsonElement stopElement = departureObject.get("stop");
|
||||||
|
|
||||||
|
if (stopElement == null) {
|
||||||
|
logger.warn("Skipping stationboard item. Stop element is missing from departure object");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject stopObject = stopElement.getAsJsonObject();
|
||||||
|
|
||||||
|
JsonElement categoryElement = departureObject.get("category");
|
||||||
|
JsonElement numberElement = departureObject.get("number");
|
||||||
|
JsonElement destinationElement = departureObject.get("to");
|
||||||
|
JsonElement departureTimeElement = stopObject.get("departureTimestamp");
|
||||||
|
|
||||||
|
if (categoryElement == null || numberElement == null || destinationElement == null
|
||||||
|
|| departureTimeElement == null) {
|
||||||
|
logger.warn("Skipping stationboard item."
|
||||||
|
+ "One of the following is null: category: {}, number: {}, destination: {}, departureTime: {}",
|
||||||
|
categoryElement, numberElement, destinationElement, departureTimeElement);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String category = categoryElement.getAsString();
|
||||||
|
String number = numberElement.getAsString();
|
||||||
|
String destination = destinationElement.getAsString();
|
||||||
|
Long departureTime = departureTimeElement.getAsLong();
|
||||||
|
|
||||||
|
String identifier = createIdentifier(category, number);
|
||||||
|
|
||||||
|
String delay = getStringValueOrNull(departureObject.get("delay"));
|
||||||
|
String track = getStringValueOrNull(stopObject.get("platform"));
|
||||||
|
|
||||||
|
updateState(getChannelUIDForPosition(i),
|
||||||
|
new StringType(formatDeparture(identifier, departureTime, destination, track, delay)));
|
||||||
|
tsvRows.add(String.join("\t", identifier, departureTimeElement.toString(), destination, track, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState(TSV_CHANNEL, new StringType(String.join("\n", tsvRows)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String getStringValueOrNull(@Nullable JsonElement jsonElement) {
|
||||||
|
if (jsonElement == null || jsonElement.isJsonNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String stringValue = jsonElement.getAsString();
|
||||||
|
|
||||||
|
if (stringValue.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDeparture(String identifier, Long departureTimestamp, String destination,
|
||||||
|
@Nullable String track, @Nullable String delay) {
|
||||||
|
Date departureDate = new Date(departureTimestamp * 1000);
|
||||||
|
String formattedDate = timeFormat.format(departureDate);
|
||||||
|
|
||||||
|
String result = String.format("%s - %s %s", formattedDate, identifier, destination);
|
||||||
|
|
||||||
|
if (track != null) {
|
||||||
|
result += " - Pl. " + track;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay != null) {
|
||||||
|
result += String.format(" (%s' late)", delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createIdentifier(String category, String number) {
|
||||||
|
// Only show the number for buses
|
||||||
|
if ("B".equals(category)) {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some weird quirk with the API
|
||||||
|
if (number.startsWith(category)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
return category + number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDynamicChannels(int numberOfChannels) {
|
||||||
|
List<Channel> existingChannels = getThing().getChannelsOfGroup(dynamicChannelGroupUID.getId());
|
||||||
|
|
||||||
|
ThingBuilder thingBuilder = editThing();
|
||||||
|
|
||||||
|
for (int i = existingChannels.size(); i < numberOfChannels; i++) {
|
||||||
|
Channel channel = ChannelBuilder.create(getChannelUIDForPosition(i), "String")
|
||||||
|
.withLabel("Departure " + (i + 1))
|
||||||
|
.withType(new ChannelTypeUID("publictransportswitzerland", "departure")).build();
|
||||||
|
thingBuilder.withChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateThing(thingBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUnusedDynamicChannelsToUndef(int amountOfUsedChannels) {
|
||||||
|
getThing().getChannelsOfGroup(dynamicChannelGroupUID.getId()).stream().skip(amountOfUsedChannels)
|
||||||
|
.forEach(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelUID getChannelUIDForPosition(int position) {
|
||||||
|
return new ChannelUID(dynamicChannelGroupUID, String.valueOf(position + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<binding:binding id="publictransportswitzerland" 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>Public Transport Switzerland Binding</name>
|
||||||
|
<description>Connects to the "Swiss public transport API" to provide real-time public transport information.</description>
|
||||||
|
|
||||||
|
</binding:binding>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="publictransportswitzerland"
|
||||||
|
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">
|
||||||
|
|
||||||
|
<thing-type id="stationboard">
|
||||||
|
<label>Stationboard</label>
|
||||||
|
<description>Upcoming departures for a single station.</description>
|
||||||
|
<channels>
|
||||||
|
<channel typeId="tsv" id="tsv"/>
|
||||||
|
</channels>
|
||||||
|
<config-description>
|
||||||
|
<parameter name="station" type="text" required="true">
|
||||||
|
<label>Station</label>
|
||||||
|
<description>The name of the station</description>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</thing-type>
|
||||||
|
|
||||||
|
<channel-type id="tsv" advanced="true">
|
||||||
|
<item-type>String</item-type>
|
||||||
|
<label>Tab Separated Time Table</label>
|
||||||
|
</channel-type>
|
||||||
|
|
||||||
|
<channel-type id="departure">
|
||||||
|
<item-type>String</item-type>
|
||||||
|
<label>Departure</label>
|
||||||
|
<description>A single departure</description>
|
||||||
|
<state readOnly="true" pattern="%s"/>
|
||||||
|
</channel-type>
|
||||||
|
|
||||||
|
</thing:thing-descriptions>
|
||||||
@ -277,6 +277,7 @@
|
|||||||
<module>org.openhab.binding.plugwiseha</module>
|
<module>org.openhab.binding.plugwiseha</module>
|
||||||
<module>org.openhab.binding.powermax</module>
|
<module>org.openhab.binding.powermax</module>
|
||||||
<module>org.openhab.binding.proteusecometer</module>
|
<module>org.openhab.binding.proteusecometer</module>
|
||||||
|
<module>org.openhab.binding.publictransportswitzerland</module>
|
||||||
<module>org.openhab.binding.pulseaudio</module>
|
<module>org.openhab.binding.pulseaudio</module>
|
||||||
<module>org.openhab.binding.pushbullet</module>
|
<module>org.openhab.binding.pushbullet</module>
|
||||||
<module>org.openhab.binding.pushover</module>
|
<module>org.openhab.binding.pushover</module>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user