]> git.basschouten.com Git - openhab-addons.git/blob
abc9703b6e6f1b624c31f74588910084e6b9c9f1
[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.math.BigDecimal;
18 import java.time.Instant;
19 import java.time.ZonedDateTime;
20 import java.time.temporal.ChronoField;
21 import java.util.List;
22 import java.util.concurrent.CopyOnWriteArrayList;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.icalendar.internal.config.EventFilterConfiguration;
29 import org.openhab.binding.icalendar.internal.handler.PullJob.CalendarUpdateListener;
30 import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
31 import org.openhab.binding.icalendar.internal.logic.Event;
32 import org.openhab.binding.icalendar.internal.logic.EventTextFilter;
33 import org.openhab.core.i18n.TimeZoneProvider;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.Channel;
38 import org.openhab.core.thing.ChannelGroupUID;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingStatusInfo;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.thing.binding.ThingHandlerCallback;
46 import org.openhab.core.thing.binding.builder.ChannelBuilder;
47 import org.openhab.core.thing.binding.builder.ThingBuilder;
48 import org.openhab.core.thing.util.ThingHandlerHelper;
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 EventFilterHandler} filters events from a calendar and presents them in a dynamic way.
57  *
58  * @author Michael Wodniok - Initial Contribution
59  */
60 @NonNullByDefault
61 public class EventFilterHandler extends BaseThingHandler implements CalendarUpdateListener {
62
63     private @Nullable EventFilterConfiguration configuration;
64     private final Logger logger = LoggerFactory.getLogger(EventFilterHandler.class);
65     private final List<ResultChannelSet> resultChannels;
66     private final TimeZoneProvider tzProvider;
67     private @Nullable ScheduledFuture<?> updateFuture;
68
69     public EventFilterHandler(Thing thing, TimeZoneProvider tzProvider) {
70         super(thing);
71         resultChannels = new CopyOnWriteArrayList<>();
72         this.tzProvider = tzProvider;
73     }
74
75     @Override
76     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
77         if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
78             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
79         } else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
80             updateStates();
81         } else {
82             updateStatus(ThingStatus.UNKNOWN);
83         }
84     }
85
86     @Override
87     public void dispose() {
88         final ScheduledFuture<?> currentUpdateFuture = updateFuture;
89         if (currentUpdateFuture != null) {
90             currentUpdateFuture.cancel(true);
91         }
92     }
93
94     @Override
95     public void handleCommand(ChannelUID channelUID, Command command) {
96         if (command instanceof RefreshType) {
97             updateStates();
98         }
99     }
100
101     @Override
102     public void initialize() {
103         Bridge iCalendarBridge = getBridge();
104         if (iCalendarBridge == null) {
105             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
106                     "This thing requires a bridge configured to work.");
107             return;
108         }
109
110         final EventFilterConfiguration config = getConfigAs(EventFilterConfiguration.class);
111         if (config.datetimeUnit == null && (config.datetimeEnd != null || config.datetimeStart != null)) {
112             logger.warn("Start/End date-time is set but no unit. This will ignore the filter.");
113         }
114         if (config.textEventField != null && config.textValueType == null) {
115             logger.warn("Event field is set but not match type. This will ignore the filter.");
116         }
117         configuration = config;
118
119         updateChannelSet(config);
120         if (iCalendarBridge.getStatus() != ThingStatus.ONLINE) {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
122             return;
123         }
124
125         updateStatus(ThingStatus.UNKNOWN);
126     }
127
128     @Override
129     public void onCalendarUpdated() {
130         updateStates();
131     }
132
133     /**
134      * Consists of a set of channels and their group for describing a filtered event. *
135      */
136     private class ResultChannelSet {
137         ChannelGroupUID resultGroup;
138         ChannelUID beginChannel;
139         ChannelUID endChannel;
140         ChannelUID titleChannel;
141
142         public ResultChannelSet(ChannelGroupUID group, ChannelUID begin, ChannelUID end, ChannelUID title) {
143             resultGroup = group;
144             beginChannel = begin;
145             endChannel = end;
146             titleChannel = title;
147         }
148     }
149
150     /**
151      * Describes some fixed time factors for unit selection.
152      */
153     private enum TimeMultiplicator {
154         MINUTE(60),
155         HOUR(3600),
156         DAY(86400),
157         WEEK(604800);
158
159         private final int secondsPerUnit;
160
161         private TimeMultiplicator(int secondsPerUnit) {
162             this.secondsPerUnit = secondsPerUnit;
163         }
164
165         /**
166          * Returns the count of seconds per unit.
167          *
168          * @return Seconds per unit.
169          */
170         public int getMultiplier() {
171             return secondsPerUnit;
172         }
173     }
174
175     /**
176      * Generates a list of channel sets according to the required amount.
177      *
178      * @param resultCount The required amount of results.
179      */
180     private void generateExpectedChannelList(int resultCount) {
181         synchronized (resultChannels) {
182             if (resultChannels.size() == resultCount) {
183                 return;
184             }
185             resultChannels.clear();
186             for (int position = 0; position < resultCount; position++) {
187                 ChannelGroupUID currentGroup = new ChannelGroupUID(getThing().getUID(),
188                         RESULT_GROUP_ID_PREFIX + position);
189                 ResultChannelSet current = new ResultChannelSet(currentGroup,
190                         new ChannelUID(currentGroup, RESULT_BEGIN_ID), new ChannelUID(currentGroup, RESULT_END_ID),
191                         new ChannelUID(currentGroup, RESULT_TITLE_ID));
192                 resultChannels.add(current);
193             }
194         }
195     }
196
197     /**
198      * Checks existing channels, adds missing and removes extraneous channels from the Thing.
199      *
200      * @param config The validated Configuration of the Thing.
201      */
202     private void updateChannelSet(EventFilterConfiguration config) {
203         final ThingHandlerCallback handlerCallback = getCallback();
204         if (handlerCallback == null) {
205             return;
206         }
207
208         final List<Channel> currentChannels = getThing().getChannels();
209         final ThingBuilder thingBuilder = editThing();
210         BigDecimal maxEvents = config.maxEvents;
211         if (maxEvents == null || maxEvents.compareTo(BigDecimal.ZERO) < 1) {
212             thingBuilder.withoutChannels(currentChannels);
213             updateThing(thingBuilder.build());
214             return;
215         }
216         generateExpectedChannelList(maxEvents.intValue());
217
218         synchronized (resultChannels) {
219             currentChannels.stream().filter((Channel current) -> {
220                 String currentGroupId = current.getUID().getGroupId();
221                 if (currentGroupId == null) {
222                     return true;
223                 }
224                 for (ResultChannelSet channelSet : resultChannels) {
225                     if (channelSet.resultGroup.getId().contentEquals(currentGroupId)) {
226                         return false;
227                     }
228                 }
229                 return true;
230             }).forEach((Channel toDelete) -> {
231                 thingBuilder.withoutChannel(toDelete.getUID());
232             });
233
234             resultChannels.stream().filter((ResultChannelSet current) -> {
235                 return (getThing().getChannelsOfGroup(current.resultGroup.toString()).size() == 0);
236             }).forEach((ResultChannelSet current) -> {
237                 for (ChannelBuilder builder : handlerCallback.createChannelBuilders(current.resultGroup,
238                         GROUP_TYPE_UID)) {
239                     Channel currentChannel = builder.build();
240                     Channel existingChannel = getThing().getChannel(currentChannel.getUID());
241                     if (existingChannel == null) {
242                         thingBuilder.withChannel(currentChannel);
243                     }
244                 }
245             });
246         }
247         updateThing(thingBuilder.build());
248     }
249
250     /**
251      * Updates all states and channels. Reschedules an update if no error occurs.
252      */
253     private void updateStates() {
254         if (!ThingHandlerHelper.isHandlerInitialized(this)) {
255             logger.debug("Ignoring call for updating states as this instance is not initialized yet.");
256             return;
257         }
258         final Bridge iCalendarBridge = getBridge();
259         if (iCalendarBridge == null) {
260             logger.debug("Bridge not instantiated!");
261             return;
262         }
263         final ICalendarHandler iCalendarHandler = (ICalendarHandler) iCalendarBridge.getHandler();
264         if (iCalendarHandler == null) {
265             logger.debug("ICalendarHandler not instantiated!");
266             return;
267         }
268         final EventFilterConfiguration config = configuration;
269         if (config == null) {
270             logger.debug("Configuration not instantiated!");
271             return;
272         }
273         final AbstractPresentableCalendar cal = iCalendarHandler.getRuntimeCalendar();
274         if (cal != null) {
275             updateStatus(ThingStatus.ONLINE);
276
277             Instant reference = Instant.now();
278             TimeMultiplicator multiplicator = null;
279             EventTextFilter filter = null;
280             int maxEvents;
281             Instant begin = Instant.EPOCH;
282             Instant end = Instant.ofEpochMilli(Long.MAX_VALUE);
283
284             try {
285                 String textFilterValue = config.textEventValue;
286                 if (textFilterValue != null) {
287                     String textEventField = config.textEventField;
288                     String textValueType = config.textValueType;
289                     if (textEventField == null || textValueType == null) {
290                         throw new ConfigBrokenException("Text filter settings are not set properly.");
291                     }
292                     try {
293                         EventTextFilter.Field textFilterField = EventTextFilter.Field.valueOf(textEventField);
294                         EventTextFilter.Type textFilterType = EventTextFilter.Type.valueOf(textValueType);
295
296                         filter = new EventTextFilter(textFilterField, textFilterValue, textFilterType);
297                     } catch (IllegalArgumentException e2) {
298                         throw new ConfigBrokenException("textEventField or textValueType are not set properly.");
299                     }
300                 }
301
302                 BigDecimal maxEventsBD = config.maxEvents;
303                 if (maxEventsBD == null) {
304                     throw new ConfigBrokenException("maxEvents is not set.");
305                 }
306                 maxEvents = maxEventsBD.intValue();
307                 if (maxEvents < 0) {
308                     throw new ConfigBrokenException("maxEvents is less than 0. This is not allowed.");
309                 }
310
311                 try {
312                     final String datetimeUnit = config.datetimeUnit;
313                     if (datetimeUnit != null) {
314                         multiplicator = TimeMultiplicator.valueOf(datetimeUnit);
315                     }
316                 } catch (IllegalArgumentException e) {
317                     throw new ConfigBrokenException("datetimeUnit is not set properly.");
318                 }
319
320                 final Boolean datetimeRound = config.datetimeRound;
321                 if (datetimeRound != null && datetimeRound.booleanValue()) {
322                     if (multiplicator == null) {
323                         throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeRound.");
324                     }
325                     ZonedDateTime refDT = reference.atZone(tzProvider.getTimeZone());
326                     switch (multiplicator) {
327                         case WEEK:
328                             refDT = refDT.with(ChronoField.DAY_OF_WEEK, 1);
329                         case DAY:
330                             refDT = refDT.with(ChronoField.HOUR_OF_DAY, 0);
331                         case HOUR:
332                             refDT = refDT.with(ChronoField.MINUTE_OF_HOUR, 0);
333                         case MINUTE:
334                             refDT = refDT.with(ChronoField.SECOND_OF_MINUTE, 0);
335                     }
336                     reference = refDT.toInstant();
337                 }
338
339                 BigDecimal datetimeStart = config.datetimeStart;
340                 if (datetimeStart != null) {
341                     if (multiplicator == null) {
342                         throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeStart.");
343                     }
344                     begin = reference.plusSeconds(datetimeStart.longValue() * multiplicator.getMultiplier());
345                 }
346                 BigDecimal datetimeEnd = config.datetimeEnd;
347                 if (datetimeEnd != null) {
348                     if (multiplicator == null) {
349                         throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeEnd.");
350                     }
351                     end = reference.plusSeconds(datetimeEnd.longValue() * multiplicator.getMultiplier());
352                 }
353             } catch (ConfigBrokenException e) {
354                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
355                 return;
356             }
357
358             synchronized (resultChannels) {
359                 List<Event> results = cal.getFilteredEventsBetween(begin, end, filter, maxEvents);
360                 for (int position = 0; position < resultChannels.size(); position++) {
361                     ResultChannelSet channels = resultChannels.get(position);
362                     if (position < results.size()) {
363                         Event result = results.get(position);
364                         updateState(channels.titleChannel, new StringType(result.title));
365                         updateState(channels.beginChannel,
366                                 new DateTimeType(result.start.atZone(tzProvider.getTimeZone())));
367                         updateState(channels.endChannel, new DateTimeType(result.end.atZone(tzProvider.getTimeZone())));
368                     } else {
369                         updateState(channels.titleChannel, UnDefType.UNDEF);
370                         updateState(channels.beginChannel, UnDefType.UNDEF);
371                         updateState(channels.endChannel, UnDefType.UNDEF);
372                     }
373                 }
374             }
375         } else {
376             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
377                     "Calendar has not been retrieved yet.");
378         }
379
380         int refreshTime = DEFAULT_FILTER_REFRESH;
381         if (config.refreshTime != null) {
382             refreshTime = config.refreshTime.intValue();
383             if (refreshTime < 1) {
384                 logger.debug("refreshTime is set to invalid value. Using default.");
385                 refreshTime = DEFAULT_FILTER_REFRESH;
386             }
387         }
388         ScheduledFuture<?> currentUpdateFuture = updateFuture;
389         if (currentUpdateFuture != null) {
390             currentUpdateFuture.cancel(true);
391         }
392         updateFuture = scheduler.scheduleWithFixedDelay(this::updateStates, refreshTime, refreshTime, TimeUnit.MINUTES);
393     }
394 }