]> git.basschouten.com Git - openhab-addons.git/blob
de52dc1b4891affa276e4f67400e81b633f4f713
[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.io.hueemulation.internal.automation;
14
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;
21 import java.util.Map;
22 import java.util.function.Predicate;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
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;
41
42 /**
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.
45  * <p>
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.
48  *
49  * @author David Graeff - Initial contribution
50  */
51 @NonNullByDefault
52 public class HueRuleConditionHandler extends BaseModuleHandler<Condition> implements ConditionHandler {
53
54     private final Logger logger = LoggerFactory.getLogger(HueRuleConditionHandler.class);
55
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";
59
60     public static final String CFG_ADDRESS = "address";
61     public static final String CFG_OP = "operator";
62     public static final String CFG_VALUE = "value";
63
64     private static final String TIME_FORMAT = "HH:mm:ss";
65
66     protected final HueRuleEntry.Condition config;
67     protected String itemUID;
68
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 };
74
75     @SuppressWarnings({ "null", "unused" })
76     public HueRuleConditionHandler(Condition module, HueDataStore ds) {
77         super(module);
78         config = module.getConfiguration().as(HueRuleEntry.Condition.class);
79
80         // pattern: "/sensors/2/state/buttonevent" or "/config/localtime"
81         String[] validation = config.address.split("/");
82         String uid = validation[2];
83
84         if ("groups".equals(validation[1]) && "action".equals(validation[3])) {
85             HueGroupEntry entry = ds.groups.get(uid);
86             if (entry == null) {
87                 throw new IllegalStateException("Group does not exist: " + uid);
88             }
89             itemUID = entry.groupItem.getUID();
90         } else if ("lights".equals(validation[1]) && "state".equals(validation[3])) {
91             HueLightEntry entry = ds.lights.get(uid);
92             if (entry == null) {
93                 throw new IllegalStateException("Light does not exist: " + uid);
94             }
95             itemUID = entry.item.getUID();
96         } else if ("sensors".equals(validation[1]) && "state".equals(validation[3])) {
97             HueSensorEntry entry = ds.sensors.get(uid);
98             if (entry == null) {
99                 throw new IllegalStateException("Sensor does not exist: " + uid);
100             }
101             itemUID = entry.item.getUID();
102         } else if ("config".equals(validation[1]) && "localtime".equals(validation[2])) {
103             // Item not used
104             itemUID = "";
105         } else {
106             throw new IllegalStateException("Can only handle groups and lights");
107         }
108
109         if (itemUID == null) {
110             throw new IllegalStateException("Can only handle groups and lights");
111         }
112
113         final String value = config.value;
114         switch (config.operator) {
115             case eq:
116                 if (value == null) {
117                     throw new IllegalStateException("Equal operator requires a value!");
118                 }
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);
126                     }
127                     return state.toFullString().equals(value);
128                 };
129                 break;
130             case gt:
131                 if (value == null) {
132                     throw new IllegalStateException("GreaterThan operator requires a value!");
133                 } else {
134                     final Integer integer = Integer.valueOf(value);
135
136                     predicate = state -> {
137                         if (state instanceof Number) {
138                             return integer < ((Number) state).intValue();
139                         } else {
140                             return false;
141                         }
142                     };
143                 }
144                 break;
145             case lt:
146                 if (value == null) {
147                     throw new IllegalStateException("LowerThan operator requires a value!");
148                 } else {
149                     final Integer integer = Integer.valueOf(value);
150
151                     predicate = state -> {
152                         if (state instanceof Number) {
153                             return integer > ((Number) state).intValue();
154                         } else {
155                             return false;
156                         }
157                     };
158                 }
159                 break;
160             case in:
161             case not_in:
162                 if (value == null) {
163                     throw new IllegalStateException("InRange operator requires a value!");
164                 }
165
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);
170                 }
171
172                 final LocalTime timeStart = LocalTime.from(timeFormatter.parse(m.group(2)));
173                 final LocalTime timeEnd = LocalTime.from(timeFormatter.parse(m.group(3)));
174
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;
182                     }
183                 }
184
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;
191                 };
192                 break;
193             default:
194                 predicate = s -> true;
195                 break;
196         }
197     }
198
199     // For test injection
200     protected LocalDateTime getNow() {
201         return LocalDateTime.now();
202     }
203
204     @NonNullByDefault({})
205     @Override
206     public boolean isSatisfied(Map<String, Object> context) {
207         switch (config.operator) {
208             case in:
209             case not_in:
210                 return predicate.test(OnOffType.ON);
211             default:
212         }
213
214         State state = (State) context.get("newState");
215         State oldState = (State) context.get("oldState");
216
217         if (state == null || oldState == null) {
218             logger.warn("Expected a state and oldState input or an in/not_in operator!");
219             return false;
220         }
221
222         switch (config.operator) {
223             case ddx: // Item changes always satisfies the "hue change" and "hue change delay" condition
224             case dx:
225                 return true;
226             case eq:
227             case gt:
228             case lt:
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()));
234             case unknown:
235             default:
236                 logger.warn("Operator {} not handled! ", config.operator.name());
237                 return false;
238         }
239     }
240 }