]> git.basschouten.com Git - openhab-addons.git/blob
98554e2963b7076ea0e607aa7dce1bbfca7f66ce
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.*;
16
17 import java.time.Instant;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.util.Calendar;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.function.Function;
27
28 import org.openhab.binding.onebusaway.internal.config.ChannelConfig;
29 import org.openhab.binding.onebusaway.internal.config.RouteConfiguration;
30 import org.openhab.binding.onebusaway.internal.handler.ObaStopArrivalResponse.ArrivalAndDeparture;
31 import org.openhab.core.library.types.DateTimeType;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.thing.type.ChannelKind;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The {@link RouteHandler} is responsible for handling commands, which are
47  * sent to one of the channels.
48  *
49  * @author Shawn Wilsher - Initial contribution
50  */
51 public class RouteHandler extends BaseThingHandler implements RouteDataListener {
52
53     public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_ROUTE;
54
55     private final Logger logger = LoggerFactory.getLogger(RouteHandler.class);
56
57     private RouteConfiguration config;
58     private List<ScheduledFuture<?>> scheduledFutures = new CopyOnWriteArrayList<>();
59
60     public RouteHandler(Thing thing) {
61         super(thing);
62     }
63
64     @Override
65     public void handleCommand(ChannelUID channelUID, Command command) {
66         if (RefreshType.REFRESH == command) {
67             logger.debug("Refreshing {}...", channelUID);
68             switch (channelUID.getId()) {
69                 case CHANNEL_ID_ARRIVAL:
70                 case CHANNEL_ID_DEPARTURE:
71                 case CHANNEL_ID_UPDATE:
72                     StopHandler stopHandler = getStopHandler();
73                     if (stopHandler != null) {
74                         stopHandler.forceUpdate();
75                     }
76                     break;
77                 default:
78                     logger.warn("Unnknown channel UID {} with comamnd {}", channelUID.getId(), command);
79             }
80         } else {
81             logger.debug("The OneBusAway route is read-only and can not handle commands.");
82         }
83     }
84
85     @Override
86     public void initialize() {
87         logger.debug("Initializing OneBusAway route stop...");
88
89         config = loadAndCheckConfiguration();
90         if (config == null) {
91             logger.debug("Initialization of OneBusAway route stop failed!");
92             return;
93         }
94         StopHandler stopHandler = getStopHandler();
95         if (stopHandler != null) {
96             // We will be marked as ONLINE when we get data in our callback, which should be immediately because the
97             // StopHandler, our bridge, won't be marked as online until it has data itself.
98             stopHandler.registerRouteDataListener(this);
99         } else {
100             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge unavailable");
101         }
102     }
103
104     @Override
105     public void dispose() {
106         cancelAllScheduledFutures();
107         StopHandler stopHandler = getStopHandler();
108         if (stopHandler != null) {
109             stopHandler.unregisterRouteDataListener(this);
110         }
111     }
112
113     @Override
114     public String getRouteId() {
115         return config.getRouteId();
116     }
117
118     @Override
119     public void onNewRouteData(long lastUpdateTime, List<ArrivalAndDeparture> data) {
120         if (data.isEmpty()) {
121             return;
122         }
123         // Publish to all of our linked channels.
124         Calendar now = Calendar.getInstance();
125         for (Channel channel : getThing().getChannels()) {
126             if (channel.getKind() == ChannelKind.TRIGGER) {
127                 scheduleTriggerEvents(channel.getUID(), now, data);
128             } else {
129                 publishChannel(channel.getUID(), now, lastUpdateTime, data);
130             }
131         }
132         updateStatus(ThingStatus.ONLINE);
133     }
134
135     private StopHandler getStopHandler() {
136         return (StopHandler) getBridge().getHandler();
137     }
138
139     private RouteConfiguration loadAndCheckConfiguration() {
140         RouteConfiguration config = getConfigAs(RouteConfiguration.class);
141         if (config.getRouteId() == null) {
142             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "routeId is not set");
143             return null;
144         }
145         return config;
146     }
147
148     private void cancelAllScheduledFutures() {
149         for (ScheduledFuture<?> future : scheduledFutures) {
150             if (!future.isDone() || !future.isCancelled()) {
151                 future.cancel(true);
152             }
153         }
154         scheduledFutures = new CopyOnWriteArrayList<>();
155     }
156
157     private void updatePropertiesFromArrivalAndDeparture(ArrivalAndDeparture data) {
158         Map<String, String> props = editProperties();
159         props.put(ROUTE_PROPERTY_HEADSIGN, data.tripHeadsign);
160         props.put(ROUTE_PROPERTY_LONG_NAME, data.routeLongName);
161         props.put(ROUTE_PROPERTY_SHORT_NAME, data.routeShortName);
162         updateProperties(props);
163     }
164
165     /**
166      * Publishes the channel with data and possibly schedules work to update it again when the next event has passed.
167      */
168     private void publishChannel(ChannelUID channelUID, Calendar now, long lastUpdateTime,
169             List<ArrivalAndDeparture> arrivalAndDepartures) {
170         if (channelUID.getId().equals(CHANNEL_ID_UPDATE)) {
171             updateState(channelUID, new DateTimeType(
172                     ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastUpdateTime), ZoneId.systemDefault())));
173             return;
174         }
175
176         ChannelConfig channelConfig = getThing().getChannel(channelUID.getId()).getConfiguration()
177                 .as(ChannelConfig.class);
178         long offsetMs = TimeUnit.SECONDS.toMillis(channelConfig.getOffset());
179         for (int i = 0; i < arrivalAndDepartures.size(); i++) {
180             ArrivalAndDeparture data = arrivalAndDepartures.get(i);
181             Calendar time;
182             switch (channelUID.getId()) {
183                 case CHANNEL_ID_ARRIVAL:
184                     time = (new Calendar.Builder())
185                             .setInstant(
186                                     (data.predicted ? data.predictedArrivalTime : data.scheduledArrivalTime) - offsetMs)
187                             .build();
188                     break;
189                 case CHANNEL_ID_DEPARTURE:
190                     time = (new Calendar.Builder()).setInstant(
191                             (data.predicted ? data.predictedDepartureTime : data.scheduledDepartureTime) - offsetMs)
192                             .build();
193                     break;
194                 default:
195                     logger.warn("No code to handle publishing to {}", channelUID.getId());
196                     return;
197             }
198
199             // Do not publish this if it's already passed.
200             if (time.before(now)) {
201                 logger.debug("Not notifying {} because it is in the past.", channelUID.getId());
202                 continue;
203             }
204             updateState(channelUID,
205                     new DateTimeType(ZonedDateTime.ofInstant(time.toInstant(), ZoneId.systemDefault())));
206
207             // Update properties only when we update arrival information. This is not perfect.
208             if (channelUID.getId().equals(CHANNEL_ID_ARRIVAL)) {
209                 updatePropertiesFromArrivalAndDeparture(data);
210             }
211
212             // Schedule updates in the future. These may be canceled if we are notified about new data in the future.
213             List<ArrivalAndDeparture> remaining = arrivalAndDepartures.subList(i + 1, arrivalAndDepartures.size());
214             if (remaining.isEmpty()) {
215                 return;
216             }
217             scheduledFutures.add(scheduler.schedule(() -> {
218                 publishChannel(channelUID, Calendar.getInstance(), lastUpdateTime, remaining);
219             }, time.getTimeInMillis() - now.getTimeInMillis(), TimeUnit.MILLISECONDS));
220             return;
221         }
222     }
223
224     private void scheduleTriggerEvents(ChannelUID channelUID, Calendar now,
225             List<ArrivalAndDeparture> arrivalAndDepartures) {
226         scheduleTriggerEvents(channelUID, now, arrivalAndDepartures,
227                 (ArrivalAndDeparture data) -> data.predicted ? data.predictedArrivalTime : data.scheduledArrivalTime,
228                 EVENT_ARRIVAL);
229         scheduleTriggerEvents(channelUID, now, arrivalAndDepartures, new Function<ArrivalAndDeparture, Long>() {
230             @Override
231             public Long apply(ArrivalAndDeparture data) {
232                 return data.predicted ? data.predictedDepartureTime : data.scheduledDepartureTime;
233             }
234         }, EVENT_DEPARTURE);
235     }
236
237     private void scheduleTriggerEvents(ChannelUID channelUID, Calendar now,
238             List<ArrivalAndDeparture> arrivalAndDepartures, Function<ArrivalAndDeparture, Long> dataPiece,
239             String event) {
240         ChannelConfig channelConfig = getThing().getChannel(channelUID.getId()).getConfiguration()
241                 .as(ChannelConfig.class);
242         long offsetMs = TimeUnit.SECONDS.toMillis(channelConfig.getOffset());
243         for (ArrivalAndDeparture data : arrivalAndDepartures) {
244             long time = dataPiece.apply(data);
245             // Do not schedule this if it's already passed.
246             Calendar cal = (new Calendar.Builder()).setInstant(time - offsetMs).build();
247             if (cal.before(now)) {
248                 continue;
249             }
250
251             // Schedule this trigger
252             scheduledFutures.add(scheduler.schedule(() -> {
253                 triggerChannel(channelUID, event);
254             }, cal.getTimeInMillis() - now.getTimeInMillis(), TimeUnit.MILLISECONDS));
255         }
256     }
257 }