]> git.basschouten.com Git - openhab-addons.git/commitdiff
[publictransportswitzerland] Public Transport Switzerland Initial contribution (...
authorJeremy Stucki <dev@jeremystucki.ch>
Mon, 27 Dec 2021 20:19:22 +0000 (21:19 +0100)
committerGitHub <noreply@github.com>
Mon, 27 Dec 2021 20:19:22 +0000 (21:19 +0100)
* [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>
14 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.publictransportswitzerland/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/README.md [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/doc/departure_board_habpanel.png [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/PublicTransportSwitzerlandBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/PublicTransportSwitzerlandHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/stationboard/PublicTransportSwitzerlandStationboardConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/stationboard/PublicTransportSwitzerlandStationboardHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.publictransportswitzerland/src/main/resources/OH-INF/thing/stationboard.xml [new file with mode: 0644]
bundles/pom.xml

index c09e2b9f1e3beebde8953ddb18e40317fd06cc7a..a3f827da53b3ed90a2e3448a79f1bb93d5b2fe59 100644 (file)
 /bundles/org.openhab.binding.plugwiseha/ @lsiepel
 /bundles/org.openhab.binding.powermax/ @lolodomo
 /bundles/org.openhab.binding.proteusecometer/ @2chilled
+/bundles/org.openhab.binding.publictransportswitzerland/ @jeremystucki
 /bundles/org.openhab.binding.pulseaudio/ @peuter
 /bundles/org.openhab.binding.pushbullet/ @hakan42
 /bundles/org.openhab.binding.pushover/ @cweitkamp
index 8e2a9ade62de9587c38dba147a32b364c39eb560..2a71e5b30ca63b3323bfb53d5b0d7d0aaa9d6108 100644 (file)
       <artifactId>org.openhab.binding.proteusecometer</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.publictransportswitzerland</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.pulseaudio</artifactId>
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/NOTICE b/bundles/org.openhab.binding.publictransportswitzerland/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/README.md b/bundles/org.openhab.binding.publictransportswitzerland/README.md
new file mode 100644 (file)
index 0000000..e8b601c
--- /dev/null
@@ -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))
+
+![Departure board in HABPanel](doc/departure_board_habpanel.png)
+
+## 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.
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/doc/departure_board_habpanel.png b/bundles/org.openhab.binding.publictransportswitzerland/doc/departure_board_habpanel.png
new file mode 100644 (file)
index 0000000..233a3b9
Binary files /dev/null and b/bundles/org.openhab.binding.publictransportswitzerland/doc/departure_board_habpanel.png differ
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/pom.xml b/bundles/org.openhab.binding.publictransportswitzerland/pom.xml
new file mode 100644 (file)
index 0000000..7392a0d
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/feature/feature.xml b/bundles/org.openhab.binding.publictransportswitzerland/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..38b894e
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/PublicTransportSwitzerlandBindingConstants.java b/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/PublicTransportSwitzerlandBindingConstants.java
new file mode 100644 (file)
index 0000000..a666604
--- /dev/null
@@ -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/";
+}
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/PublicTransportSwitzerlandHandlerFactory.java b/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/PublicTransportSwitzerlandHandlerFactory.java
new file mode 100644 (file)
index 0000000..b697882
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/stationboard/PublicTransportSwitzerlandStationboardConfiguration.java b/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/stationboard/PublicTransportSwitzerlandStationboardConfiguration.java
new file mode 100644 (file)
index 0000000..565c0f6
--- /dev/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;
+}
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/stationboard/PublicTransportSwitzerlandStationboardHandler.java b/bundles/org.openhab.binding.publictransportswitzerland/src/main/java/org/openhab/binding/publictransportswitzerland/internal/stationboard/PublicTransportSwitzerlandStationboardHandler.java
new file mode 100644 (file)
index 0000000..37c95d4
--- /dev/null
@@ -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));
+    }
+}
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.publictransportswitzerland/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..ab321cc
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.publictransportswitzerland/src/main/resources/OH-INF/thing/stationboard.xml b/bundles/org.openhab.binding.publictransportswitzerland/src/main/resources/OH-INF/thing/stationboard.xml
new file mode 100644 (file)
index 0000000..d2b417d
--- /dev/null
@@ -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>
index 62cc68f539505f932850a01d20beccf4dec3faec..a633b89ee6ee48f31b98773a0d6c25cf7412a39c 100644 (file)
     <module>org.openhab.binding.plugwiseha</module>
     <module>org.openhab.binding.powermax</module>
     <module>org.openhab.binding.proteusecometer</module>
+    <module>org.openhab.binding.publictransportswitzerland</module>
     <module>org.openhab.binding.pulseaudio</module>
     <module>org.openhab.binding.pushbullet</module>
     <module>org.openhab.binding.pushover</module>