added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.onebusaway</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

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,86 @@
# OneBusAway Binding
[OneBusAway](https://onebusaway.org/) is an open source, real-time, transit-information service. This binding allows you to get events based on transit arrival and departures, so you can create rules to do something based on that data.
## Preparation
You will need to obtain an API key from the transit provider you want to load data from.
Different providers of the service have different policies, so you will have to figure this part out for each [deployment](https://github.com/OneBusAway/onebusaway/wiki/OneBusAway-Deployments).
## Supported Things
This binding supports route arrival and departure times for all stops provided from a OneBusAway deployment.
## Binding Configuration
The following configuration options are available for the API binding:
| Parameter | Name | Description | Required |
|-------------|------------|-------------------------------------------------------------------------------------|----------|
| `apiKey` | API Key | The API key given to you by a transit provider for their deployment. | yes |
| `apiServer` | API Server | The domain name of the deployment to talk to, e.g. `api.pugetsound.onebusaway.org`. | yes |
The following configuration options are available for the Stop binding (which requires an API binding):
| Parameter | Name | Description | Required |
|-----------|------|-------------|----------|
| `stopId` | Stop ID | The OneBusAway ID of the stop to obtain data for, e.g. `1_26860`. | yes |
| `interval` | Update Interval | The number of seconds between updates. | no |
## Thing Configuration
The following configuration options are available for a Route (which requires a Stop binding):
| Parameter | Name | Description | Required |
|-----------|----------|---------------------------------------------------------------------|----------|
| `routeId` | Route ID | The OneBusAway ID of the route to obtain data for, e.g. `1_102574`. | yes |
## Channels
The Route Thing supports the following state channels:
| Channel Type ID | Channel Kind | Item Type | Description |
|------------------|--------------|-----------|----------------------------------------------------------------------------------------------------------|
| arrival | state | DateTime | The arrival time of a Route at a Stop. |
| departure | state | DateTime | The departure time of a Route at a Stop. |
| update | state | DateTime | The last time this data was updated (per the data provider, not the last time openHAB updated the data). |
| arrivalDeparture | trigger | DateTime | Triggered when a Route arrives or departs a Stop. |
### Channel Configurations
The `arrival`, `departure`, and `arrivalDeparture` channels can be configured with an `offset` specifying the number of seconds to move an event back in time.
## Full Example
Here is an example of a configuration for a bus stop in Seattle, WA, USA that has three routes configured.
`demo.things`:
```
Bridge onebusaway:api:pugetsound [apiKey="your-api-key", apiServer="api.pugetsound.onebusaway.org"] {
Bridge onebusaway:stop:1_26860 [stopId="1_26860"] {
Thing onebusaway:route:1_100193 [routeId="1_100193"]
Thing onebusaway:route:1_102574 [routeId="1_102574"]
Thing onebusaway:route:1_100252 [routeId="1_100252"]
}
}
```
`demo.items`:
```
// Route 1_100193 (#32)
DateTime Fremont_32_Arrival "32 - University District" { channel="onebusaway:route:1_100193:arrival" }
DateTime Fremont_32_Departure "32 - University District" { channel="onebusaway:route:1_100193:departure" }
// Route 1_102574 (#40)
DateTime Fremont_40_Arrival "40 - Ballard" { channel="onebusaway:route:1_102574:arrival" }
DateTime Fremont_40_Departure "40 - Ballard" { channel="onebusaway:route:1_102574:departure" }
// Route 1_100252 (#62)
DateTime Fremont_62_Arrival "62 - Sand Point East Green Lake" { channel="onebusaway:route:1_100252:arrival" }
DateTime Fremont_62_Departure "62 - Sand Point East Green Lake" { channel="onebusaway:route:1_100252:departure" }
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.onebusaway</artifactId>
<name>openHAB Add-ons :: Bundles :: OneBusAway Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.onebusaway-${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-onebusaway" description="OneBusAway Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.onebusaway/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link OneBusAwayBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Shawn Wilsher - Initial contribution
*/
@NonNullByDefault
public class OneBusAwayBindingConstants {
public static final String BINDING_ID = "onebusaway";
// Things
public static final ThingTypeUID THING_TYPE_API = new ThingTypeUID(BINDING_ID, "api");
public static final ThingTypeUID THING_TYPE_ROUTE = new ThingTypeUID(BINDING_ID, "route");
public static final ThingTypeUID THING_TYPE_STOP = new ThingTypeUID(BINDING_ID, "stop");
// Channel IDs
public static final String CHANNEL_ID_ARRIVAL = "arrival";
public static final String CHANNEL_ID_DEPARTURE = "departure";
public static final String CHANNEL_ID_UPDATE = "update";
// Events
public static final String EVENT_ARRIVAL = "ARRIVAL";
public static final String EVENT_DEPARTURE = "DEPARTURE";
// Event channel IDs
public static final String EVENT_CHANNEL_ID_ARRIVAL = "arrivalDeparture#event";
// Channel configs
public static final String CHANNEL_CONFIG_OFFSET = "offset";
// API configs
public static final String API_CONFIG_API_KEY = "apiKey";
public static final String API_CONFIG_API_SERVER = "apiServer";
// Route configs
public static final String ROUTE_CONFIG_ROUTE_ID = "routeId";
// Stop configs
public static final String STOP_CONFIG_ID = "stopId";
public static final String STOP_CONFIG_INTERVAL = "interval";
// Route properties
public static final String ROUTE_PROPERTY_HEADSIGN = "headsign";
public static final String ROUTE_PROPERTY_LONG_NAME = "longName";
public static final String ROUTE_PROPERTY_SHORT_NAME = "shortName";
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.onebusaway.internal.handler.ApiHandler;
import org.openhab.binding.onebusaway.internal.handler.RouteHandler;
import org.openhab.binding.onebusaway.internal.handler.StopHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link OneBusAwayHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Shawn Wilsher - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.onebusaway")
@NonNullByDefault
public class OneBusAwayHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(ApiHandler.SUPPORTED_THING_TYPE, RouteHandler.SUPPORTED_THING_TYPE, StopHandler.SUPPORTED_THING_TYPE)
.collect(Collectors.toSet()));
private final HttpClient httpClient;
@Activate
public OneBusAwayHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@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 (thingTypeUID.equals(THING_TYPE_API)) {
return new ApiHandler((Bridge) thing);
} else if (thingTypeUID.equals(THING_TYPE_ROUTE)) {
return new RouteHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_STOP)) {
return new StopHandler((Bridge) thing, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.config;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
* The {@link ApiConfiguration} defines the model for a API bridge configuration.
*
* @author Shawn Wilsher - Initial contribution
*/
public class ApiConfiguration {
private String apiKey;
private String apiServer;
/**
* @return the API Key to access the OneBusAway server.
*/
public String getApiKey() {
return apiKey;
}
/**
* Sets the API Key for the OneBusAway server.
*/
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
/**
* @return the OneBusAway API server to use.
*/
public String getApiServer() {
return apiServer;
}
/**
* Sets the OneBusAway API server.
*/
public void setApiServer(String apiServer) {
this.apiServer = apiServer;
}
@Override
public String toString() {
return new ToStringBuilder(this).append(API_CONFIG_API_KEY, this.getApiKey())
.append(API_CONFIG_API_SERVER, this.getApiServer()).toString();
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.config;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.CHANNEL_CONFIG_OFFSET;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
* The {@link ChannelConfig} defines the model for a channel configuration.
*
* @author Shawn Wilsher - Initial contribution
*/
public class ChannelConfig {
private Integer offset = 0;
/**
* @return the offset (in seconds).
*/
public Integer getOffset() {
return offset;
}
/**
* Sets the offset (in seconds).
*/
public void setOffset(Integer offset) {
this.offset = offset;
}
@Override
public String toString() {
return new ToStringBuilder(this).append(CHANNEL_CONFIG_OFFSET, this.getOffset()).toString();
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.config;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.ROUTE_CONFIG_ROUTE_ID;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
* The {@link RouteConfiguration} defines the model for a route stop configuration.
*
* @author Shawn Wilsher - Initial contribution
*/
public class RouteConfiguration {
private String routeId;
/**
* @return the route ID.
*/
public String getRouteId() {
return routeId;
}
/**
* Sets the route ID.
*/
public void setRouteId(String routeId) {
this.routeId = routeId;
}
@Override
public String toString() {
return new ToStringBuilder(this).append(ROUTE_CONFIG_ROUTE_ID, this.getRouteId()).toString();
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.config;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
* The {@link StopConfiguration} defines the model for a stop bridge configuration.
*
* @author Shawn Wilsher - Initial contribution
*/
public class StopConfiguration {
private Integer interval;
private String stopId;
/**
* @return the update interval (in seconds).
*/
public Integer getInterval() {
return interval;
}
/**
* Sets the update interval (in seconds).
*/
public void setInterval(Integer interval) {
this.interval = interval;
}
/**
* @return the stop ID.
*/
public String getStopId() {
return stopId;
}
/**
* Sets the stop ID.
*/
public void setStopId(String stopId) {
this.stopId = stopId;
}
@Override
public String toString() {
return new ToStringBuilder(this).append(STOP_CONFIG_INTERVAL, this.getInterval())
.append(STOP_CONFIG_ID, this.getStopId()).toString();
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.handler;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.THING_TYPE_API;
import org.openhab.binding.onebusaway.internal.config.ApiConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ApiHandler} is responsible for storing basic configuration data for talking to a OneBusAway API server.
*
* @author Shawn Wilsher - Initial contribution
*/
public class ApiHandler extends BaseBridgeHandler {
public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_API;
private ApiConfiguration config;
private Logger logger = LoggerFactory.getLogger(ApiHandler.class);
public ApiHandler(Bridge bridge) {
super(bridge);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.warn("The API bridge is a read-only and can not handle commands.");
}
@Override
public void initialize() {
logger.debug("Initializing OneBusAway bridge...");
config = loadAndCheckConfiguration();
if (config == null) {
logger.debug("Initialization of OneBusAway API bridge failed!");
return;
}
updateStatus(ThingStatus.ONLINE);
}
protected String getApiKey() {
return config.getApiKey();
}
protected String getApiServer() {
return config.getApiServer();
}
private ApiConfiguration loadAndCheckConfiguration() {
ApiConfiguration config = getConfigAs(ApiConfiguration.class);
if (config.getApiKey() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "apiKey is not set");
return null;
}
if (config.getApiServer() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "apiServer is not set");
return null;
}
return config;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.handler;
/**
* The {@link ObaStopArrivalResponse} is a representation of the OneBusAway response for requesting a stops arrival
* data.
*
* @see <a href=
* "http://developer.onebusaway.org/modules/onebusaway-application-modules/current/api/where/methods/arrivals-and-departures-for-stop.html">arrivals-and-departures-for-stop
* documentation</a>
*
* @author Shawn Wilsher - Initial contribution
*/
public class ObaStopArrivalResponse {
public long currentTime;
public Data data;
public class Data {
public Entry entry;
}
public class Entry {
public ArrivalAndDeparture[] arrivalsAndDepartures;
}
public class ArrivalAndDeparture implements Comparable<ArrivalAndDeparture> {
public boolean predicted;
public long predictedArrivalTime;
public long predictedDepartureTime;
public long scheduledArrivalTime;
public long scheduledDepartureTime;
public String routeLongName;
public String routeShortName;
public String routeId;
public String stopId;
public String tripHeadsign;
/**
* Assumes other is for the same routeId and stopId. Sorts based on arrival time.
*/
@Override
public int compareTo(ArrivalAndDeparture other) {
// Prefer predicated over scheduled times for order
return (int) ((predicted ? predictedArrivalTime : scheduledArrivalTime)
- (other.predicted ? other.predictedArrivalTime : other.scheduledArrivalTime));
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.handler;
import java.util.List;
/**
* The {@link RouteDataListener} is the interface used for the stop (a bridge) to communicate information about route
* arrivals.
*
* @author Shawn Wilsher - Initial contribution
*/
interface RouteDataListener {
/**
* @return The routeId for this listener. {@link #onNewRouteData(List)} should only receive updates for
* this route.
*/
String getRouteId();
/**
* Called when new arrival and departure data is available for this listeners route (as specified by
* {@link #getRouteId()}).
*
* @param lastUpdateTime a {@link long} representing the time the data was last updated.
* @param data a {@link List} of data from the OneBusAway API.
*/
void onNewRouteData(long lastUpdateTime, List<ObaStopArrivalResponse.ArrivalAndDeparture> data);
}

View File

@@ -0,0 +1,257 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.handler;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.*;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.openhab.binding.onebusaway.internal.config.ChannelConfig;
import org.openhab.binding.onebusaway.internal.config.RouteConfiguration;
import org.openhab.binding.onebusaway.internal.handler.ObaStopArrivalResponse.ArrivalAndDeparture;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RouteHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Shawn Wilsher - Initial contribution
*/
public class RouteHandler extends BaseThingHandler implements RouteDataListener {
public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_ROUTE;
private final Logger logger = LoggerFactory.getLogger(RouteHandler.class);
private RouteConfiguration config;
private List<ScheduledFuture<?>> scheduledFutures = new CopyOnWriteArrayList<>();
public RouteHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH == command) {
logger.debug("Refreshing {}...", channelUID);
switch (channelUID.getId()) {
case CHANNEL_ID_ARRIVAL:
case CHANNEL_ID_DEPARTURE:
case CHANNEL_ID_UPDATE:
StopHandler stopHandler = getStopHandler();
if (stopHandler != null) {
stopHandler.forceUpdate();
}
break;
default:
logger.warn("Unnknown channel UID {} with comamnd {}", channelUID.getId(), command);
}
} else {
logger.debug("The OneBusAway route is read-only and can not handle commands.");
}
}
@Override
public void initialize() {
logger.debug("Initializing OneBusAway route stop...");
config = loadAndCheckConfiguration();
if (config == null) {
logger.debug("Initialization of OneBusAway route stop failed!");
return;
}
StopHandler stopHandler = getStopHandler();
if (stopHandler != null) {
// We will be marked as ONLINE when we get data in our callback, which should be immediately because the
// StopHandler, our bridge, won't be marked as online until it has data itself.
stopHandler.registerRouteDataListener(this);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge unavailable");
}
}
@Override
public void dispose() {
cancelAllScheduledFutures();
StopHandler stopHandler = getStopHandler();
if (stopHandler != null) {
stopHandler.unregisterRouteDataListener(this);
}
}
@Override
public String getRouteId() {
return config.getRouteId();
}
@Override
public void onNewRouteData(long lastUpdateTime, List<ArrivalAndDeparture> data) {
if (data.isEmpty()) {
return;
}
// Publish to all of our linked channels.
Calendar now = Calendar.getInstance();
for (Channel channel : getThing().getChannels()) {
if (channel.getKind() == ChannelKind.TRIGGER) {
scheduleTriggerEvents(channel.getUID(), now, data);
} else {
publishChannel(channel.getUID(), now, lastUpdateTime, data);
}
}
updateStatus(ThingStatus.ONLINE);
}
private StopHandler getStopHandler() {
return (StopHandler) getBridge().getHandler();
}
private RouteConfiguration loadAndCheckConfiguration() {
RouteConfiguration config = getConfigAs(RouteConfiguration.class);
if (config.getRouteId() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "routeId is not set");
return null;
}
return config;
}
private void cancelAllScheduledFutures() {
for (ScheduledFuture<?> future : scheduledFutures) {
if (!future.isDone() || !future.isCancelled()) {
future.cancel(true);
}
}
scheduledFutures = new CopyOnWriteArrayList<>();
}
private void updatePropertiesFromArrivalAndDeparture(ArrivalAndDeparture data) {
Map<String, String> props = editProperties();
props.put(ROUTE_PROPERTY_HEADSIGN, data.tripHeadsign);
props.put(ROUTE_PROPERTY_LONG_NAME, data.routeLongName);
props.put(ROUTE_PROPERTY_SHORT_NAME, data.routeShortName);
updateProperties(props);
}
/**
* Publishes the channel with data and possibly schedules work to update it again when the next event has passed.
*/
private void publishChannel(ChannelUID channelUID, Calendar now, long lastUpdateTime,
List<ArrivalAndDeparture> arrivalAndDepartures) {
if (channelUID.getId().equals(CHANNEL_ID_UPDATE)) {
updateState(channelUID, new DateTimeType(
ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastUpdateTime), ZoneId.systemDefault())));
return;
}
ChannelConfig channelConfig = getThing().getChannel(channelUID.getId()).getConfiguration()
.as(ChannelConfig.class);
long offsetMs = TimeUnit.SECONDS.toMillis(channelConfig.getOffset());
for (int i = 0; i < arrivalAndDepartures.size(); i++) {
ArrivalAndDeparture data = arrivalAndDepartures.get(i);
Calendar time;
switch (channelUID.getId()) {
case CHANNEL_ID_ARRIVAL:
time = (new Calendar.Builder())
.setInstant(
(data.predicted ? data.predictedArrivalTime : data.scheduledArrivalTime) - offsetMs)
.build();
break;
case CHANNEL_ID_DEPARTURE:
time = (new Calendar.Builder()).setInstant(
(data.predicted ? data.predictedDepartureTime : data.scheduledDepartureTime) - offsetMs)
.build();
break;
default:
logger.warn("No code to handle publishing to {}", channelUID.getId());
return;
}
// Do not publish this if it's already passed.
if (time.before(now)) {
logger.debug("Not notifying {} because it is in the past.", channelUID.getId());
continue;
}
updateState(channelUID,
new DateTimeType(ZonedDateTime.ofInstant(time.toInstant(), ZoneId.systemDefault())));
// Update properties only when we update arrival information. This is not perfect.
if (channelUID.getId().equals(CHANNEL_ID_ARRIVAL)) {
updatePropertiesFromArrivalAndDeparture(data);
}
// Schedule updates in the future. These may be canceled if we are notified about new data in the future.
List<ArrivalAndDeparture> remaining = arrivalAndDepartures.subList(i + 1, arrivalAndDepartures.size());
if (remaining.isEmpty()) {
return;
}
scheduledFutures.add(scheduler.schedule(() -> {
publishChannel(channelUID, Calendar.getInstance(), lastUpdateTime, remaining);
}, time.getTimeInMillis() - now.getTimeInMillis(), TimeUnit.MILLISECONDS));
return;
}
}
private void scheduleTriggerEvents(ChannelUID channelUID, Calendar now,
List<ArrivalAndDeparture> arrivalAndDepartures) {
scheduleTriggerEvents(channelUID, now, arrivalAndDepartures,
(ArrivalAndDeparture data) -> data.predicted ? data.predictedArrivalTime : data.scheduledArrivalTime,
EVENT_ARRIVAL);
scheduleTriggerEvents(channelUID, now, arrivalAndDepartures, new Function<ArrivalAndDeparture, Long>() {
@Override
public Long apply(ArrivalAndDeparture data) {
return data.predicted ? data.predictedDepartureTime : data.scheduledDepartureTime;
}
}, EVENT_DEPARTURE);
}
private void scheduleTriggerEvents(ChannelUID channelUID, Calendar now,
List<ArrivalAndDeparture> arrivalAndDepartures, Function<ArrivalAndDeparture, Long> dataPiece,
String event) {
ChannelConfig channelConfig = getThing().getChannel(channelUID.getId()).getConfiguration()
.as(ChannelConfig.class);
long offsetMs = TimeUnit.SECONDS.toMillis(channelConfig.getOffset());
for (ArrivalAndDeparture data : arrivalAndDepartures) {
long time = dataPiece.apply(data);
// Do not schedule this if it's already passed.
Calendar cal = (new Calendar.Builder()).setInstant(time - offsetMs).build();
if (cal.before(now)) {
continue;
}
// Schedule this trigger
scheduledFutures.add(scheduler.schedule(() -> {
triggerChannel(channelUID, event);
}, cal.getTimeInMillis() - now.getTimeInMillis(), TimeUnit.MILLISECONDS));
}
}
}

View File

@@ -0,0 +1,232 @@
/**
* Copyright (c) 2010-2020 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.onebusaway.internal.handler;
import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.THING_TYPE_STOP;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.onebusaway.internal.config.StopConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link StopHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Shawn Wilsher - Initial contribution
*/
public class StopHandler extends BaseBridgeHandler {
public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_STOP;
private final Logger logger = LoggerFactory.getLogger(StopHandler.class);
private StopConfiguration config;
private Gson gson;
private HttpClient httpClient;
private ScheduledFuture<?> pollingJob;
private AtomicBoolean fetchInProgress = new AtomicBoolean(false);
private long routeDataLastUpdateMs = 0;
private final Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> routeData = new HashMap<>();
private List<RouteDataListener> routeDataListeners = new CopyOnWriteArrayList<>();
public StopHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH == command) {
logger.debug("Refreshing {}...", channelUID);
forceUpdate();
} else {
logger.debug("The OneBusAway Stop is a read-only and can not handle commands.");
}
}
@Override
public void initialize() {
logger.debug("Initializing OneBusAway stop bridge...");
config = loadAndCheckConfiguration();
if (config == null) {
logger.debug("Initialization of OneBusAway bridge failed!");
return;
}
// Do the rest of the work asynchronously because it can take a while.
scheduler.submit(() -> {
gson = new Gson();
pollingJob = scheduler.scheduleWithFixedDelay(this::fetchAndUpdateStopData, 0, config.getInterval(),
TimeUnit.SECONDS);
});
}
@Override
public void dispose() {
if (pollingJob != null && !pollingJob.isCancelled()) {
pollingJob.cancel(true);
pollingJob = null;
}
}
/**
* Registers the listener to receive updates about arrival and departure times for its route.
*
* @param listener
* @return true if successful.
*/
protected boolean registerRouteDataListener(RouteDataListener listener) {
if (listener == null) {
throw new IllegalArgumentException("It makes no sense to register a null listener!");
}
boolean added = routeDataListeners.add(listener);
if (added) {
String routeId = listener.getRouteId();
List<ObaStopArrivalResponse.ArrivalAndDeparture> copiedRouteData;
synchronized (routeData) {
copiedRouteData = new ArrayList<>(routeData.get(routeId));
}
Collections.sort(copiedRouteData);
listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData);
}
return added;
}
/**
* Unregisters the listener so it no longer receives updates about arrival and departure times for its route.
*
* @param listener
* @return true if successful.
*/
protected boolean unregisterRouteDataListener(RouteDataListener listener) {
return routeDataListeners.remove(listener);
}
/**
* Forced an update to be scheduled immediately.
*/
protected void forceUpdate() {
scheduler.execute(this::fetchAndUpdateStopData);
}
private ApiHandler getApiHandler() {
return (ApiHandler) getBridge().getHandler();
}
private StopConfiguration loadAndCheckConfiguration() {
StopConfiguration config = getConfigAs(StopConfiguration.class);
if (config.getInterval() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "interval is not set");
return null;
}
if (config.getStopId() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "stopId is not set");
return null;
}
return config;
}
private boolean fetchAndUpdateStopData() {
try {
ApiHandler apiHandler = getApiHandler();
if (apiHandler == null) {
// We must be offline.
return false;
}
boolean alreadyFetching = !fetchInProgress.compareAndSet(false, true);
if (alreadyFetching) {
return false;
}
logger.debug("Fetching data for stop ID {}", config.getStopId());
String url = String.format("http://%s/api/where/arrivals-and-departures-for-stop/%s.json?key=%s",
apiHandler.getApiServer(), config.getStopId(), apiHandler.getApiKey());
URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
logger.error("Unable to parse {} as a URI.", url);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"stopId or apiKey is set to a bogus value");
return false;
}
ContentResponse response;
try {
response = httpClient.newRequest(uri).send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
return false;
}
if (response.getStatus() != HttpStatus.OK_200) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
String.format("While fetching stop data: %d: %s", response.getStatus(), response.getReason()));
return false;
}
ObaStopArrivalResponse data = gson.fromJson(response.getContentAsString(), ObaStopArrivalResponse.class);
routeDataLastUpdateMs = data.currentTime;
updateStatus(ThingStatus.ONLINE);
Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> copiedRouteData = new HashMap<>();
synchronized (routeData) {
routeData.clear();
for (ObaStopArrivalResponse.ArrivalAndDeparture d : data.data.entry.arrivalsAndDepartures) {
routeData.put(d.routeId, Arrays.asList(d));
}
for (String key : routeData.keySet()) {
List<ObaStopArrivalResponse.ArrivalAndDeparture> copy = new ArrayList<>(routeData.get(key));
Collections.sort(copy);
copiedRouteData.put(key, copy);
}
}
for (RouteDataListener listener : routeDataListeners) {
listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData.get(listener.getRouteId()));
}
return true;
} catch (Exception e) {
logger.debug("Exception refreshing route data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
return false;
} finally {
fetchInProgress.set(false);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="onebusaway" 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>OneBusAway Binding</name>
<description>This is the binding for OneBusAway, an open system to provide transit data.</description>
<author>Shawn Wilsher</author>
</binding:binding>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type:onebusaway:config">
<parameter name="offset" type="integer" min="0" max="3600">
<label>Offset</label>
<description>Moves an event or datetime value backward (in seconds)</description>
<default>0</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onebusaway"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="api">
<label>OneBusAway Service</label>
<description>The Service settings to talk to a OneBusAway deployment.</description>
<config-description>
<parameter name="apiKey" type="text" required="true">
<label>API Key</label>
<description>The OneBusAway API key to use.</description>
<context>password</context>
</parameter>
<parameter name="apiServer" type="text" required="true">
<label>API Server</label>
<description>The OneBusAway API Server to use.</description>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onebusaway"
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">
<channel-type id="arrival">
<item-type>DateTime</item-type>
<label>Arrival Time</label>
<description>The arrival time</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
<config-description-ref uri="channel-type:onebusaway:config"/>
</channel-type>
<channel-type id="departure">
<item-type>DateTime</item-type>
<label>Departure Time</label>
<description>The departure time</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
<config-description-ref uri="channel-type:onebusaway:config"/>
</channel-type>
<channel-type id="routeEvent">
<kind>trigger</kind>
<label>Route Event</label>
<description>Route event</description>
<event>
<options>
<option value="ARRIVAL">arrival</option>
<option value="DEPARTURE">departure</option>
</options>
</event>
<config-description-ref uri="channel-type:onebusaway:config"/>
</channel-type>
<channel-type id="update">
<item-type>DateTime</item-type>
<label>Last Update Time</label>
<description>The last time information was updated</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onebusaway"
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="route">
<supported-bridge-type-refs>
<bridge-type-ref id="stop"/>
</supported-bridge-type-refs>
<label>Route</label>
<description>Provides data about a route at a specific stop.</description>
<channels>
<channel id="arrival" typeId="arrival"/>
<channel id="departure" typeId="departure"/>
<channel id="event" typeId="routeEvent"/>
<channel id="update" typeId="update"/>
</channels>
<config-description uri="thing-type:onebusaway:config">
<parameter name="routeId" type="text" required="true">
<label>Route ID</label>
<description>The OneBusAway route ID.</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onebusaway"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="stop">
<supported-bridge-type-refs>
<bridge-type-ref id="api"/>
</supported-bridge-type-refs>
<label>Stop</label>
<description>Provides data about different routes at a specific stop.</description>
<config-description>
<parameter name="interval" type="integer" min="60" max="7200">
<label>Interval</label>
<description>Refresh interval for arrival data in seconds.</description>
<default>300</default>
</parameter>
<parameter name="stopId" type="text" required="true">
<label>Stop ID</label>
<description>The OneBusAway stop ID.</description>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>