]> git.basschouten.com Git - openhab-addons.git/blob
40c346cee858c5ac5050b3f36be1549949db277f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.icalendar.internal.handler;
14
15 import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.*;
16
17 import java.io.File;
18 import java.io.FileInputStream;
19 import java.io.IOException;
20 import java.net.URI;
21 import java.net.URISyntaxException;
22 import java.time.Instant;
23 import java.time.ZoneId;
24 import java.util.List;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.openhab.binding.icalendar.internal.config.ICalendarConfiguration;
32 import org.openhab.binding.icalendar.internal.handler.PullJob.CalendarUpdateListener;
33 import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
34 import org.openhab.binding.icalendar.internal.logic.CalendarException;
35 import org.openhab.binding.icalendar.internal.logic.CommandTag;
36 import org.openhab.binding.icalendar.internal.logic.CommandTagType;
37 import org.openhab.binding.icalendar.internal.logic.Event;
38 import org.openhab.core.config.core.ConfigConstants;
39 import org.openhab.core.events.EventPublisher;
40 import org.openhab.core.items.events.ItemEventFactory;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.StringType;
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.binding.BaseThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * The {@link ICalendarHandler} is responsible for handling commands, which are
57  * sent to one of the channels.
58  *
59  * @author Michael Wodniok - Initial contribution
60  * @author Andrew Fiddian-Green - Support for Command Tags embedded in the Event description
61  */
62 @NonNullByDefault
63 public class ICalendarHandler extends BaseThingHandler implements CalendarUpdateListener {
64
65     private final File calendarFile;
66     private @Nullable ICalendarConfiguration configuration;
67     private final EventPublisher eventPublisherCallback;
68     private final HttpClient httpClient;
69     private final Logger logger = LoggerFactory.getLogger(ICalendarHandler.class);
70     private @Nullable ScheduledFuture<?> pullJobFuture;
71     private @Nullable AbstractPresentableCalendar runtimeCalendar;
72     private @Nullable ScheduledFuture<?> updateJobFuture;
73     private Instant updateStatesLastCalledTime;
74
75     public ICalendarHandler(Thing thing, HttpClient httpClient, EventPublisher eventPublisher) {
76         super(thing);
77         this.httpClient = httpClient;
78         calendarFile = new File(ConfigConstants.getUserDataFolder() + File.separator
79                 + getThing().getUID().getAsString().replaceAll("[<>:\"/\\\\|?*]", "_") + ".ical");
80         eventPublisherCallback = eventPublisher;
81         updateStatesLastCalledTime = Instant.now();
82     }
83
84     @Override
85     public void dispose() {
86         final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
87         if (currentUpdateJobFuture != null) {
88             currentUpdateJobFuture.cancel(true);
89         }
90         final ScheduledFuture<?> currentPullJobFuture = pullJobFuture;
91         if (currentPullJobFuture != null) {
92             currentPullJobFuture.cancel(true);
93         }
94     }
95
96     @Override
97     public void handleCommand(ChannelUID channelUID, Command command) {
98         switch (channelUID.getId()) {
99             case CHANNEL_CURRENT_EVENT_PRESENT:
100             case CHANNEL_CURRENT_EVENT_TITLE:
101             case CHANNEL_CURRENT_EVENT_START:
102             case CHANNEL_CURRENT_EVENT_END:
103             case CHANNEL_NEXT_EVENT_TITLE:
104             case CHANNEL_NEXT_EVENT_START:
105             case CHANNEL_NEXT_EVENT_END:
106                 if (command instanceof RefreshType) {
107                     updateStates();
108                 }
109                 break;
110             default:
111                 logger.warn("Framework sent command to unknown channel with id '{}'", channelUID.getId());
112         }
113     }
114
115     @Override
116     public void initialize() {
117         updateStatus(ThingStatus.UNKNOWN);
118
119         final ICalendarConfiguration currentConfiguration = getConfigAs(ICalendarConfiguration.class);
120         configuration = currentConfiguration;
121
122         if ((currentConfiguration.username == null && currentConfiguration.password != null)
123                 || (currentConfiguration.username != null && currentConfiguration.password == null)) {
124             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
125                     "Only one of username and password was set. This is invalid.");
126             return;
127         }
128
129         PullJob regularPull;
130         try {
131             regularPull = new PullJob(httpClient, new URI(currentConfiguration.url), currentConfiguration.username,
132                     currentConfiguration.password, calendarFile, currentConfiguration.maxSize * 1048576, this);
133         } catch (URISyntaxException e) {
134             logger.warn(
135                     "The URI '{}' for downloading the calendar contains syntax errors. This will result in no downloads/updates.",
136                     currentConfiguration.url, e);
137             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
138             return;
139         }
140
141         if (calendarFile.isFile()) {
142             if (reloadCalendar()) {
143                 updateStatus(ThingStatus.ONLINE);
144                 updateStates();
145                 rescheduleCalendarStateUpdate();
146             } else {
147                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
148                         "The calendar seems to be configured correctly, but the local copy of calendar could not be loaded.");
149             }
150             pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, currentConfiguration.refreshTime.longValue(),
151                     currentConfiguration.refreshTime.longValue(), TimeUnit.MINUTES);
152         } else {
153             updateStatus(ThingStatus.OFFLINE);
154             logger.debug(
155                     "The calendar is currently offline as no local copy exists. It will go online as soon as a valid valid calendar is retrieved.");
156             pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, 0,
157                     currentConfiguration.refreshTime.longValue(), TimeUnit.MINUTES);
158         }
159     }
160
161     @Override
162     public void onCalendarUpdated() {
163         if (reloadCalendar()) {
164             updateStates();
165         } else {
166             logger.trace("Calendar was updated, but loading failed.");
167         }
168     }
169
170     private void executeEventCommands(List<Event> events, CommandTagType execTime) {
171         // no begun or ended events => exit quietly as there is nothing to do
172         if (events.isEmpty()) {
173             return;
174         }
175
176         // prevent potential synchronization issues (MVN null pointer warnings) in "configuration"
177         @Nullable
178         ICalendarConfiguration syncConfiguration = configuration;
179         if (syncConfiguration == null) {
180             logger.debug("Configuration not instantiated!");
181             return;
182         }
183         // loop through all events in the list
184         for (Event event : events) {
185
186             // loop through all command tags in the event
187             for (CommandTag cmdTag : event.commandTags) {
188
189                 // only process the BEGIN resp. END tags
190                 if (cmdTag.getTagType() != execTime) {
191                     continue;
192                 }
193                 if (!cmdTag.isAuthorized(syncConfiguration.authorizationCode)) {
194                     logger.warn("Event: {}, Command Tag: {} => Command not authorized!", event.title,
195                             cmdTag.getFullTag());
196                     continue;
197                 }
198
199                 final Command cmdState = cmdTag.getCommand();
200                 if (cmdState == null) {
201                     logger.warn("Event: {}, Command Tag: {} => Error creating Command State!", event.title,
202                             cmdTag.getFullTag());
203                     continue;
204                 }
205
206                 // (try to) execute the command
207                 try {
208                     eventPublisherCallback.post(ItemEventFactory.createCommandEvent(cmdTag.getItemName(), cmdState));
209                     if (logger.isDebugEnabled()) {
210                         String cmdType = cmdState.getClass().toString();
211                         int index = cmdType.lastIndexOf(".") + 1;
212                         if ((index > 0) && (index < cmdType.length())) {
213                             cmdType = cmdType.substring(index);
214                         }
215                         logger.debug("Event: {}, Command Tag: {} => {}.postUpdate({}: {})", event.title,
216                                 cmdTag.getFullTag(), cmdTag.getItemName(), cmdType, cmdState);
217                     }
218                 } catch (IllegalArgumentException | IllegalStateException e) {
219                     logger.warn("Event: {}, Command Tag: {} => Unable to push command to target item!", event.title,
220                             cmdTag.getFullTag());
221                     logger.debug("Exception occured while pushing to item!", e);
222                 }
223             }
224         }
225     }
226
227     /**
228      * Reloads the calendar from local ical-file. Replaces the class internal calendar - if loading succeeds. Else
229      * logging details at warn-level logger.
230      *
231      * @return Whether the calendar was loaded successfully.
232      */
233     private boolean reloadCalendar() {
234         if (!calendarFile.isFile()) {
235             logger.info("Local file for reloading calendar is missing.");
236             return false;
237         }
238         final ICalendarConfiguration config = configuration;
239         if (config == null) {
240             logger.warn("Can't reload calendar when configuration is missing.");
241             return false;
242         }
243         try (final FileInputStream fileStream = new FileInputStream(calendarFile)) {
244             final AbstractPresentableCalendar calendar = AbstractPresentableCalendar.create(fileStream);
245             runtimeCalendar = calendar;
246             rescheduleCalendarStateUpdate();
247         } catch (IOException | CalendarException e) {
248             logger.warn("Loading calendar failed: {}", e.getMessage());
249             return false;
250         }
251         return true;
252     }
253
254     /**
255      * Reschedules the next update of the states.
256      */
257     private void rescheduleCalendarStateUpdate() {
258         final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
259         if (currentUpdateJobFuture != null) {
260             if (!(currentUpdateJobFuture.isCancelled() || currentUpdateJobFuture.isDone())) {
261                 currentUpdateJobFuture.cancel(true);
262             }
263             updateJobFuture = null;
264         }
265         final AbstractPresentableCalendar currentCalendar = runtimeCalendar;
266         if (currentCalendar == null) {
267             return;
268         }
269         final Instant now = Instant.now();
270         if (currentCalendar.isEventPresent(now)) {
271             final Event currentEvent = currentCalendar.getCurrentEvent(now);
272             if (currentEvent == null) {
273                 logger.debug(
274                         "Could not schedule next update of states, due to unexpected behaviour of calendar implementation.");
275                 return;
276             }
277             updateJobFuture = scheduler.schedule(() -> {
278                 ICalendarHandler.this.updateStates();
279                 ICalendarHandler.this.rescheduleCalendarStateUpdate();
280             }, currentEvent.end.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
281         } else {
282             final Event nextEvent = currentCalendar.getNextEvent(now);
283             final ICalendarConfiguration currentConfig = this.configuration;
284             if (currentConfig == null) {
285                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
286                         "Something is broken, the configuration is not available.");
287                 return;
288             }
289             if (nextEvent == null) {
290                 updateJobFuture = scheduler.schedule(() -> {
291                     ICalendarHandler.this.rescheduleCalendarStateUpdate();
292                 }, 1L, TimeUnit.DAYS);
293             } else {
294                 updateJobFuture = scheduler.schedule(() -> {
295                     ICalendarHandler.this.updateStates();
296                     ICalendarHandler.this.rescheduleCalendarStateUpdate();
297                 }, nextEvent.start.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
298             }
299         }
300     }
301
302     /**
303      * Updates the states of the Thing and its channels.
304      */
305     private void updateStates() {
306         final AbstractPresentableCalendar calendar = runtimeCalendar;
307         if (calendar == null) {
308             updateStatus(ThingStatus.OFFLINE);
309         } else {
310             updateStatus(ThingStatus.ONLINE);
311
312             final Instant now = Instant.now();
313             if (calendar.isEventPresent(now)) {
314                 updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.ON);
315                 final Event currentEvent = calendar.getCurrentEvent(now);
316                 if (currentEvent == null) {
317                     logger.warn("Unexpected inconsistency of internal API. Not Updating event details.");
318                 } else {
319                     updateState(CHANNEL_CURRENT_EVENT_TITLE, new StringType(currentEvent.title));
320                     updateState(CHANNEL_CURRENT_EVENT_START,
321                             new DateTimeType(currentEvent.start.atZone(ZoneId.systemDefault())));
322                     updateState(CHANNEL_CURRENT_EVENT_END,
323                             new DateTimeType(currentEvent.end.atZone(ZoneId.systemDefault())));
324                 }
325             } else {
326                 updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.OFF);
327                 updateState(CHANNEL_CURRENT_EVENT_TITLE, UnDefType.UNDEF);
328                 updateState(CHANNEL_CURRENT_EVENT_START, UnDefType.UNDEF);
329                 updateState(CHANNEL_CURRENT_EVENT_END, UnDefType.UNDEF);
330             }
331
332             final Event nextEvent = calendar.getNextEvent(now);
333             if (nextEvent != null) {
334                 updateState(CHANNEL_NEXT_EVENT_TITLE, new StringType(nextEvent.title));
335                 updateState(CHANNEL_NEXT_EVENT_START, new DateTimeType(nextEvent.start.atZone(ZoneId.systemDefault())));
336                 updateState(CHANNEL_NEXT_EVENT_END, new DateTimeType(nextEvent.end.atZone(ZoneId.systemDefault())));
337             } else {
338                 updateState(CHANNEL_NEXT_EVENT_TITLE, UnDefType.UNDEF);
339                 updateState(CHANNEL_NEXT_EVENT_START, UnDefType.UNDEF);
340                 updateState(CHANNEL_NEXT_EVENT_END, UnDefType.UNDEF);
341             }
342
343             // process all Command Tags in all Calendar Events which ENDED since updateStates was last called
344             // the END Event tags must be processed before the BEGIN ones
345             executeEventCommands(calendar.getJustEndedEvents(updateStatesLastCalledTime, now), CommandTagType.END);
346
347             // process all Command Tags in all Calendar Events which BEGAN since updateStates was last called
348             // the END Event tags must be processed before the BEGIN ones
349             executeEventCommands(calendar.getJustBegunEvents(updateStatesLastCalledTime, now), CommandTagType.BEGIN);
350
351             // save time when updateStates was previously called
352             // the purpose is to prevent repeat command execution of events that have already been executed
353             updateStatesLastCalledTime = now;
354         }
355     }
356 }