2 * Copyright (c) 2010-2023 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.*;
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;
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;
56 * The {@link EventFilterHandler} filters events from a calendar and presents them in a dynamic way.
58 * @author Michael Wodniok - Initial Contribution
59 * @author Michael Wodniok - Fixed subsecond search if rounding to unit
62 public class EventFilterHandler extends BaseThingHandler implements CalendarUpdateListener {
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;
70 public EventFilterHandler(Thing thing, TimeZoneProvider tzProvider) {
72 resultChannels = new CopyOnWriteArrayList<>();
73 this.tzProvider = tzProvider;
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) {
83 updateStatus(ThingStatus.UNKNOWN);
88 public void dispose() {
89 final ScheduledFuture<?> currentUpdateFuture = updateFuture;
90 if (currentUpdateFuture != null) {
91 currentUpdateFuture.cancel(true);
96 public void handleCommand(ChannelUID channelUID, Command command) {
97 if (command instanceof RefreshType) {
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.");
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.");
115 if (config.textEventField != null && config.textValueType == null) {
116 logger.warn("Event field is set but not match type. This will ignore the filter.");
118 configuration = config;
120 updateChannelSet(config);
121 if (iCalendarBridge.getStatus() != ThingStatus.ONLINE) {
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
126 updateStatus(ThingStatus.UNKNOWN);
130 public void onCalendarUpdated() {
135 * Consists of a set of channels and their group for describing a filtered event. *
137 private class ResultChannelSet {
138 ChannelGroupUID resultGroup;
139 ChannelUID beginChannel;
140 ChannelUID endChannel;
141 ChannelUID titleChannel;
143 public ResultChannelSet(ChannelGroupUID group, ChannelUID begin, ChannelUID end, ChannelUID title) {
145 beginChannel = begin;
147 titleChannel = title;
152 * Describes some fixed time factors for unit selection.
154 private enum TimeMultiplicator {
160 private final int secondsPerUnit;
162 private TimeMultiplicator(int secondsPerUnit) {
163 this.secondsPerUnit = secondsPerUnit;
167 * Returns the count of seconds per unit.
169 * @return Seconds per unit.
171 public int getMultiplier() {
172 return secondsPerUnit;
177 * Generates a list of channel sets according to the required amount.
179 * @param resultCount The required amount of results.
181 private void generateExpectedChannelList(int resultCount) {
182 synchronized (resultChannels) {
183 if (resultChannels.size() == resultCount) {
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);
199 * Checks existing channels, adds missing and removes extraneous channels from the Thing.
201 * @param config The validated Configuration of the Thing.
203 private void updateChannelSet(EventFilterConfiguration config) {
204 final ThingHandlerCallback handlerCallback = getCallback();
205 if (handlerCallback == null) {
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());
217 generateExpectedChannelList(maxEvents.intValue());
219 synchronized (resultChannels) {
220 currentChannels.stream().filter((Channel current) -> {
221 String currentGroupId = current.getUID().getGroupId();
222 if (currentGroupId == null) {
225 for (ResultChannelSet channelSet : resultChannels) {
226 if (channelSet.resultGroup.getId().contentEquals(currentGroupId)) {
231 }).forEach((Channel toDelete) -> {
232 thingBuilder.withoutChannel(toDelete.getUID());
235 resultChannels.stream().filter((ResultChannelSet current) -> {
236 return (getThing().getChannelsOfGroup(current.resultGroup.toString()).size() == 0);
237 }).forEach((ResultChannelSet current) -> {
238 for (ChannelBuilder builder : handlerCallback.createChannelBuilders(current.resultGroup,
240 Channel currentChannel = builder.build();
241 Channel existingChannel = getThing().getChannel(currentChannel.getUID());
242 if (existingChannel == null) {
243 thingBuilder.withChannel(currentChannel);
248 updateThing(thingBuilder.build());
252 * Updates all states and channels. Reschedules an update if no error occurs.
254 private void updateStates() {
255 if (!ThingHandlerHelper.isHandlerInitialized(this)) {
256 logger.debug("Ignoring call for updating states as this instance is not initialized yet.");
259 final Bridge iCalendarBridge = getBridge();
260 if (iCalendarBridge == null) {
261 logger.debug("Bridge not instantiated!");
264 final ICalendarHandler iCalendarHandler = (ICalendarHandler) iCalendarBridge.getHandler();
265 if (iCalendarHandler == null) {
266 logger.debug("ICalendarHandler not instantiated!");
269 final EventFilterConfiguration config = configuration;
270 if (config == null) {
271 logger.debug("Configuration not instantiated!");
274 final AbstractPresentableCalendar cal = iCalendarHandler.getRuntimeCalendar();
276 updateStatus(ThingStatus.ONLINE);
278 Instant reference = Instant.now();
279 TimeMultiplicator multiplicator = null;
280 EventTextFilter filter = null;
282 Instant begin = Instant.EPOCH;
283 Instant end = Instant.ofEpochMilli(Long.MAX_VALUE);
286 String textFilterValue = config.textEventValue;
287 if (textFilterValue != null) {
288 String textEventField = config.textEventField;
289 String textValueType = config.textValueType;
290 if (textEventField == null || textValueType == null) {
291 throw new ConfigBrokenException("Text filter settings are not set properly.");
294 EventTextFilter.Field textFilterField = EventTextFilter.Field.valueOf(textEventField);
295 EventTextFilter.Type textFilterType = EventTextFilter.Type.valueOf(textValueType);
297 filter = new EventTextFilter(textFilterField, textFilterValue, textFilterType);
298 } catch (IllegalArgumentException e2) {
299 throw new ConfigBrokenException("textEventField or textValueType are not set properly.");
303 BigDecimal maxEventsBD = config.maxEvents;
304 if (maxEventsBD == null) {
305 throw new ConfigBrokenException("maxEvents is not set.");
307 maxEvents = maxEventsBD.intValue();
309 throw new ConfigBrokenException("maxEvents is less than 0. This is not allowed.");
313 final String datetimeUnit = config.datetimeUnit;
314 if (datetimeUnit != null) {
315 multiplicator = TimeMultiplicator.valueOf(datetimeUnit);
317 } catch (IllegalArgumentException e) {
318 throw new ConfigBrokenException("datetimeUnit is not set properly.");
321 final Boolean datetimeRound = config.datetimeRound;
322 if (datetimeRound != null && datetimeRound.booleanValue()) {
323 if (multiplicator == null) {
324 throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeRound.");
326 ZonedDateTime refDT = reference.atZone(tzProvider.getTimeZone());
327 switch (multiplicator) {
329 refDT = refDT.with(ChronoField.DAY_OF_WEEK, 1);
331 refDT = refDT.with(ChronoField.HOUR_OF_DAY, 0);
333 refDT = refDT.with(ChronoField.MINUTE_OF_HOUR, 0);
335 refDT = refDT.with(ChronoField.SECOND_OF_MINUTE, 0).with(ChronoField.NANO_OF_SECOND, 0);
337 reference = refDT.toInstant();
340 BigDecimal datetimeStart = config.datetimeStart;
341 if (datetimeStart != null) {
342 if (multiplicator == null) {
343 throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeStart.");
345 begin = reference.plusSeconds(datetimeStart.longValue() * multiplicator.getMultiplier());
347 BigDecimal datetimeEnd = config.datetimeEnd;
348 if (datetimeEnd != null) {
349 if (multiplicator == null) {
350 throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeEnd.");
352 end = reference.plusSeconds(datetimeEnd.longValue() * multiplicator.getMultiplier());
354 } catch (ConfigBrokenException e) {
355 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
359 synchronized (resultChannels) {
360 List<Event> results = cal.getFilteredEventsBetween(begin, end, filter, maxEvents);
361 for (int position = 0; position < resultChannels.size(); position++) {
362 ResultChannelSet channels = resultChannels.get(position);
363 if (position < results.size()) {
364 Event result = results.get(position);
365 updateState(channels.titleChannel, new StringType(result.title));
366 updateState(channels.beginChannel,
367 new DateTimeType(result.start.atZone(tzProvider.getTimeZone())));
368 updateState(channels.endChannel, new DateTimeType(result.end.atZone(tzProvider.getTimeZone())));
370 updateState(channels.titleChannel, UnDefType.UNDEF);
371 updateState(channels.beginChannel, UnDefType.UNDEF);
372 updateState(channels.endChannel, UnDefType.UNDEF);
377 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
378 "Calendar has not been retrieved yet.");
381 int refreshTime = DEFAULT_FILTER_REFRESH;
382 if (config.refreshTime != null) {
383 refreshTime = config.refreshTime.intValue();
384 if (refreshTime < 1) {
385 logger.debug("refreshTime is set to invalid value. Using default.");
386 refreshTime = DEFAULT_FILTER_REFRESH;
389 ScheduledFuture<?> currentUpdateFuture = updateFuture;
390 if (currentUpdateFuture != null) {
391 currentUpdateFuture.cancel(true);
393 updateFuture = scheduler.scheduleWithFixedDelay(this::updateStates, refreshTime, refreshTime, TimeUnit.MINUTES);