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