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