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.io.hueemulation.internal.automation;
15 import java.time.LocalDateTime;
16 import java.time.LocalTime;
17 import java.time.format.DateTimeFormatter;
18 import java.time.temporal.ChronoField;
19 import java.util.ArrayList;
20 import java.util.List;
22 import java.util.function.Predicate;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.core.automation.Condition;
28 import org.openhab.core.automation.handler.BaseModuleHandler;
29 import org.openhab.core.automation.handler.ConditionHandler;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.OpenClosedType;
32 import org.openhab.core.types.State;
33 import org.openhab.io.hueemulation.internal.dto.HueDataStore;
34 import org.openhab.io.hueemulation.internal.dto.HueGroupEntry;
35 import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
36 import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
37 import org.openhab.io.hueemulation.internal.dto.HueRuleEntry.Operator;
38 import org.openhab.io.hueemulation.internal.dto.HueSensorEntry;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * This condition is parameterized with Hue rule condition arguments. A Hue rule works
44 * on the Hue datastore and considers lights / groups / sensors that are available there.
46 * Implementation details: The predicate function for the condition is computed in the constructor of
47 * this condition. It will only be called and evaluated for isSatisfied.
49 * @author David Graeff - Initial contribution
52 public class HueRuleConditionHandler extends BaseModuleHandler<Condition> implements ConditionHandler {
54 private final Logger logger = LoggerFactory.getLogger(HueRuleConditionHandler.class);
56 public static final String MODULE_TYPE_ID = "hue.ruleCondition";
57 public static final String CALLBACK_CONTEXT_NAME = "CALLBACK";
58 public static final String MODULE_CONTEXT_NAME = "MODULE";
60 public static final String CFG_ADDRESS = "address";
61 public static final String CFG_OP = "operator";
62 public static final String CFG_VALUE = "value";
64 private static final String TIME_FORMAT = "HH:mm:ss";
66 protected final HueRuleEntry.Condition config;
67 protected String itemUID;
69 protected Predicate<State> predicate;
70 private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(TIME_FORMAT);
71 private final Pattern timePattern = Pattern.compile("W(.*)/T(.*)/T(.*)");
72 // weekdays range from Monday to Sunday (1-7). The first entry is not used
73 private final boolean[] weekDaysAllowed = { false, false, false, false, false, false, false, false };
75 @SuppressWarnings({ "null", "unused" })
76 public HueRuleConditionHandler(Condition module, HueDataStore ds) {
78 config = module.getConfiguration().as(HueRuleEntry.Condition.class);
80 // pattern: "/sensors/2/state/buttonevent" or "/config/localtime"
81 String[] validation = config.address.split("/");
82 String uid = validation[2];
84 if ("groups".equals(validation[1]) && "action".equals(validation[3])) {
85 HueGroupEntry entry = ds.groups.get(uid);
87 throw new IllegalStateException("Group does not exist: " + uid);
89 itemUID = entry.groupItem.getUID();
90 } else if ("lights".equals(validation[1]) && "state".equals(validation[3])) {
91 HueLightEntry entry = ds.lights.get(uid);
93 throw new IllegalStateException("Light does not exist: " + uid);
95 itemUID = entry.item.getUID();
96 } else if ("sensors".equals(validation[1]) && "state".equals(validation[3])) {
97 HueSensorEntry entry = ds.sensors.get(uid);
99 throw new IllegalStateException("Sensor does not exist: " + uid);
101 itemUID = entry.item.getUID();
102 } else if ("config".equals(validation[1]) && "localtime".equals(validation[2])) {
106 throw new IllegalStateException("Can only handle groups and lights");
109 if (itemUID == null) {
110 throw new IllegalStateException("Can only handle groups and lights");
113 final String value = config.value;
114 switch (config.operator) {
117 throw new IllegalStateException("Equal operator requires a value!");
119 predicate = state -> {
120 if (state instanceof Number) {
121 return Integer.valueOf(value) == ((Number) state).intValue();
122 } else if (state instanceof OnOffType) {
123 return Boolean.valueOf(value) == (((OnOffType) state) == OnOffType.ON);
124 } else if (state instanceof OpenClosedType) {
125 return Boolean.valueOf(value) == (((OpenClosedType) state) == OpenClosedType.OPEN);
127 return state.toFullString().equals(value);
132 throw new IllegalStateException("GreaterThan operator requires a value!");
134 final Integer integer = Integer.valueOf(value);
136 predicate = state -> {
137 if (state instanceof Number) {
138 return integer < ((Number) state).intValue();
147 throw new IllegalStateException("LowerThan operator requires a value!");
149 final Integer integer = Integer.valueOf(value);
151 predicate = state -> {
152 if (state instanceof Number) {
153 return integer > ((Number) state).intValue();
163 throw new IllegalStateException("InRange operator requires a value!");
166 Matcher m = timePattern.matcher(!value.startsWith("W") ? "W127/" + value : value);
167 if (!m.matches() || m.groupCount() < 3) {
168 throw new IllegalStateException(
169 "Time pattern incorrect for in/not_in hue rule condition: " + value);
172 final LocalTime timeStart = LocalTime.from(timeFormatter.parse(m.group(2)));
173 final LocalTime timeEnd = LocalTime.from(timeFormatter.parse(m.group(3)));
175 // Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday = 1
176 int weekdaysBinaryEncoded = Integer.valueOf(m.group(1));
177 List<String> cronWeekdays = new ArrayList<>();
178 for (int bin = 64, c = 1; bin > 0; bin /= 2, c += 1) {
179 if (weekdaysBinaryEncoded / bin == 1) {
180 weekdaysBinaryEncoded = weekdaysBinaryEncoded % bin;
181 weekDaysAllowed[c] = true;
185 predicate = state -> {
186 LocalDateTime now = getNow();
187 int dow = now.get(ChronoField.DAY_OF_WEEK);
188 LocalTime localTime = now.toLocalTime();
189 return weekDaysAllowed[dow] && localTime.isAfter(timeStart) && localTime.isBefore(timeEnd)
190 && config.operator == Operator.in;
194 predicate = s -> true;
199 // For test injection
200 protected LocalDateTime getNow() {
201 return LocalDateTime.now();
204 @NonNullByDefault({})
206 public boolean isSatisfied(Map<String, Object> context) {
207 switch (config.operator) {
210 return predicate.test(OnOffType.ON);
214 State state = (State) context.get("newState");
215 State oldState = (State) context.get("oldState");
217 if (state == null || oldState == null) {
218 logger.warn("Expected a state and oldState input or an in/not_in operator!");
222 switch (config.operator) {
223 case ddx: // Item changes always satisfies the "hue change" and "hue change delay" condition
229 return predicate.test(state);
230 case not_stable: // state changed?
231 return (!state.toFullString().equals(oldState.toFullString()));
232 case stable: // state stable?
233 return (state.toFullString().equals(oldState.toFullString()));
236 logger.warn("Operator {} not handled! ", config.operator.name());