]> git.basschouten.com Git - openhab-addons.git/blob
39fbf3be96f166dc175a2637f8083d29709e367c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.onebusaway.internal.handler;
14
15 import static org.openhab.binding.onebusaway.internal.OneBusAwayBindingConstants.THING_TYPE_STOP;
16
17 import java.net.URI;
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;
24 import java.util.Map;
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;
31
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;
46
47 import com.google.gson.Gson;
48
49 /**
50  * The {@link StopHandler} is responsible for handling commands, which are
51  * sent to one of the channels.
52  *
53  * @author Shawn Wilsher - Initial contribution
54  */
55 public class StopHandler extends BaseBridgeHandler {
56
57     public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_STOP;
58
59     private final Logger logger = LoggerFactory.getLogger(StopHandler.class);
60
61     private StopConfiguration config;
62     private Gson gson;
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<>();
69
70     public StopHandler(Bridge bridge, HttpClient httpClient) {
71         super(bridge);
72         this.httpClient = httpClient;
73     }
74
75     @Override
76     public void handleCommand(ChannelUID channelUID, Command command) {
77         if (RefreshType.REFRESH == command) {
78             logger.debug("Refreshing {}...", channelUID);
79             forceUpdate();
80         } else {
81             logger.debug("The OneBusAway Stop is a read-only and can not handle commands.");
82         }
83     }
84
85     @Override
86     public void initialize() {
87         logger.debug("Initializing OneBusAway stop bridge...");
88
89         config = loadAndCheckConfiguration();
90         if (config == null) {
91             logger.debug("Initialization of OneBusAway bridge failed!");
92             return;
93         }
94
95         // Do the rest of the work asynchronously because it can take a while.
96         scheduler.submit(() -> {
97             gson = new Gson();
98
99             pollingJob = scheduler.scheduleWithFixedDelay(this::fetchAndUpdateStopData, 0, config.getInterval(),
100                     TimeUnit.SECONDS);
101         });
102     }
103
104     @Override
105     public void dispose() {
106         if (pollingJob != null && !pollingJob.isCancelled()) {
107             pollingJob.cancel(true);
108             pollingJob = null;
109         }
110     }
111
112     /**
113      * Registers the listener to receive updates about arrival and departure times for its route.
114      *
115      * @param listener
116      * @return true if successful.
117      */
118     protected boolean registerRouteDataListener(RouteDataListener listener) {
119         if (listener == null) {
120             throw new IllegalArgumentException("It makes no sense to register a null listener!");
121         }
122         boolean added = routeDataListeners.add(listener);
123         if (added) {
124             String routeId = listener.getRouteId();
125             List<ObaStopArrivalResponse.ArrivalAndDeparture> copiedRouteData;
126             synchronized (routeData) {
127                 copiedRouteData = new ArrayList<>(routeData.get(routeId));
128             }
129             Collections.sort(copiedRouteData);
130             listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData);
131         }
132         return added;
133     }
134
135     /**
136      * Unregisters the listener so it no longer receives updates about arrival and departure times for its route.
137      *
138      * @param listener
139      * @return true if successful.
140      */
141     protected boolean unregisterRouteDataListener(RouteDataListener listener) {
142         return routeDataListeners.remove(listener);
143     }
144
145     /**
146      * Forced an update to be scheduled immediately.
147      */
148     protected void forceUpdate() {
149         scheduler.execute(this::fetchAndUpdateStopData);
150     }
151
152     private ApiHandler getApiHandler() {
153         return (ApiHandler) getBridge().getHandler();
154     }
155
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");
160             return null;
161         }
162         if (config.getStopId() == null) {
163             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "stopId is not set");
164             return null;
165         }
166         return config;
167     }
168
169     private boolean fetchAndUpdateStopData() {
170         try {
171             ApiHandler apiHandler = getApiHandler();
172             if (apiHandler == null) {
173                 // We must be offline.
174                 return false;
175             }
176             boolean alreadyFetching = !fetchInProgress.compareAndSet(false, true);
177             if (alreadyFetching) {
178                 return false;
179             }
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());
183             URI uri;
184             try {
185                 uri = new URI(url);
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");
190                 return false;
191             }
192             ContentResponse response;
193             try {
194                 response = httpClient.newRequest(uri).send();
195             } catch (InterruptedException | TimeoutException | ExecutionException e) {
196                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
197                 return false;
198             }
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()));
202                 return false;
203             }
204             ObaStopArrivalResponse data = gson.fromJson(response.getContentAsString(), ObaStopArrivalResponse.class);
205             routeDataLastUpdateMs = data.currentTime;
206             updateStatus(ThingStatus.ONLINE);
207
208             Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> copiedRouteData = new HashMap<>();
209             synchronized (routeData) {
210                 routeData.clear();
211                 for (ObaStopArrivalResponse.ArrivalAndDeparture d : data.data.entry.arrivalsAndDepartures) {
212                     routeData.put(d.routeId, Arrays.asList(d));
213                 }
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);
218                 }
219             }
220             for (RouteDataListener listener : routeDataListeners) {
221                 listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData.get(listener.getRouteId()));
222             }
223             return true;
224         } catch (Exception e) {
225             logger.debug("Exception refreshing route data", e);
226             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
227             return false;
228         } finally {
229             fetchInProgress.set(false);
230         }
231     }
232 }