]> git.basschouten.com Git - openhab-addons.git/blob
7d8f592f8bf74cde6ec389f7245b0c36fc326bcf
[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.deutschebahn.internal;
14
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Collections;
18 import java.util.Date;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.locks.Lock;
26 import java.util.concurrent.locks.ReentrantLock;
27 import java.util.function.Supplier;
28
29 import javax.xml.bind.JAXBException;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.deutschebahn.internal.filter.AndPredicate;
34 import org.openhab.binding.deutschebahn.internal.filter.FilterParserException;
35 import org.openhab.binding.deutschebahn.internal.filter.FilterScannerException;
36 import org.openhab.binding.deutschebahn.internal.filter.TimetableStopPredicate;
37 import org.openhab.binding.deutschebahn.internal.timetable.TimetableLoader;
38 import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1Api;
39 import org.openhab.binding.deutschebahn.internal.timetable.TimetablesV1ApiFactory;
40 import org.openhab.binding.deutschebahn.internal.timetable.dto.TimetableStop;
41 import org.openhab.core.io.net.http.HttpUtil;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.binding.BaseBridgeHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * The {@link DeutscheBahnTimetableHandler} is responsible for handling commands, which are
58  * sent to one of the channels.
59  *
60  * @author Sönke Küper - Initial contribution
61  */
62 @NonNullByDefault
63 public class DeutscheBahnTimetableHandler extends BaseBridgeHandler {
64
65     /**
66      * Wrapper containing things grouped by their position and calculates the max. required position.
67      */
68     private static final class GroupedThings {
69
70         private int maxPosition = 0;
71         private final Map<Integer, List<Thing>> thingsPerPosition = new HashMap<>();
72
73         public void addThing(Thing thing) {
74             if (isTrain(thing)) {
75                 int position = thing.getConfiguration().as(DeutscheBahnTrainConfiguration.class).position;
76                 this.maxPosition = Math.max(this.maxPosition, position);
77                 List<Thing> thingsAtPosition = this.thingsPerPosition.get(position);
78                 if (thingsAtPosition == null) {
79                     thingsAtPosition = new ArrayList<>();
80                     this.thingsPerPosition.put(position, thingsAtPosition);
81                 }
82                 thingsAtPosition.add(thing);
83             }
84         }
85
86         /**
87          * Returns the things at the given position.
88          */
89         @Nullable
90         public List<Thing> getThingsAtPosition(int position) {
91             return this.thingsPerPosition.get(position);
92         }
93
94         /**
95          * Returns the max. configured position.
96          */
97         public int getMaxPosition() {
98             return this.maxPosition;
99         }
100     }
101
102     private static final long UPDATE_INTERVAL_SECONDS = 30;
103
104     private final Lock monitor = new ReentrantLock();
105     private @Nullable ScheduledFuture<?> updateJob;
106
107     private final Logger logger = LoggerFactory.getLogger(DeutscheBahnTimetableHandler.class);
108     private @Nullable TimetableLoader loader;
109
110     private final TimetablesV1ApiFactory timetablesV1ApiFactory;
111
112     private final Supplier<Date> currentTimeProvider;
113
114     private final ScheduledExecutorService executorService;
115
116     /**
117      * Creates a new {@link DeutscheBahnTimetableHandler}.
118      */
119     public DeutscheBahnTimetableHandler( //
120             final Bridge bridge, //
121             final TimetablesV1ApiFactory timetablesV1ApiFactory, //
122             final Supplier<Date> currentTimeProvider, //
123             @Nullable final ScheduledExecutorService executorService) {
124         super(bridge);
125         this.timetablesV1ApiFactory = timetablesV1ApiFactory;
126         this.currentTimeProvider = currentTimeProvider;
127         this.executorService = executorService == null ? this.scheduler : executorService;
128     }
129
130     private List<TimetableStop> loadTimetable(TimetableLoader currentLoader) {
131         try {
132             final List<TimetableStop> stops = currentLoader.getTimetableStops();
133             this.updateStatus(ThingStatus.ONLINE);
134             return stops;
135         } catch (final IOException e) {
136             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
137             return Collections.emptyList();
138         }
139     }
140
141     /**
142      * The Bridge-Handler does not handle any commands.
143      */
144     @Override
145     public void handleCommand(final ChannelUID channelUID, final Command command) {
146     }
147
148     @Override
149     public void initialize() {
150         final DeutscheBahnTimetableConfiguration config = this.getConfigAs(DeutscheBahnTimetableConfiguration.class);
151
152         try {
153             final TimetablesV1Api api = this.timetablesV1ApiFactory.create( //
154                     config.clientId, //
155                     config.clientSecret, //
156                     HttpUtil::executeUrl //
157             );
158
159             final TimetableStopFilter stopFilter = config.getTrainFilterFilter();
160             final TimetableStopPredicate additionalFilter = config.getAdditionalFilter();
161
162             final TimetableStopPredicate combinedFilter;
163             if (additionalFilter == null) {
164                 combinedFilter = stopFilter;
165             } else {
166                 combinedFilter = new AndPredicate(stopFilter, additionalFilter);
167             }
168
169             final EventType eventSelection = stopFilter == TimetableStopFilter.ARRIVALS ? EventType.ARRIVAL
170                     : EventType.DEPARTURE;
171
172             this.loader = new TimetableLoader( //
173                     api, //
174                     combinedFilter, //
175                     eventSelection, //
176                     currentTimeProvider, //
177                     config.evaNo, //
178                     1); // will be updated on first call
179
180             this.updateStatus(ThingStatus.UNKNOWN);
181
182             this.executorService.execute(() -> {
183                 this.updateChannels();
184                 this.restartJob();
185             });
186         } catch (FilterScannerException | FilterParserException e) {
187             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
188         } catch (JAXBException e) {
189             this.logger.error("Error initializing api", e);
190             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
191         }
192     }
193
194     @Override
195     public void dispose() {
196         this.stopUpdateJob();
197     }
198
199     /**
200      * Schedules a job that updates the timetable every 30 seconds.
201      */
202     private void restartJob() {
203         this.logger.debug("Restarting jobs for bridge {}", this.getThing().getUID());
204         this.monitor.lock();
205         try {
206             this.stopUpdateJob();
207             if (this.getThing().getStatus() == ThingStatus.ONLINE) {
208                 this.updateJob = this.executorService.scheduleWithFixedDelay(//
209                         this::updateChannels, //
210                         0L, //
211                         UPDATE_INTERVAL_SECONDS, //
212                         TimeUnit.SECONDS //
213                 );
214
215                 this.logger.debug("Scheduled {} update of deutsche bahn timetable", this.updateJob);
216             }
217         } finally {
218             this.monitor.unlock();
219         }
220     }
221
222     /**
223      * Stops the update job.
224      */
225     private void stopUpdateJob() {
226         this.monitor.lock();
227         try {
228             final ScheduledFuture<?> job = this.updateJob;
229             if (job != null) {
230                 job.cancel(true);
231             }
232             this.updateJob = null;
233         } finally {
234             this.monitor.unlock();
235         }
236     }
237
238     private void updateChannels() {
239         final TimetableLoader currentLoader = this.loader;
240         if (currentLoader == null) {
241             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
242             return;
243         }
244         final GroupedThings groupedThings = this.groupThingsPerPosition();
245         currentLoader.setStopCount(groupedThings.getMaxPosition());
246         final List<TimetableStop> timetableStops = this.loadTimetable(currentLoader);
247         if (timetableStops.isEmpty()) {
248             updateThingsToUndefined(groupedThings);
249             return;
250         }
251
252         this.logger.debug("Retrieved {} timetable stops.", timetableStops.size());
253         this.updateThings(groupedThings, timetableStops);
254     }
255
256     /**
257      * No data was retrieved, so update all channel values to undefined.
258      */
259     private void updateThingsToUndefined(GroupedThings groupedThings) {
260         for (List<Thing> things : groupedThings.thingsPerPosition.values()) {
261             for (Thing thing : things) {
262                 updateChannelsToUndefined(thing);
263             }
264         }
265     }
266
267     private void updateChannelsToUndefined(Thing thing) {
268         for (Channel channel : thing.getChannels()) {
269             this.updateState(channel.getUID(), UnDefType.UNDEF);
270         }
271     }
272
273     private void updateThings(GroupedThings groupedThings, final List<TimetableStop> timetableStops) {
274         int position = 1;
275         for (final TimetableStop stop : timetableStops) {
276             final List<Thing> thingsAtPosition = groupedThings.getThingsAtPosition(position);
277
278             if (thingsAtPosition != null) {
279                 for (Thing thing : thingsAtPosition) {
280                     final ThingHandler thingHandler = thing.getHandler();
281                     if (thingHandler != null) {
282                         assert thingHandler instanceof DeutscheBahnTrainHandler;
283                         ((DeutscheBahnTrainHandler) thingHandler).updateChannels(stop);
284                     }
285                 }
286             }
287             position++;
288         }
289
290         // Update all things to undefined, for which no data was received.
291         while (position <= groupedThings.getMaxPosition()) {
292             final List<Thing> thingsAtPosition = groupedThings.getThingsAtPosition(position);
293             if (thingsAtPosition != null) {
294                 for (Thing thing : thingsAtPosition) {
295                     updateChannelsToUndefined(thing);
296                 }
297             }
298             position++;
299         }
300     }
301
302     /**
303      * Returns a map containing the things grouped by timetable stop position.
304      */
305     private GroupedThings groupThingsPerPosition() {
306         final GroupedThings groupedThings = new GroupedThings();
307         for (Thing child : this.getThing().getThings()) {
308             groupedThings.addThing(child);
309         }
310         return groupedThings;
311     }
312
313     private static boolean isTrain(Thing thing) {
314         final ThingTypeUID thingTypeUid = thing.getThingTypeUID();
315         return thingTypeUid.equals(DeutscheBahnBindingConstants.TRAIN_TYPE);
316     }
317 }