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