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.deutschebahn.internal;
15 import java.io.IOException;
16 import java.net.URISyntaxException;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.Date;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.locks.Lock;
27 import java.util.concurrent.locks.ReentrantLock;
28 import java.util.function.Supplier;
30 import javax.xml.bind.JAXBException;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.deutschebahn.internal.filter.AndPredicate;
35 import org.openhab.binding.deutschebahn.internal.filter.FilterParserException;
36 import org.openhab.binding.deutschebahn.internal.filter.FilterScannerException;
37 import org.openhab.binding.deutschebahn.internal.filter.TimetableStopPredicate;
38 import org.openhab.binding.deutschebahn.internal.timetable.TimetableLoader;
39 import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Api;
40 import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ApiFactory;
41 import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
42 import org.openhab.core.io.net.http.HttpUtil;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.Channel;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.ThingTypeUID;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.thing.binding.ThingHandler;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56 import org.xml.sax.SAXException;
59 * The {@link DeutscheBahnTimetableHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Sönke Küper - Initial contribution
65 public class DeutscheBahnTimetableHandler extends BaseBridgeHandler {
68 * Wrapper containing things grouped by their position and calculates the max. required position.
70 private static final class GroupedThings {
72 private int maxPosition = 0;
73 private final Map<Integer, List<Thing>> thingsPerPosition = new HashMap<>();
75 public void addThing(Thing thing) {
77 int position = thing.getConfiguration().as(DeutscheBahnTrainConfiguration.class).position;
78 this.maxPosition = Math.max(this.maxPosition, position);
79 List<Thing> thingsAtPosition = this.thingsPerPosition.get(position);
80 if (thingsAtPosition == null) {
81 thingsAtPosition = new ArrayList<>();
82 this.thingsPerPosition.put(position, thingsAtPosition);
84 thingsAtPosition.add(thing);
89 * Returns the things at the given position.
92 public List<Thing> getThingsAtPosition(int position) {
93 return this.thingsPerPosition.get(position);
97 * Returns the max. configured position.
99 public int getMaxPosition() {
100 return this.maxPosition;
104 private static final long UPDATE_INTERVAL_SECONDS = 30;
106 private final Lock monitor = new ReentrantLock();
107 private @Nullable ScheduledFuture<?> updateJob;
109 private final Logger logger = LoggerFactory.getLogger(DeutscheBahnTimetableHandler.class);
110 private @Nullable TimetableLoader loader;
112 private final TimetablesV1ApiFactory timetablesV1ApiFactory;
114 private final Supplier<Date> currentTimeProvider;
116 private final ScheduledExecutorService executorService;
119 * Creates an new {@link DeutscheBahnTimetableHandler}.
121 public DeutscheBahnTimetableHandler( //
122 final Bridge bridge, //
123 final TimetablesV1ApiFactory timetablesV1ApiFactory, //
124 final Supplier<Date> currentTimeProvider, //
125 @Nullable final ScheduledExecutorService executorService) {
127 this.timetablesV1ApiFactory = timetablesV1ApiFactory;
128 this.currentTimeProvider = currentTimeProvider;
129 this.executorService = executorService == null ? this.scheduler : executorService;
132 private List<TimetableStop> loadTimetable() {
133 final TimetableLoader currentLoader = this.loader;
134 if (currentLoader == null) {
135 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
136 return Collections.emptyList();
140 final List<TimetableStop> stops = currentLoader.getTimetableStops();
141 this.updateStatus(ThingStatus.ONLINE);
143 } catch (final IOException e) {
144 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
145 return Collections.emptyList();
150 * The Bridge-Handler does not handle any commands.
153 public void handleCommand(final ChannelUID channelUID, final Command command) {
157 public void initialize() {
158 final DeutscheBahnTimetableConfiguration config = this.getConfigAs(DeutscheBahnTimetableConfiguration.class);
161 final TimetablesV1Api api = this.timetablesV1ApiFactory.create(config.accessToken, HttpUtil::executeUrl);
163 final TimetableStopFilter stopFilter = config.getTrainFilterFilter();
164 final TimetableStopPredicate additionalFilter = config.getAdditionalFilter();
166 final TimetableStopPredicate combinedFilter;
167 if (additionalFilter == null) {
168 combinedFilter = stopFilter;
170 combinedFilter = new AndPredicate(stopFilter, additionalFilter);
173 final EventType eventSelection = stopFilter == TimetableStopFilter.ARRIVALS ? EventType.ARRIVAL
176 this.loader = new TimetableLoader( //
180 currentTimeProvider, //
182 1); // will be updated on first call
184 this.updateStatus(ThingStatus.UNKNOWN);
186 this.executorService.execute(() -> {
187 this.updateChannels();
190 } catch (FilterScannerException | FilterParserException e) {
191 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
192 } catch (JAXBException | SAXException | URISyntaxException e) {
193 this.logger.error("Error initializing api", e);
194 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
199 public void dispose() {
200 this.stopUpdateJob();
204 * Schedules an job that updates the timetable every 30 seconds.
206 private void restartJob() {
207 this.logger.debug("Restarting jobs for bridge {}", this.getThing().getUID());
210 this.stopUpdateJob();
211 if (this.getThing().getStatus() == ThingStatus.ONLINE) {
212 this.updateJob = this.executorService.scheduleWithFixedDelay(//
213 this::updateChannels, //
215 UPDATE_INTERVAL_SECONDS, //
219 this.logger.debug("Scheduled {} update of deutsche bahn timetable", this.updateJob);
222 this.monitor.unlock();
227 * Stops the update job.
229 private void stopUpdateJob() {
232 final ScheduledFuture<?> job = this.updateJob;
236 this.updateJob = null;
238 this.monitor.unlock();
242 private void updateChannels() {
243 final TimetableLoader currentLoader = this.loader;
244 if (currentLoader == null) {
245 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
248 final GroupedThings groupedThings = this.groupThingsPerPosition();
249 currentLoader.setStopCount(groupedThings.getMaxPosition());
250 final List<TimetableStop> timetableStops = this.loadTimetable();
251 if (timetableStops.isEmpty()) {
252 updateThingsToUndefined(groupedThings);
256 this.logger.debug("Retrieved {} timetable stops.", timetableStops.size());
257 this.updateThings(groupedThings, timetableStops);
261 * No data was retrieved, so update all channel values to undefined.
263 private void updateThingsToUndefined(GroupedThings groupedThings) {
264 for (List<Thing> things : groupedThings.thingsPerPosition.values()) {
265 for (Thing thing : things) {
266 updateChannelsToUndefined(thing);
271 private void updateChannelsToUndefined(Thing thing) {
272 for (Channel channel : thing.getChannels()) {
273 this.updateState(channel.getUID(), UnDefType.UNDEF);
277 private void updateThings(GroupedThings groupedThings, final List<TimetableStop> timetableStops) {
279 for (final TimetableStop stop : timetableStops) {
280 final List<Thing> thingsAtPosition = groupedThings.getThingsAtPosition(position);
282 if (thingsAtPosition != null) {
283 for (Thing thing : thingsAtPosition) {
284 final ThingHandler thingHandler = thing.getHandler();
285 if (thingHandler != null) {
286 assert thingHandler instanceof DeutscheBahnTrainHandler;
287 ((DeutscheBahnTrainHandler) thingHandler).updateChannels(stop);
294 // Update all things to undefined, for which no data was received.
295 while (position <= groupedThings.getMaxPosition()) {
296 final List<Thing> thingsAtPosition = groupedThings.getThingsAtPosition(position);
297 if (thingsAtPosition != null) {
298 for (Thing thing : thingsAtPosition) {
299 updateChannelsToUndefined(thing);
307 * Returns an map containing the things grouped by timetable stop position.
309 private GroupedThings groupThingsPerPosition() {
310 final GroupedThings groupedThings = new GroupedThings();
311 for (Thing child : this.getThing().getThings()) {
312 groupedThings.addThing(child);
314 return groupedThings;
317 private static boolean isTrain(Thing thing) {
318 final ThingTypeUID thingTypeUid = thing.getThingTypeUID();
319 return thingTypeUid.equals(DeutscheBahnBindingConstants.TRAIN_TYPE);