2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.onebusaway.internal.handler;
15 import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.THING_TYPE_STOP;
18 import java.net.URISyntaxException;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
30 import java.util.concurrent.atomic.AtomicBoolean;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.openhab.binding.onebusaway.internal.config.StopConfiguration;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingTypeUID;
41 import org.openhab.core.thing.binding.BaseBridgeHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.google.gson.Gson;
50 * The {@link StopHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Shawn Wilsher - Initial contribution
55 public class StopHandler extends BaseBridgeHandler {
57 public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_STOP;
59 private final Logger logger = LoggerFactory.getLogger(StopHandler.class);
61 private StopConfiguration config;
63 private HttpClient httpClient;
64 private ScheduledFuture<?> pollingJob;
65 private AtomicBoolean fetchInProgress = new AtomicBoolean(false);
66 private long routeDataLastUpdateMs = 0;
67 private final Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> routeData = new HashMap<>();
68 private List<RouteDataListener> routeDataListeners = new CopyOnWriteArrayList<>();
70 public StopHandler(Bridge bridge, HttpClient httpClient) {
72 this.httpClient = httpClient;
76 public void handleCommand(ChannelUID channelUID, Command command) {
77 if (RefreshType.REFRESH == command) {
78 logger.debug("Refreshing {}...", channelUID);
81 logger.debug("The OneBusAway Stop is a read-only and can not handle commands.");
86 public void initialize() {
87 logger.debug("Initializing OneBusAway stop bridge...");
89 config = loadAndCheckConfiguration();
91 logger.debug("Initialization of OneBusAway bridge failed!");
95 // Do the rest of the work asynchronously because it can take a while.
96 scheduler.submit(() -> {
99 pollingJob = scheduler.scheduleWithFixedDelay(this::fetchAndUpdateStopData, 0, config.getInterval(),
105 public void dispose() {
106 if (pollingJob != null && !pollingJob.isCancelled()) {
107 pollingJob.cancel(true);
113 * Registers the listener to receive updates about arrival and departure times for its route.
116 * @return true if successful.
118 protected boolean registerRouteDataListener(RouteDataListener listener) {
119 if (listener == null) {
120 throw new IllegalArgumentException("It makes no sense to register a null listener!");
122 boolean added = routeDataListeners.add(listener);
124 String routeId = listener.getRouteId();
125 List<ObaStopArrivalResponse.ArrivalAndDeparture> copiedRouteData;
126 synchronized (routeData) {
127 copiedRouteData = new ArrayList<>(routeData.get(routeId));
129 Collections.sort(copiedRouteData);
130 listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData);
136 * Unregisters the listener so it no longer receives updates about arrival and departure times for its route.
139 * @return true if successful.
141 protected boolean unregisterRouteDataListener(RouteDataListener listener) {
142 return routeDataListeners.remove(listener);
146 * Forced an update to be scheduled immediately.
148 protected void forceUpdate() {
149 scheduler.execute(this::fetchAndUpdateStopData);
152 private ApiHandler getApiHandler() {
153 return (ApiHandler) getBridge().getHandler();
156 private StopConfiguration loadAndCheckConfiguration() {
157 StopConfiguration config = getConfigAs(StopConfiguration.class);
158 if (config.getInterval() == null) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "interval is not set");
162 if (config.getStopId() == null) {
163 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "stopId is not set");
169 private boolean fetchAndUpdateStopData() {
171 ApiHandler apiHandler = getApiHandler();
172 if (apiHandler == null) {
173 // We must be offline.
176 boolean alreadyFetching = !fetchInProgress.compareAndSet(false, true);
177 if (alreadyFetching) {
180 logger.debug("Fetching data for stop ID {}", config.getStopId());
181 String url = String.format("http://%s/api/where/arrivals-and-departures-for-stop/%s.json?key=%s",
182 apiHandler.getApiServer(), config.getStopId(), apiHandler.getApiKey());
186 } catch (URISyntaxException e) {
187 logger.error("Unable to parse {} as a URI.", url);
188 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
189 "stopId or apiKey is set to a bogus value");
192 ContentResponse response;
194 response = httpClient.newRequest(uri).send();
195 } catch (InterruptedException | TimeoutException | ExecutionException e) {
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
199 if (response.getStatus() != HttpStatus.OK_200) {
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
201 String.format("While fetching stop data: %d: %s", response.getStatus(), response.getReason()));
204 ObaStopArrivalResponse data = gson.fromJson(response.getContentAsString(), ObaStopArrivalResponse.class);
205 routeDataLastUpdateMs = data.currentTime;
206 updateStatus(ThingStatus.ONLINE);
208 Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> copiedRouteData = new HashMap<>();
209 synchronized (routeData) {
211 for (ObaStopArrivalResponse.ArrivalAndDeparture d : data.data.entry.arrivalsAndDepartures) {
212 routeData.put(d.routeId, Arrays.asList(d));
214 for (String key : routeData.keySet()) {
215 List<ObaStopArrivalResponse.ArrivalAndDeparture> copy = new ArrayList<>(routeData.get(key));
216 Collections.sort(copy);
217 copiedRouteData.put(key, copy);
220 for (RouteDataListener listener : routeDataListeners) {
221 listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData.get(listener.getRouteId()));
224 } catch (Exception e) {
225 logger.debug("Exception refreshing route data", e);
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
229 fetchInProgress.set(false);