[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:
Jeremy Stucki 2021-12-27 21:19:22 +01:00 committed by GitHub
parent d38f37fa1a
commit 1cae863569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 584 additions and 0 deletions

View File

@ -245,6 +245,7 @@
/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

View File

@ -1216,6 +1216,11 @@
<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>

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,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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

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

View File

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

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.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/";
}

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

View File

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

View File

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

View File

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

View File

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

View File

@ -277,6 +277,7 @@
<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>