2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.icalendar.internal.handler;
15 import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.*;
18 import java.io.FileInputStream;
19 import java.io.IOException;
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;
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.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;
56 * The {@link ICalendarHandler} is responsible for handling commands, which are
57 * sent to one of the channels.
59 * @author Michael Wodniok - Initial contribution
60 * @author Andrew Fiddian-Green - Support for Command Tags embedded in the Event description
63 public class ICalendarHandler extends BaseThingHandler implements CalendarUpdateListener {
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;
75 public ICalendarHandler(Thing thing, HttpClient httpClient, EventPublisher eventPublisher) {
77 this.httpClient = httpClient;
78 calendarFile = new File(OpenHAB.getUserDataFolder() + File.separator
79 + getThing().getUID().getAsString().replaceAll("[<>:\"/\\\\|?*]", "_") + ".ical");
80 eventPublisherCallback = eventPublisher;
81 updateStatesLastCalledTime = Instant.now();
85 public void dispose() {
86 final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
87 if (currentUpdateJobFuture != null) {
88 currentUpdateJobFuture.cancel(true);
90 final ScheduledFuture<?> currentPullJobFuture = pullJobFuture;
91 if (currentPullJobFuture != null) {
92 currentPullJobFuture.cancel(true);
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) {
111 logger.warn("Framework sent command to unknown channel with id '{}'", channelUID.getId());
116 public void initialize() {
117 updateStatus(ThingStatus.UNKNOWN);
119 final ICalendarConfiguration currentConfiguration = getConfigAs(ICalendarConfiguration.class);
120 configuration = currentConfiguration;
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.");
131 regularPull = new PullJob(httpClient, new URI(currentConfiguration.url), currentConfiguration.username,
132 currentConfiguration.password, calendarFile, currentConfiguration.maxSize * 1048576, this);
133 } catch (URISyntaxException e) {
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);
141 if (calendarFile.isFile()) {
142 if (reloadCalendar()) {
143 updateStatus(ThingStatus.ONLINE);
145 rescheduleCalendarStateUpdate();
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.");
150 pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, currentConfiguration.refreshTime.longValue(),
151 currentConfiguration.refreshTime.longValue(), TimeUnit.MINUTES);
153 updateStatus(ThingStatus.OFFLINE);
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);
162 public void onCalendarUpdated() {
163 if (reloadCalendar()) {
166 logger.trace("Calendar was updated, but loading failed.");
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()) {
176 // prevent potential synchronization issues (MVN null pointer warnings) in "configuration"
178 ICalendarConfiguration syncConfiguration = configuration;
179 if (syncConfiguration == null) {
180 logger.debug("Configuration not instantiated!");
183 // loop through all events in the list
184 for (Event event : events) {
186 // loop through all command tags in the event
187 for (CommandTag cmdTag : event.commandTags) {
189 // only process the BEGIN resp. END tags
190 if (cmdTag.getTagType() != execTime) {
193 if (!cmdTag.isAuthorized(syncConfiguration.authorizationCode)) {
194 logger.warn("Event: {}, Command Tag: {} => Command not authorized!", event.title,
195 cmdTag.getFullTag());
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());
206 // (try to) execute the command
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);
215 logger.debug("Event: {}, Command Tag: {} => {}.postUpdate({}: {})", event.title,
216 cmdTag.getFullTag(), cmdTag.getItemName(), cmdType, cmdState);
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);
228 * Reloads the calendar from local ical-file. Replaces the class internal calendar - if loading succeeds. Else
229 * logging details at warn-level logger.
231 * @return Whether the calendar was loaded successfully.
233 private boolean reloadCalendar() {
234 if (!calendarFile.isFile()) {
235 logger.info("Local file for reloading calendar is missing.");
238 final ICalendarConfiguration config = configuration;
239 if (config == null) {
240 logger.warn("Can't reload calendar when configuration is missing.");
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());
255 * Reschedules the next update of the states.
257 private void rescheduleCalendarStateUpdate() {
258 final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
259 if (currentUpdateJobFuture != null) {
260 if (!(currentUpdateJobFuture.isCancelled() || currentUpdateJobFuture.isDone())) {
261 currentUpdateJobFuture.cancel(true);
263 updateJobFuture = null;
265 final AbstractPresentableCalendar currentCalendar = runtimeCalendar;
266 if (currentCalendar == null) {
269 final Instant now = Instant.now();
270 if (currentCalendar.isEventPresent(now)) {
271 final Event currentEvent = currentCalendar.getCurrentEvent(now);
272 if (currentEvent == null) {
274 "Could not schedule next update of states, due to unexpected behaviour of calendar implementation.");
277 updateJobFuture = scheduler.schedule(() -> {
278 ICalendarHandler.this.updateStates();
279 ICalendarHandler.this.rescheduleCalendarStateUpdate();
280 }, currentEvent.end.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
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.");
289 if (nextEvent == null) {
290 updateJobFuture = scheduler.schedule(() -> {
291 ICalendarHandler.this.rescheduleCalendarStateUpdate();
292 }, 1L, TimeUnit.DAYS);
294 updateJobFuture = scheduler.schedule(() -> {
295 ICalendarHandler.this.updateStates();
296 ICalendarHandler.this.rescheduleCalendarStateUpdate();
297 }, nextEvent.start.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
303 * Updates the states of the Thing and its channels.
305 private void updateStates() {
306 final AbstractPresentableCalendar calendar = runtimeCalendar;
307 if (calendar == null) {
308 updateStatus(ThingStatus.OFFLINE);
310 updateStatus(ThingStatus.ONLINE);
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.");
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())));
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);
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())));
338 updateState(CHANNEL_NEXT_EVENT_TITLE, UnDefType.UNDEF);
339 updateState(CHANNEL_NEXT_EVENT_START, UnDefType.UNDEF);
340 updateState(CHANNEL_NEXT_EVENT_END, UnDefType.UNDEF);
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);
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);
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;