]> git.basschouten.com Git - openhab-addons.git/blob
439859c569f9d9fe83011e192fff303f6e29634b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.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;
22 import java.util.Map;
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;
29
30 import javax.xml.bind.JAXBException;
31
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;
57
58 /**
59  * The {@link DeutscheBahnTimetableHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
62  * @author Sönke Küper - Initial contribution
63  */
64 @NonNullByDefault
65 public class DeutscheBahnTimetableHandler extends BaseBridgeHandler {
66
67     /**
68      * Wrapper containing things grouped by their position and calculates the max. required position.
69      */
70     private static final class GroupedThings {
71
72         private int maxPosition = 0;
73         private final Map<Integer, List<Thing>> thingsPerPosition = new HashMap<>();
74
75         public void addThing(Thing thing) {
76             if (isTrain(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);
83                 }
84                 thingsAtPosition.add(thing);
85             }
86         }
87
88         /**
89          * Returns the things at the given position.
90          */
91         @Nullable
92         public List<Thing> getThingsAtPosition(int position) {
93             return this.thingsPerPosition.get(position);
94         }
95
96         /**
97          * Returns the max. configured position.
98          */
99         public int getMaxPosition() {
100             return this.maxPosition;
101         }
102     }
103
104     private static final long UPDATE_INTERVAL_SECONDS = 30;
105
106     private final Lock monitor = new ReentrantLock();
107     private @Nullable ScheduledFuture<?> updateJob;
108
109     private final Logger logger = LoggerFactory.getLogger(DeutscheBahnTimetableHandler.class);
110     private @Nullable TimetableLoader loader;
111
112     private final TimetablesV1ApiFactory timetablesV1ApiFactory;
113
114     private final Supplier<Date> currentTimeProvider;
115
116     private final ScheduledExecutorService executorService;
117
118     /**
119      * Creates an new {@link DeutscheBahnTimetableHandler}.
120      */
121     public DeutscheBahnTimetableHandler( //
122             final Bridge bridge, //
123             final TimetablesV1ApiFactory timetablesV1ApiFactory, //
124             final Supplier<Date> currentTimeProvider, //
125             @Nullable final ScheduledExecutorService executorService) {
126         super(bridge);
127         this.timetablesV1ApiFactory = timetablesV1ApiFactory;
128         this.currentTimeProvider = currentTimeProvider;
129         this.executorService = executorService == null ? this.scheduler : executorService;
130     }
131
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();
137         }
138
139         try {
140             final List<TimetableStop> stops = currentLoader.getTimetableStops();
141             this.updateStatus(ThingStatus.ONLINE);
142             return stops;
143         } catch (final IOException e) {
144             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
145             return Collections.emptyList();
146         }
147     }
148
149     /**
150      * The Bridge-Handler does not handle any commands.
151      */
152     @Override
153     public void handleCommand(final ChannelUID channelUID, final Command command) {
154     }
155
156     @Override
157     public void initialize() {
158         final DeutscheBahnTimetableConfiguration config = this.getConfigAs(DeutscheBahnTimetableConfiguration.class);
159
160         try {
161             final TimetablesV1Api api = this.timetablesV1ApiFactory.create(config.accessToken, HttpUtil::executeUrl);
162
163             final TimetableStopFilter stopFilter = config.getTrainFilterFilter();
164             final TimetableStopPredicate additionalFilter = config.getAdditionalFilter();
165
166             final TimetableStopPredicate combinedFilter;
167             if (additionalFilter == null) {
168                 combinedFilter = stopFilter;
169             } else {
170                 combinedFilter = new AndPredicate(stopFilter, additionalFilter);
171             }
172
173             final EventType eventSelection = stopFilter == TimetableStopFilter.ARRIVALS ? EventType.ARRIVAL
174                     : EventType.ARRIVAL;
175
176             this.loader = new TimetableLoader( //
177                     api, //
178                     combinedFilter, //
179                     eventSelection, //
180                     currentTimeProvider, //
181                     config.evaNo, //
182                     1); // will be updated on first call
183
184             this.updateStatus(ThingStatus.UNKNOWN);
185
186             this.executorService.execute(() -> {
187                 this.updateChannels();
188                 this.restartJob();
189             });
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());
195         }
196     }
197
198     @Override
199     public void dispose() {
200         this.stopUpdateJob();
201     }
202
203     /**
204      * Schedules an job that updates the timetable every 30 seconds.
205      */
206     private void restartJob() {
207         this.logger.debug("Restarting jobs for bridge {}", this.getThing().getUID());
208         this.monitor.lock();
209         try {
210             this.stopUpdateJob();
211             if (this.getThing().getStatus() == ThingStatus.ONLINE) {
212                 this.updateJob = this.executorService.scheduleWithFixedDelay(//
213                         this::updateChannels, //
214                         0L, //
215                         UPDATE_INTERVAL_SECONDS, //
216                         TimeUnit.SECONDS //
217                 );
218
219                 this.logger.debug("Scheduled {} update of deutsche bahn timetable", this.updateJob);
220             }
221         } finally {
222             this.monitor.unlock();
223         }
224     }
225
226     /**
227      * Stops the update job.
228      */
229     private void stopUpdateJob() {
230         this.monitor.lock();
231         try {
232             final ScheduledFuture<?> job = this.updateJob;
233             if (job != null) {
234                 job.cancel(true);
235             }
236             this.updateJob = null;
237         } finally {
238             this.monitor.unlock();
239         }
240     }
241
242     private void updateChannels() {
243         final TimetableLoader currentLoader = this.loader;
244         if (currentLoader == null) {
245             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
246             return;
247         }
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);
253             return;
254         }
255
256         this.logger.debug("Retrieved {} timetable stops.", timetableStops.size());
257         this.updateThings(groupedThings, timetableStops);
258     }
259
260     /**
261      * No data was retrieved, so update all channel values to undefined.
262      */
263     private void updateThingsToUndefined(GroupedThings groupedThings) {
264         for (List<Thing> things : groupedThings.thingsPerPosition.values()) {
265             for (Thing thing : things) {
266                 updateChannelsToUndefined(thing);
267             }
268         }
269     }
270
271     private void updateChannelsToUndefined(Thing thing) {
272         for (Channel channel : thing.getChannels()) {
273             this.updateState(channel.getUID(), UnDefType.UNDEF);
274         }
275     }
276
277     private void updateThings(GroupedThings groupedThings, final List<TimetableStop> timetableStops) {
278         int position = 1;
279         for (final TimetableStop stop : timetableStops) {
280             final List<Thing> thingsAtPosition = groupedThings.getThingsAtPosition(position);
281
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);
288                     }
289                 }
290             }
291             position++;
292         }
293
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);
300                 }
301             }
302             position++;
303         }
304     }
305
306     /**
307      * Returns an map containing the things grouped by timetable stop position.
308      */
309     private GroupedThings groupThingsPerPosition() {
310         final GroupedThings groupedThings = new GroupedThings();
311         for (Thing child : this.getThing().getThings()) {
312             groupedThings.addThing(child);
313         }
314         return groupedThings;
315     }
316
317     private static boolean isTrain(Thing thing) {
318         final ThingTypeUID thingTypeUid = thing.getThingTypeUID();
319         return thingTypeUid.equals(DeutscheBahnBindingConstants.TRAIN_TYPE);
320     }
321 }