]> git.basschouten.com Git - openhab-addons.git/blob
632a818f3a5e5c3786780e1f095af4e174652e04
[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.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.math.BigDecimal;
21 import java.net.URI;
22 import java.net.URISyntaxException;
23 import java.time.Instant;
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.OpenHAB;
39 import org.openhab.core.events.EventPublisher;
40 import org.openhab.core.i18n.TimeZoneProvider;
41 import org.openhab.core.items.events.ItemEventFactory;
42 import org.openhab.core.library.types.DateTimeType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.thing.binding.ThingHandler;
52 import org.openhab.core.thing.binding.ThingHandlerCallback;
53 import org.openhab.core.thing.binding.builder.ChannelBuilder;
54 import org.openhab.core.thing.binding.builder.ThingBuilder;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.RefreshType;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 /**
62  * The {@link ICalendarHandler} is responsible for handling commands, which are
63  * sent to one of the channels.
64  *
65  * @author Michael Wodniok - Initial contribution
66  * @author Andrew Fiddian-Green - Support for Command Tags embedded in the Event description
67  * @author Michael Wodniok - Added last_update-channel and additional needed handling of it
68  */
69 @NonNullByDefault
70 public class ICalendarHandler extends BaseBridgeHandler implements CalendarUpdateListener {
71
72     private final File calendarFile;
73     private @Nullable ICalendarConfiguration configuration;
74     private final EventPublisher eventPublisherCallback;
75     private final HttpClient httpClient;
76     private final Logger logger = LoggerFactory.getLogger(ICalendarHandler.class);
77     private final TimeZoneProvider tzProvider;
78     private @Nullable ScheduledFuture<?> pullJobFuture;
79     private @Nullable AbstractPresentableCalendar runtimeCalendar;
80     private @Nullable ScheduledFuture<?> updateJobFuture;
81     private Instant updateStatesLastCalledTime;
82     private @Nullable Instant calendarDownloadedTime;
83
84     public ICalendarHandler(Bridge bridge, HttpClient httpClient, EventPublisher eventPublisher,
85             TimeZoneProvider tzProvider) {
86         super(bridge);
87         this.httpClient = httpClient;
88         final File cacheFolder = new File(new File(OpenHAB.getUserDataFolder(), "cache"),
89                 "org.openhab.binding.icalendar");
90         if (!cacheFolder.exists()) {
91             logger.debug("Creating cache folder '{}'", cacheFolder.getAbsolutePath());
92             cacheFolder.mkdirs();
93         }
94         calendarFile = new File(cacheFolder,
95                 getThing().getUID().getAsString().replaceAll("[<>:\"/\\\\|?*]", "_") + ".ical");
96         eventPublisherCallback = eventPublisher;
97         updateStatesLastCalledTime = Instant.now();
98         this.tzProvider = tzProvider;
99     }
100
101     @Override
102     public void dispose() {
103         final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
104         if (currentUpdateJobFuture != null) {
105             currentUpdateJobFuture.cancel(true);
106         }
107         final ScheduledFuture<?> currentPullJobFuture = pullJobFuture;
108         if (currentPullJobFuture != null) {
109             currentPullJobFuture.cancel(true);
110         }
111     }
112
113     @Override
114     public void handleCommand(ChannelUID channelUID, Command command) {
115         switch (channelUID.getId()) {
116             case CHANNEL_CURRENT_EVENT_PRESENT:
117             case CHANNEL_CURRENT_EVENT_TITLE:
118             case CHANNEL_CURRENT_EVENT_START:
119             case CHANNEL_CURRENT_EVENT_END:
120             case CHANNEL_NEXT_EVENT_TITLE:
121             case CHANNEL_NEXT_EVENT_START:
122             case CHANNEL_NEXT_EVENT_END:
123             case CHANNEL_LAST_UPDATE:
124                 if (command instanceof RefreshType) {
125                     updateStates();
126                 }
127                 break;
128             default:
129                 logger.warn("Framework sent command to unknown channel with id '{}'", channelUID.getId());
130         }
131     }
132
133     @Override
134     public void initialize() {
135         migrateLastUpdateChannel();
136
137         final ICalendarConfiguration currentConfiguration = getConfigAs(ICalendarConfiguration.class);
138         configuration = currentConfiguration;
139
140         try {
141             if ((currentConfiguration.username == null && currentConfiguration.password != null)
142                     || (currentConfiguration.username != null && currentConfiguration.password == null)) {
143                 throw new ConfigBrokenException("Only one of username and password was set. This is invalid.");
144             }
145
146             PullJob regularPull;
147             final BigDecimal maxSizeBD = currentConfiguration.maxSize;
148             if (maxSizeBD == null || maxSizeBD.intValue() < 1) {
149                 throw new ConfigBrokenException(
150                         "maxSize is either not set or less than 1 (mebibyte), which is not allowed.");
151             }
152             final int maxSize = maxSizeBD.intValue();
153             try {
154                 regularPull = new PullJob(httpClient, new URI(currentConfiguration.url), currentConfiguration.username,
155                         currentConfiguration.password, calendarFile, maxSize * 1048576, this);
156             } catch (URISyntaxException e) {
157                 throw new ConfigBrokenException(String.format(
158                         "The URI '%s' for downloading the calendar contains syntax errors.", currentConfiguration.url));
159
160             }
161
162             final BigDecimal refreshTimeBD = currentConfiguration.refreshTime;
163             if (refreshTimeBD == null || refreshTimeBD.longValue() < 1) {
164                 throw new ConfigBrokenException(
165                         "refreshTime is either not set or less than 1 (minute), which is not allowed.");
166             }
167             final long refreshTime = refreshTimeBD.longValue();
168             if (calendarFile.isFile()) {
169                 updateStatus(ThingStatus.ONLINE);
170
171                 scheduler.submit(() -> {
172                     // reload calendar file asynchronously
173                     if (reloadCalendar()) {
174                         updateStates();
175                         updateChildren();
176                     } else {
177                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
178                                 "The calendar seems to be configured correctly, but the local copy of calendar could not be loaded.");
179                     }
180                 });
181                 pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, refreshTime, refreshTime,
182                         TimeUnit.MINUTES);
183             } else {
184                 updateStatus(ThingStatus.OFFLINE);
185                 logger.debug(
186                         "The calendar is currently offline as no local copy exists. It will go online as soon as a valid valid calendar is retrieved.");
187                 pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, 0, refreshTime, TimeUnit.MINUTES);
188             }
189         } catch (ConfigBrokenException e) {
190             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
191         }
192     }
193
194     @Override
195     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
196         final AbstractPresentableCalendar calendar = runtimeCalendar;
197         if (calendar != null) {
198             updateChild(childHandler);
199         }
200     }
201
202     @Override
203     public void onCalendarUpdated() {
204         if (reloadCalendar()) {
205             updateStates();
206             updateChildren();
207         } else {
208             logger.trace("Calendar was updated, but loading failed.");
209         }
210     }
211
212     /**
213      * @return the calendar that is used for all operations
214      */
215     @Nullable
216     public AbstractPresentableCalendar getRuntimeCalendar() {
217         return runtimeCalendar;
218     }
219
220     private void executeEventCommands(List<Event> events, CommandTagType execTime) {
221         // no begun or ended events => exit quietly as there is nothing to do
222         if (events.isEmpty()) {
223             return;
224         }
225
226         final ICalendarConfiguration syncConfiguration = configuration;
227         if (syncConfiguration == null) {
228             logger.debug("Configuration not instantiated!");
229             return;
230         }
231         // loop through all events in the list
232         for (Event event : events) {
233
234             // loop through all command tags in the event
235             for (CommandTag cmdTag : event.commandTags) {
236
237                 // only process the BEGIN resp. END tags
238                 if (cmdTag.getTagType() != execTime) {
239                     continue;
240                 }
241                 if (!cmdTag.isAuthorized(syncConfiguration.authorizationCode)) {
242                     logger.warn("Event: {}, Command Tag: {} => Command not authorized!", event.title,
243                             cmdTag.getFullTag());
244                     continue;
245                 }
246
247                 final Command cmdState = cmdTag.getCommand();
248                 if (cmdState == null) {
249                     logger.warn("Event: {}, Command Tag: {} => Error creating Command State!", event.title,
250                             cmdTag.getFullTag());
251                     continue;
252                 }
253
254                 // (try to) execute the command
255                 try {
256                     eventPublisherCallback.post(ItemEventFactory.createCommandEvent(cmdTag.getItemName(), cmdState));
257                     if (logger.isDebugEnabled()) {
258                         String cmdType = cmdState.getClass().toString();
259                         int index = cmdType.lastIndexOf(".") + 1;
260                         if ((index > 0) && (index < cmdType.length())) {
261                             cmdType = cmdType.substring(index);
262                         }
263                         logger.debug("Event: {}, Command Tag: {} => {}.postUpdate({}: {})", event.title,
264                                 cmdTag.getFullTag(), cmdTag.getItemName(), cmdType, cmdState);
265                     }
266                 } catch (IllegalArgumentException | IllegalStateException e) {
267                     logger.warn("Event: {}, Command Tag: {} => Unable to push command to target item!", event.title,
268                             cmdTag.getFullTag());
269                     logger.debug("Exception occured while pushing to item!", e);
270                 }
271             }
272         }
273     }
274
275     /**
276      * Migration for last_update-channel as this change is compatible to previous instances.
277      */
278     private void migrateLastUpdateChannel() {
279         final Thing thing = getThing();
280         if (thing.getChannel(CHANNEL_LAST_UPDATE) == null) {
281             logger.trace("last_update channel is missing in this Thing. Adding it.");
282             final ThingHandlerCallback callback = getCallback();
283             if (callback == null) {
284                 logger.debug("ThingHandlerCallback is null. Skipping migration of last_update channel.");
285                 return;
286             }
287             final ChannelBuilder channelBuilder = callback
288                     .createChannelBuilder(new ChannelUID(thing.getUID(), CHANNEL_LAST_UPDATE), LAST_UPDATE_TYPE_UID);
289             final ThingBuilder thingBuilder = editThing();
290             thingBuilder.withChannel(channelBuilder.build());
291             updateThing(thingBuilder.build());
292         }
293     }
294
295     /**
296      * Reloads the calendar from local ical-file. Replaces the class internal calendar - if loading succeeds. Else
297      * logging details at warn-level logger.
298      *
299      * @return Whether the calendar was loaded successfully.
300      */
301     private boolean reloadCalendar() {
302         logger.trace("reloading calendar of {}", getThing().getUID());
303         if (!calendarFile.isFile()) {
304             logger.info("Local file for reloading calendar is missing.");
305             return false;
306         }
307         final ICalendarConfiguration config = configuration;
308         if (config == null) {
309             logger.warn("Can't reload calendar when configuration is missing.");
310             return false;
311         }
312         try (final FileInputStream fileStream = new FileInputStream(calendarFile)) {
313             final AbstractPresentableCalendar calendar = AbstractPresentableCalendar.create(fileStream);
314             runtimeCalendar = calendar;
315             rescheduleCalendarStateUpdate();
316             calendarDownloadedTime = Instant.ofEpochMilli(calendarFile.lastModified());
317         } catch (IOException | CalendarException e) {
318             logger.warn("Loading calendar failed: {}", e.getMessage());
319             return false;
320         }
321         return true;
322     }
323
324     /**
325      * Reschedules the next update of the states.
326      */
327     private void rescheduleCalendarStateUpdate() {
328         final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
329         if (currentUpdateJobFuture != null) {
330             if (!(currentUpdateJobFuture.isCancelled() || currentUpdateJobFuture.isDone())) {
331                 currentUpdateJobFuture.cancel(true);
332             }
333             updateJobFuture = null;
334         }
335         final AbstractPresentableCalendar currentCalendar = runtimeCalendar;
336         if (currentCalendar == null) {
337             return;
338         }
339         final Instant now = Instant.now();
340         if (currentCalendar.isEventPresent(now)) {
341             final Event currentEvent = currentCalendar.getCurrentEvent(now);
342             if (currentEvent == null) {
343                 logger.debug(
344                         "Could not schedule next update of states, due to unexpected behaviour of calendar implementation.");
345                 return;
346             }
347             updateJobFuture = scheduler.schedule(() -> {
348                 ICalendarHandler.this.updateStates();
349                 ICalendarHandler.this.rescheduleCalendarStateUpdate();
350             }, currentEvent.end.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
351             logger.debug("Scheduled update in {} seconds", currentEvent.end.getEpochSecond() - now.getEpochSecond());
352         } else {
353             final Event nextEvent = currentCalendar.getNextEvent(now);
354             final ICalendarConfiguration currentConfig = this.configuration;
355             if (currentConfig == null) {
356                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
357                         "Something is broken, the configuration is not available.");
358                 return;
359             }
360             if (nextEvent == null) {
361                 updateJobFuture = scheduler.schedule(() -> {
362                     ICalendarHandler.this.rescheduleCalendarStateUpdate();
363                 }, 1L, TimeUnit.DAYS);
364                 logger.debug("Scheduled reschedule in 1 day");
365             } else {
366                 updateJobFuture = scheduler.schedule(() -> {
367                     ICalendarHandler.this.updateStates();
368                     ICalendarHandler.this.rescheduleCalendarStateUpdate();
369                 }, nextEvent.start.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
370                 logger.debug("Scheduled update in {} seconds", nextEvent.start.getEpochSecond() - now.getEpochSecond());
371
372             }
373         }
374     }
375
376     /**
377      * Updates the states of the Thing and its channels.
378      */
379     private void updateStates() {
380         logger.trace("updating states of {}", getThing().getUID());
381         final AbstractPresentableCalendar calendar = runtimeCalendar;
382         if (calendar == null) {
383             updateStatus(ThingStatus.OFFLINE);
384         } else {
385             updateStatus(ThingStatus.ONLINE);
386
387             final Instant now = Instant.now();
388             if (calendar.isEventPresent(now)) {
389                 updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.ON);
390                 final Event currentEvent = calendar.getCurrentEvent(now);
391                 if (currentEvent == null) {
392                     logger.warn("Unexpected inconsistency of internal API. Not Updating event details.");
393                 } else {
394                     updateState(CHANNEL_CURRENT_EVENT_TITLE, new StringType(currentEvent.title));
395                     updateState(CHANNEL_CURRENT_EVENT_START,
396                             new DateTimeType(currentEvent.start.atZone(tzProvider.getTimeZone())));
397                     updateState(CHANNEL_CURRENT_EVENT_END,
398                             new DateTimeType(currentEvent.end.atZone(tzProvider.getTimeZone())));
399                 }
400             } else {
401                 updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.OFF);
402                 updateState(CHANNEL_CURRENT_EVENT_TITLE, UnDefType.UNDEF);
403                 updateState(CHANNEL_CURRENT_EVENT_START, UnDefType.UNDEF);
404                 updateState(CHANNEL_CURRENT_EVENT_END, UnDefType.UNDEF);
405             }
406
407             final Event nextEvent = calendar.getNextEvent(now);
408             if (nextEvent != null) {
409                 updateState(CHANNEL_NEXT_EVENT_TITLE, new StringType(nextEvent.title));
410                 updateState(CHANNEL_NEXT_EVENT_START,
411                         new DateTimeType(nextEvent.start.atZone(tzProvider.getTimeZone())));
412                 updateState(CHANNEL_NEXT_EVENT_END, new DateTimeType(nextEvent.end.atZone(tzProvider.getTimeZone())));
413             } else {
414                 updateState(CHANNEL_NEXT_EVENT_TITLE, UnDefType.UNDEF);
415                 updateState(CHANNEL_NEXT_EVENT_START, UnDefType.UNDEF);
416                 updateState(CHANNEL_NEXT_EVENT_END, UnDefType.UNDEF);
417             }
418
419             final Instant lastUpdate = calendarDownloadedTime;
420             updateState(CHANNEL_LAST_UPDATE,
421                     (lastUpdate != null ? new DateTimeType(lastUpdate.atZone(tzProvider.getTimeZone()))
422                             : UnDefType.UNDEF));
423
424             // process all Command Tags in all Calendar Events which ENDED since updateStates was last called
425             // the END Event tags must be processed before the BEGIN ones
426             executeEventCommands(calendar.getJustEndedEvents(updateStatesLastCalledTime, now), CommandTagType.END);
427
428             // process all Command Tags in all Calendar Events which BEGAN since updateStates was last called
429             // the END Event tags must be processed before the BEGIN ones
430             executeEventCommands(calendar.getJustBegunEvents(updateStatesLastCalledTime, now), CommandTagType.BEGIN);
431
432             // save time when updateStates was previously called
433             // the purpose is to prevent repeat command execution of events that have already been executed
434             updateStatesLastCalledTime = now;
435         }
436     }
437
438     /**
439      * Updates all children of this handler.
440      */
441     private void updateChildren() {
442         getThing().getThings().forEach(childThing -> updateChild(childThing.getHandler()));
443     }
444
445     /**
446      * Updates a specific child handler.
447      *
448      * @param childHandler the handler to be updated
449      */
450     private void updateChild(@Nullable ThingHandler childHandler) {
451         if (childHandler instanceof CalendarUpdateListener) {
452             logger.trace("Notifying {} about fresh calendar.", childHandler.getThing().getUID());
453             try {
454                 ((CalendarUpdateListener) childHandler).onCalendarUpdated();
455             } catch (Exception e) {
456                 logger.trace("The update of a child handler failed. Ignoring.", e);
457             }
458         }
459     }
460 }