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