]> git.basschouten.com Git - openhab-addons.git/blob
1a0dff88cb0ad35cace008bc18e1d474ba945c1a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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         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()));
127         }
128         Collections.sort(copiedRouteData);
129         listener.onNewRouteData(routeDataLastUpdateMs, copiedRouteData);
130
131         return true;
132     }
133
134     /**
135      * Unregisters the listener so it no longer receives updates about arrival and departure times for its route.
136      *
137      * @param listener
138      * @return true if successful.
139      */
140     protected boolean unregisterRouteDataListener(RouteDataListener listener) {
141         return routeDataListeners.remove(listener);
142     }
143
144     /**
145      * Forced an update to be scheduled immediately.
146      */
147     protected void forceUpdate() {
148         scheduler.execute(this::fetchAndUpdateStopData);
149     }
150
151     private ApiHandler getApiHandler() {
152         return (ApiHandler) getBridge().getHandler();
153     }
154
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");
159             return null;
160         }
161         if (config.getStopId() == null) {
162             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "stopId is not set");
163             return null;
164         }
165         return config;
166     }
167
168     private boolean fetchAndUpdateStopData() {
169         try {
170             ApiHandler apiHandler = getApiHandler();
171             if (apiHandler == null) {
172                 // We must be offline.
173                 return false;
174             }
175             boolean alreadyFetching = !fetchInProgress.compareAndSet(false, true);
176             if (alreadyFetching) {
177                 return false;
178             }
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());
182             URI uri;
183             try {
184                 uri = new URI(url);
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");
189                 return false;
190             }
191             ContentResponse response;
192             try {
193                 response = httpClient.newRequest(uri).send();
194             } catch (InterruptedException | TimeoutException | ExecutionException e) {
195                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
196                 return false;
197             }
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()));
201                 return false;
202             }
203             ObaStopArrivalResponse data = gson.fromJson(response.getContentAsString(), ObaStopArrivalResponse.class);
204             routeDataLastUpdateMs = data.currentTime;
205             updateStatus(ThingStatus.ONLINE);
206
207             Map<String, List<ObaStopArrivalResponse.ArrivalAndDeparture>> copiedRouteData = new HashMap<>();
208             synchronized (routeData) {
209                 routeData.clear();
210                 for (ObaStopArrivalResponse.ArrivalAndDeparture d : data.data.entry.arrivalsAndDepartures) {
211                     routeData.put(d.routeId, Arrays.asList(d));
212                 }
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);
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 }