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