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