2 * Copyright (c) 2010-2022 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 routeDataListeners.add(listener);
123 String routeId = listener.getRouteId();
124 List<ObaStopArrivalResponse.ArrivalAndDeparture> copiedRouteData;
125 synchronized (routeData) {
126 copiedRouteData = new ArrayList<>(routeData.getOrDefault(routeId, List.of()));
128 Collections.sort(copiedRouteData);
129 listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData);
135 * Unregisters the listener so it no longer receives updates about arrival and departure times for its route.
138 * @return true if successful.
140 protected boolean unregisterRouteDataListener(RouteDataListener listener) {
141 return routeDataListeners.remove(listener);
145 * Forced an update to be scheduled immediately.
147 protected void forceUpdate() {
148 scheduler.execute(this::fetchAndUpdateStopData);
151 private ApiHandler getApiHandler() {
152 return (ApiHandler) getBridge().getHandler();
155 private StopConfiguration loadAndCheckConfiguration() {
156 StopConfiguration config = getConfigAs(StopConfiguration.class);
157 if (config.getInterval() == null) {
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "interval is not set");
161 if (config.getStopId() == null) {
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "stopId is not set");
168 private boolean fetchAndUpdateStopData() {
170 ApiHandler apiHandler = getApiHandler();
171 if (apiHandler == null) {
172 // We must be offline.
175 boolean alreadyFetching = !fetchInProgress.compareAndSet(false, true);
176 if (alreadyFetching) {
179 logger.debug("Fetching data for stop ID {}", config.getStopId());
180 String url = String.format("http://%s/api/where/arrivals-and-departures-for-stop/%s.json?key=%s",
181 apiHandler.getApiServer(), config.getStopId(), apiHandler.getApiKey());
185 } catch (URISyntaxException e) {
186 logger.error("Unable to parse {} as a URI.", url);
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
188 "stopId or apiKey is set to a bogus value");
191 ContentResponse response;
193 response = httpClient.newRequest(uri).send();
194 } catch (InterruptedException | TimeoutException | ExecutionException e) {
195 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
198 if (response.getStatus() != HttpStatus.OK_200) {
199 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
200 String.format("While fetching stop data: %d: %s", response.getStatus(), response.getReason()));
203 ObaStopArrivalResponse data = gson.fromJson(response.getContentAsString(), ObaStopArrivalResponse.class);
204 routeDataLastUpdateMs = data.currentTime;
205 updateStatus(ThingStatus.ONLINE);
207 Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> copiedRouteData = new HashMap<>();
208 synchronized (routeData) {
210 for (ObaStopArrivalResponse.ArrivalAndDeparture d : data.data.entry.arrivalsAndDepartures) {
211 routeData.put(d.routeId, Arrays.asList(d));
213 for (String key : routeData.keySet()) {
214 List<ObaStopArrivalResponse.ArrivalAndDeparture> copy = new ArrayList<>(
215 routeData.getOrDefault(key, List.of()));
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);