2 * Copyright (c) 2010-2023 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.*;
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;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.function.Function;
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;
46 * The {@link RouteHandler} is responsible for handling commands, which are
47 * sent to one of the channels.
49 * @author Shawn Wilsher - Initial contribution
51 public class RouteHandler extends BaseThingHandler implements RouteDataListener {
53 public static final ThingTypeUID SUPPORTED_THING_TYPE = THING_TYPE_ROUTE;
55 private final Logger logger = LoggerFactory.getLogger(RouteHandler.class);
57 private RouteConfiguration config;
58 private List<ScheduledFuture<?>> scheduledFutures = new CopyOnWriteArrayList<>();
60 public RouteHandler(Thing thing) {
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();
78 logger.warn("Unnknown channel UID {} with comamnd {}", channelUID.getId(), command);
81 logger.debug("The OneBusAway route is read-only and can not handle commands.");
86 public void initialize() {
87 logger.debug("Initializing OneBusAway route stop...");
89 config = loadAndCheckConfiguration();
91 logger.debug("Initialization of OneBusAway route stop failed!");
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);
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge unavailable");
105 public void dispose() {
106 cancelAllScheduledFutures();
107 StopHandler stopHandler = getStopHandler();
108 if (stopHandler != null) {
109 stopHandler.unregisterRouteDataListener(this);
114 public String getRouteId() {
115 return config.getRouteId();
119 public void onNewRouteData(long lastUpdateTime, List<ArrivalAndDeparture> data) {
120 if (data.isEmpty()) {
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);
129 publishChannel(channel.getUID(), now, lastUpdateTime, data);
132 updateStatus(ThingStatus.ONLINE);
135 private StopHandler getStopHandler() {
136 return (StopHandler) getBridge().getHandler();
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");
148 private void cancelAllScheduledFutures() {
149 for (ScheduledFuture<?> future : scheduledFutures) {
150 if (!future.isDone() || !future.isCancelled()) {
154 scheduledFutures = new CopyOnWriteArrayList<>();
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);
166 * Publishes the channel with data and possibly schedules work to update it again when the next event has passed.
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())));
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);
182 switch (channelUID.getId()) {
183 case CHANNEL_ID_ARRIVAL:
184 time = (new Calendar.Builder())
186 (data.predicted ? data.predictedArrivalTime : data.scheduledArrivalTime) - offsetMs)
189 case CHANNEL_ID_DEPARTURE:
190 time = (new Calendar.Builder()).setInstant(
191 (data.predicted ? data.predictedDepartureTime : data.scheduledDepartureTime) - offsetMs)
195 logger.warn("No code to handle publishing to {}", channelUID.getId());
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());
204 updateState(channelUID,
205 new DateTimeType(ZonedDateTime.ofInstant(time.toInstant(), ZoneId.systemDefault())));
207 // Update properties only when we update arrival information. This is not perfect.
208 if (channelUID.getId().equals(CHANNEL_ID_ARRIVAL)) {
209 updatePropertiesFromArrivalAndDeparture(data);
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()) {
217 scheduledFutures.add(scheduler.schedule(() -> {
218 publishChannel(channelUID, Calendar.getInstance(), lastUpdateTime, remaining);
219 }, time.getTimeInMillis() - now.getTimeInMillis(), TimeUnit.MILLISECONDS));
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,
229 scheduleTriggerEvents(channelUID, now, arrivalAndDepartures, new Function<ArrivalAndDeparture, Long>() {
231 public Long apply(ArrivalAndDeparture data) {
232 return data.predicted ? data.predictedDepartureTime : data.scheduledDepartureTime;
237 private void scheduleTriggerEvents(ChannelUID channelUID, Calendar now,
238 List<ArrivalAndDeparture> arrivalAndDepartures, Function<ArrivalAndDeparture, Long> dataPiece,
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)) {
251 // Schedule this trigger
252 scheduledFutures.add(scheduler.schedule(() -> {
253 triggerChannel(channelUID, event);
254 }, cal.getTimeInMillis() - now.getTimeInMillis(), TimeUnit.MILLISECONDS));