]> git.basschouten.com Git - openhab-addons.git/blob
e803b602aa9a3fcd1b7d9077d7262db5db836e2f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.transform.basicprofiles.internal.profiles;
14
15 import static org.hamcrest.Matchers.*;
16 import static org.junit.jupiter.api.Assertions.assertEquals;
17 import static org.mockito.ArgumentMatchers.*;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.ArgumentMatchers.eq;
20 import static org.mockito.Mockito.*;
21 import static org.mockito.Mockito.reset;
22 import static org.mockito.Mockito.times;
23 import static org.mockito.Mockito.verify;
24 import static org.mockito.Mockito.when;
25
26 import java.util.Hashtable;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.stream.Stream;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.junit.jupiter.api.BeforeEach;
33 import org.junit.jupiter.api.Test;
34 import org.junit.jupiter.api.extension.ExtendWith;
35 import org.junit.jupiter.params.ParameterizedTest;
36 import org.junit.jupiter.params.provider.Arguments;
37 import org.junit.jupiter.params.provider.MethodSource;
38 import org.mockito.Mock;
39 import org.mockito.Mockito;
40 import org.mockito.junit.jupiter.MockitoExtension;
41 import org.mockito.junit.jupiter.MockitoSettings;
42 import org.mockito.quality.Strictness;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.i18n.UnitProvider;
45 import org.openhab.core.internal.i18n.I18nProviderImpl;
46 import org.openhab.core.items.GenericItem;
47 import org.openhab.core.items.Item;
48 import org.openhab.core.items.ItemNotFoundException;
49 import org.openhab.core.items.ItemRegistry;
50 import org.openhab.core.library.items.*;
51 import org.openhab.core.library.types.*;
52 import org.openhab.core.library.unit.SIUnits;
53 import org.openhab.core.thing.link.ItemChannelLink;
54 import org.openhab.core.thing.profiles.ProfileCallback;
55 import org.openhab.core.thing.profiles.ProfileContext;
56 import org.openhab.core.types.State;
57 import org.openhab.core.types.UnDefType;
58 import org.osgi.framework.BundleContext;
59 import org.osgi.service.component.ComponentContext;
60
61 /**
62  * Basic unit tests for {@link StateFilterProfile}.
63  *
64  * @author Arne Seime - Initial contribution
65  */
66 @ExtendWith(MockitoExtension.class)
67 @MockitoSettings(strictness = Strictness.WARN)
68 @NonNullByDefault
69 public class StateFilterProfileTest {
70
71     private @Mock @NonNullByDefault({}) ProfileCallback mockCallback;
72     private @Mock @NonNullByDefault({}) ProfileContext mockContext;
73     private @Mock @NonNullByDefault({}) ItemRegistry mockItemRegistry;
74     private @Mock @NonNullByDefault({}) ItemChannelLink mockItemChannelLink;
75
76     private static final UnitProvider UNIT_PROVIDER;
77
78     static {
79         ComponentContext context = Mockito.mock(ComponentContext.class);
80         BundleContext bundleContext = Mockito.mock(BundleContext.class);
81         Hashtable<String, Object> properties = new Hashtable<>();
82         properties.put("measurementSystem", SIUnits.MEASUREMENT_SYSTEM_NAME);
83         when(context.getProperties()).thenReturn(properties);
84         when(context.getBundleContext()).thenReturn(bundleContext);
85         UNIT_PROVIDER = new I18nProviderImpl(context);
86     }
87
88     @BeforeEach
89     public void setup() throws ItemNotFoundException {
90         reset(mockContext);
91         reset(mockCallback);
92         reset(mockItemChannelLink);
93         when(mockCallback.getItemChannelLink()).thenReturn(mockItemChannelLink);
94         when(mockItemRegistry.getItem("")).thenThrow(ItemNotFoundException.class);
95     }
96
97     @Test
98     public void testNoConditions() {
99         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "")));
100         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
101
102         State expectation = OnOffType.ON;
103         profile.onStateUpdateFromHandler(expectation);
104         verify(mockCallback, times(0)).sendUpdate(eq(expectation));
105     }
106
107     @Test
108     public void testMalformedConditions() {
109         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName invalid")));
110         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
111
112         State expectation = OnOffType.ON;
113         profile.onStateUpdateFromHandler(expectation);
114         verify(mockCallback, times(0)).sendUpdate(eq(expectation));
115     }
116
117     @Test
118     public void testInvalidComparatorConditions() throws ItemNotFoundException {
119         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName is Value")));
120         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
121         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value"));
122
123         State expectation = OnOffType.ON;
124         profile.onStateUpdateFromHandler(expectation);
125         verify(mockCallback, times(0)).sendUpdate(eq(expectation));
126     }
127
128     @Test
129     public void testInvalidItemConditions() throws ItemNotFoundException {
130         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value")));
131         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
132
133         when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class);
134         State expectation = OnOffType.ON;
135         profile.onStateUpdateFromHandler(expectation);
136         verify(mockCallback, times(0)).sendUpdate(eq(expectation));
137     }
138
139     @Test
140     public void testInvalidMultipleConditions() throws ItemNotFoundException {
141         when(mockContext.getConfiguration())
142                 .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value,itemname invalid")));
143         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
144         when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class);
145
146         State expectation = OnOffType.ON;
147         profile.onStateUpdateFromHandler(expectation);
148         verify(mockCallback, times(0)).sendUpdate(eq(expectation));
149     }
150
151     @Test
152     public void testSingleConditionMatch() throws ItemNotFoundException {
153         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq 'Value'")));
154         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value"));
155
156         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
157
158         State expectation = new StringType("NewValue");
159         profile.onStateUpdateFromHandler(expectation);
160         verify(mockCallback, times(1)).sendUpdate(eq(expectation));
161     }
162
163     @Test
164     public void testSingleConditionMatchQuoted() throws ItemNotFoundException {
165         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq 'Value'")));
166         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value"));
167
168         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
169
170         State expectation = new StringType("NewValue");
171         profile.onStateUpdateFromHandler(expectation);
172         verify(mockCallback, times(1)).sendUpdate(eq(expectation));
173     }
174
175     private Item stringItemWithState(String itemName, String value) {
176         StringItem item = new StringItem(itemName);
177         item.setState(new StringType(value));
178         return item;
179     }
180
181     private Item numberItemWithState(String itemType, String itemName, State value) {
182         NumberItem item = new NumberItem(itemType, itemName, null);
183         item.setState(value);
184         return item;
185     }
186
187     @Test
188     public void testMultipleCondition_AllMatch() throws ItemNotFoundException {
189         when(mockContext.getConfiguration())
190                 .thenReturn(new Configuration(Map.of("conditions", "ItemName eq 'Value', ItemName2 eq 'Value2'")));
191         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value"));
192         when(mockItemRegistry.getItem("ItemName2")).thenReturn(stringItemWithState("ItemName2", "Value2"));
193
194         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
195
196         State expectation = new StringType("NewValue");
197         profile.onStateUpdateFromHandler(expectation);
198         verify(mockCallback, times(1)).sendUpdate(eq(expectation));
199     }
200
201     @Test
202     public void testMultipleCondition_SingleMatch() throws ItemNotFoundException {
203         when(mockContext.getConfiguration())
204                 .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value, ItemName2 eq Value2")));
205         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value"));
206         when(mockItemRegistry.getItem("ItemName2")).thenThrow(ItemNotFoundException.class);
207
208         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
209
210         State expectation = new StringType("NewValue");
211         profile.onStateUpdateFromHandler(expectation);
212         verify(mockCallback, times(0)).sendUpdate(eq(expectation));
213     }
214
215     @Test
216     public void testFailingConditionWithMismatchState() throws ItemNotFoundException {
217         when(mockContext.getConfiguration())
218                 .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value", "mismatchState", "UNDEF")));
219         when(mockContext.getAcceptedDataTypes()).thenReturn(List.of(UnDefType.class, StringType.class));
220         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Mismatch"));
221
222         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
223
224         profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded"));
225         verify(mockCallback, times(1)).sendUpdate(eq(UnDefType.UNDEF));
226     }
227
228     @Test
229     public void testFailingConditionWithMismatchStateQuoted() throws ItemNotFoundException {
230         when(mockContext.getConfiguration())
231                 .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value", "mismatchState", "'UNDEF'")));
232         when(mockContext.getAcceptedDataTypes()).thenReturn(List.of(UnDefType.class, StringType.class));
233         when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Mismatch"));
234
235         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
236
237         profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded"));
238         verify(mockCallback, times(1)).sendUpdate(eq(new StringType("UNDEF")));
239     }
240
241     @Test
242     void testParseStateNonQuotes() {
243         List<Class<? extends State>> acceptedDataTypes = List.of(UnDefType.class, OnOffType.class, StringType.class);
244
245         when(mockContext.getAcceptedDataTypes()).thenReturn(acceptedDataTypes);
246         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "")));
247
248         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
249         assertEquals(UnDefType.UNDEF, profile.parseState("UNDEF", acceptedDataTypes));
250         assertEquals(new StringType("UNDEF"), profile.parseState("'UNDEF'", acceptedDataTypes));
251         assertEquals(OnOffType.ON, profile.parseState("ON", acceptedDataTypes));
252         assertEquals(new StringType("ON"), profile.parseState("'ON'", acceptedDataTypes));
253     }
254
255     public static Stream<Arguments> testComparingItemWithValue() {
256         NumberItem powerItem = new NumberItem("Number:Power", "ItemName", UNIT_PROVIDER);
257         NumberItem decimalItem = new NumberItem("ItemName");
258         StringItem stringItem = new StringItem("ItemName");
259         SwitchItem switchItem = new SwitchItem("ItemName");
260         DimmerItem dimmerItem = new DimmerItem("ItemName");
261         ContactItem contactItem = new ContactItem("ItemName");
262         RollershutterItem rollershutterItem = new RollershutterItem("ItemName");
263
264         QuantityType q_1500W = QuantityType.valueOf("1500 W");
265         DecimalType d_1500 = DecimalType.valueOf("1500");
266         StringType s_foo = StringType.valueOf("foo");
267         StringType s_NULL = StringType.valueOf("NULL");
268         StringType s_UNDEF = StringType.valueOf("UNDEF");
269         StringType s_OPEN = StringType.valueOf("OPEN");
270
271         return Stream.of( //
272                 // We should be able to check item state is/isn't UNDEF/NULL
273
274                 // First, when the item state is actually an UnDefType
275                 // An unquoted value UNDEF/NULL should be treated as an UnDefType
276                 // Only equality comparisons against the matching UnDefType will return true
277                 // Any other comparisons should return false
278                 Arguments.of(stringItem, UnDefType.UNDEF, "==", "UNDEF", true), //
279                 Arguments.of(dimmerItem, UnDefType.UNDEF, "==", "UNDEF", true), //
280                 Arguments.of(dimmerItem, UnDefType.NULL, "==", "NULL", true), //
281                 Arguments.of(dimmerItem, UnDefType.NULL, "==", "UNDEF", false), //
282                 Arguments.of(decimalItem, UnDefType.NULL, ">", "10", false), //
283                 Arguments.of(decimalItem, UnDefType.NULL, "<", "10", false), //
284                 Arguments.of(decimalItem, UnDefType.NULL, "==", "10", false), //
285                 Arguments.of(decimalItem, UnDefType.NULL, ">=", "10", false), //
286                 Arguments.of(decimalItem, UnDefType.NULL, "<=", "10", false), //
287
288                 // A quoted value (String) isn't UnDefType
289                 Arguments.of(stringItem, UnDefType.UNDEF, "==", "'UNDEF'", false), //
290                 Arguments.of(stringItem, UnDefType.UNDEF, "!=", "'UNDEF'", true), //
291                 Arguments.of(stringItem, UnDefType.NULL, "==", "'NULL'", false), //
292                 Arguments.of(stringItem, UnDefType.NULL, "!=", "'NULL'", true), //
293
294                 // When the item state is not an UnDefType
295                 // UnDefType is special. When unquoted and comparing against a StringItem,
296                 // don't treat it as a string
297                 Arguments.of(stringItem, s_NULL, "==", "'NULL'", true), // Comparing String to String
298                 Arguments.of(stringItem, s_NULL, "==", "NULL", false), // String state != UnDefType
299                 Arguments.of(stringItem, s_NULL, "!=", "NULL", true), //
300                 Arguments.of(stringItem, s_UNDEF, "==", "'UNDEF'", true), // Comparing String to String
301                 Arguments.of(stringItem, s_UNDEF, "==", "UNDEF", false), // String state != UnDefType
302                 Arguments.of(stringItem, s_UNDEF, "!=", "UNDEF", true), //
303
304                 Arguments.of(dimmerItem, PercentType.HUNDRED, "==", "UNDEF", false), //
305                 Arguments.of(dimmerItem, PercentType.HUNDRED, "!=", "UNDEF", true), //
306                 Arguments.of(dimmerItem, PercentType.HUNDRED, "==", "NULL", false), //
307                 Arguments.of(dimmerItem, PercentType.HUNDRED, "!=", "NULL", true), //
308
309                 // Check for OPEN/CLOSED
310                 Arguments.of(contactItem, OpenClosedType.OPEN, "==", "OPEN", true), //
311                 Arguments.of(contactItem, OpenClosedType.OPEN, "!=", "'OPEN'", true), // String != Enum
312                 Arguments.of(contactItem, OpenClosedType.OPEN, "!=", "CLOSED", true), //
313                 Arguments.of(contactItem, OpenClosedType.OPEN, "==", "CLOSED", false), //
314                 Arguments.of(contactItem, OpenClosedType.CLOSED, "==", "CLOSED", true), //
315                 Arguments.of(contactItem, OpenClosedType.CLOSED, "!=", "OPEN", true), //
316
317                 // ON/OFF
318                 Arguments.of(switchItem, OnOffType.ON, "==", "ON", true), //
319                 Arguments.of(switchItem, OnOffType.ON, "!=", "ON", false), //
320                 Arguments.of(switchItem, OnOffType.ON, "!=", "OFF", true), //
321                 Arguments.of(switchItem, OnOffType.ON, "!=", "UNDEF", true), //
322                 Arguments.of(switchItem, UnDefType.UNDEF, "==", "UNDEF", true), //
323                 Arguments.of(switchItem, OnOffType.ON, "==", "'ON'", false), // it's not a string
324                 Arguments.of(switchItem, OnOffType.ON, "!=", "'ON'", true), // incompatible types
325
326                 // Enum types != String
327                 Arguments.of(contactItem, OpenClosedType.OPEN, "==", "'OPEN'", false), //
328                 Arguments.of(contactItem, OpenClosedType.OPEN, "!=", "'CLOSED'", true), //
329                 Arguments.of(contactItem, OpenClosedType.OPEN, "!=", "'OPEN'", true), //
330                 Arguments.of(contactItem, OpenClosedType.OPEN, "==", "'CLOSED'", false), //
331                 Arguments.of(contactItem, OpenClosedType.CLOSED, "==", "'CLOSED'", false), //
332                 Arguments.of(contactItem, OpenClosedType.CLOSED, "!=", "'CLOSED'", true), //
333
334                 // non UnDefType checks
335                 // String constants must be quoted
336                 Arguments.of(stringItem, s_foo, "==", "'foo'", true), //
337                 Arguments.of(stringItem, s_foo, "==", "foo", false), //
338                 Arguments.of(stringItem, s_foo, "!=", "foo", true), // not quoted -> not a string
339                 Arguments.of(stringItem, s_foo, "<>", "foo", true), //
340                 Arguments.of(stringItem, s_foo, " <>", "foo", true), //
341                 Arguments.of(stringItem, s_foo, "<> ", "foo", true), //
342                 Arguments.of(stringItem, s_foo, " <> ", "foo", true), //
343                 Arguments.of(stringItem, s_foo, "!=", "'foo'", false), //
344                 Arguments.of(stringItem, s_foo, "<>", "'foo'", false), //
345                 Arguments.of(stringItem, s_foo, " <>", "'foo'", false), //
346
347                 Arguments.of(dimmerItem, PercentType.HUNDRED, "==", "100", true), //
348                 Arguments.of(dimmerItem, PercentType.HUNDRED, ">=", "100", true), //
349                 Arguments.of(dimmerItem, PercentType.HUNDRED, ">", "50", true), //
350                 Arguments.of(dimmerItem, PercentType.HUNDRED, ">=", "50", true), //
351                 Arguments.of(dimmerItem, PercentType.ZERO, "<", "50", true), //
352                 Arguments.of(dimmerItem, PercentType.ZERO, ">=", "50", false), //
353                 Arguments.of(dimmerItem, PercentType.ZERO, ">=", "0", true), //
354                 Arguments.of(dimmerItem, PercentType.ZERO, "<", "0", false), //
355                 Arguments.of(dimmerItem, PercentType.ZERO, "<=", "0", true), //
356
357                 // Numeric vs Strings aren't comparable
358                 Arguments.of(rollershutterItem, PercentType.HUNDRED, "==", "'100'", false), //
359                 Arguments.of(rollershutterItem, PercentType.HUNDRED, "!=", "'100'", true), //
360                 Arguments.of(rollershutterItem, PercentType.HUNDRED, ">", "'10'", false), //
361                 Arguments.of(powerItem, q_1500W, "==", "'1500 W'", false), // QuantityType vs String => fail
362                 Arguments.of(decimalItem, d_1500, "==", "'1500'", false), //
363
364                 // Compatible type castings are supported
365                 Arguments.of(dimmerItem, PercentType.ZERO, "==", "OFF", true), //
366                 Arguments.of(dimmerItem, PercentType.ZERO, "==", "ON", false), //
367                 Arguments.of(dimmerItem, PercentType.ZERO, "!=", "ON", true), //
368                 Arguments.of(dimmerItem, PercentType.ZERO, "!=", "OFF", false), //
369                 Arguments.of(dimmerItem, PercentType.HUNDRED, "==", "ON", true), //
370                 Arguments.of(dimmerItem, PercentType.HUNDRED, "==", "OFF", false), //
371                 Arguments.of(dimmerItem, PercentType.HUNDRED, "!=", "ON", false), //
372                 Arguments.of(dimmerItem, PercentType.HUNDRED, "!=", "OFF", true), //
373
374                 // UpDownType gets converted to PercentType for comparison
375                 Arguments.of(rollershutterItem, PercentType.HUNDRED, "==", "DOWN", true), //
376                 Arguments.of(rollershutterItem, PercentType.HUNDRED, "==", "UP", false), //
377                 Arguments.of(rollershutterItem, PercentType.HUNDRED, "!=", "UP", true), //
378                 Arguments.of(rollershutterItem, PercentType.ZERO, "==", "UP", true), //
379                 Arguments.of(rollershutterItem, PercentType.ZERO, "!=", "DOWN", true), //
380
381                 Arguments.of(decimalItem, d_1500, " eq ", "1500", true), //
382                 Arguments.of(decimalItem, d_1500, " eq ", "1500", true), //
383                 Arguments.of(decimalItem, d_1500, "==", "1500", true), //
384                 Arguments.of(decimalItem, d_1500, " ==", "1500", true), //
385                 Arguments.of(decimalItem, d_1500, "== ", "1500", true), //
386                 Arguments.of(decimalItem, d_1500, " == ", "1500", true), //
387
388                 Arguments.of(powerItem, q_1500W, " eq ", "1500", false), // no unit => fail
389                 Arguments.of(powerItem, q_1500W, "==", "1500", false), // no unit => fail
390                 Arguments.of(powerItem, q_1500W, " eq ", "1500 cm", false), // wrong unit
391                 Arguments.of(powerItem, q_1500W, "==", "1500 cm", false), // wrong unit
392
393                 Arguments.of(powerItem, q_1500W, " eq ", "1500 W", true), //
394                 Arguments.of(powerItem, q_1500W, " eq ", "1.5 kW", true), //
395                 Arguments.of(powerItem, q_1500W, " eq ", "2 kW", false), //
396                 Arguments.of(powerItem, q_1500W, "==", "1500 W", true), //
397                 Arguments.of(powerItem, q_1500W, "==", "1.5 kW", true), //
398                 Arguments.of(powerItem, q_1500W, "==", "2 kW", false), //
399
400                 Arguments.of(powerItem, q_1500W, " neq ", "500 W", true), //
401                 Arguments.of(powerItem, q_1500W, " neq ", "1500", true), // Not the same type, so not equal
402                 Arguments.of(powerItem, q_1500W, " neq ", "1500 W", false), //
403                 Arguments.of(powerItem, q_1500W, " neq ", "1.5 kW", false), //
404                 Arguments.of(powerItem, q_1500W, "!=", "500 W", true), //
405                 Arguments.of(powerItem, q_1500W, "!=", "1500", true), // not the same type
406                 Arguments.of(powerItem, q_1500W, "!=", "1500 W", false), //
407                 Arguments.of(powerItem, q_1500W, "!=", "1.5 kW", false), //
408
409                 Arguments.of(powerItem, q_1500W, " GT ", "100 W", true), //
410                 Arguments.of(powerItem, q_1500W, " GT ", "1 kW", true), //
411                 Arguments.of(powerItem, q_1500W, " GT ", "2 kW", false), //
412                 Arguments.of(powerItem, q_1500W, ">", "100 W", true), //
413                 Arguments.of(powerItem, q_1500W, ">", "1 kW", true), //
414                 Arguments.of(powerItem, q_1500W, ">", "2 kW", false), //
415                 Arguments.of(powerItem, q_1500W, " GTE ", "1500 W", true), //
416                 Arguments.of(powerItem, q_1500W, " GTE ", "1 kW", true), //
417                 Arguments.of(powerItem, q_1500W, " GTE ", "1.5 kW", true), //
418                 Arguments.of(powerItem, q_1500W, " GTE ", "2 kW", false), //
419                 Arguments.of(powerItem, q_1500W, " GTE ", "2000 mW", true), //
420                 Arguments.of(powerItem, q_1500W, " GTE ", "20", false), // no unit
421                 Arguments.of(powerItem, q_1500W, ">=", "1.5 kW", true), //
422                 Arguments.of(powerItem, q_1500W, ">=", "2 kW", false), //
423                 Arguments.of(powerItem, q_1500W, " LT ", "2 kW", true), //
424                 Arguments.of(powerItem, q_1500W, "<", "2 kW", true), //
425                 Arguments.of(powerItem, q_1500W, " LTE ", "2 kW", true), //
426                 Arguments.of(powerItem, q_1500W, "<=", "2 kW", true), //
427                 Arguments.of(powerItem, q_1500W, "<=", "1 kW", false), //
428                 Arguments.of(powerItem, q_1500W, " LTE ", "1.5 kW", true), //
429                 Arguments.of(powerItem, q_1500W, "<=", "1.5 kW", true) //
430         );
431     }
432
433     @ParameterizedTest
434     @MethodSource
435     public void testComparingItemWithValue(GenericItem item, State state, String operator, String value,
436             boolean expected) throws ItemNotFoundException {
437         String itemName = item.getName();
438         item.setState(state);
439
440         when(mockContext.getConfiguration())
441                 .thenReturn(new Configuration(Map.of("conditions", itemName + operator + value)));
442         when(mockItemRegistry.getItem(itemName)).thenReturn(item);
443
444         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
445
446         State inputData = new StringType("NewValue");
447         profile.onStateUpdateFromHandler(inputData);
448         verify(mockCallback, times(expected ? 1 : 0)).sendUpdate(eq(inputData));
449     }
450
451     public static Stream<Arguments> testComparingItemWithOtherItem() {
452         NumberItem powerItem = new NumberItem("Number:Power", "powerItem", UNIT_PROVIDER);
453         NumberItem powerItem2 = new NumberItem("Number:Power", "powerItem2", UNIT_PROVIDER);
454         NumberItem decimalItem = new NumberItem("decimalItem");
455         NumberItem decimalItem2 = new NumberItem("decimalItem2");
456         StringItem stringItem = new StringItem("stringItem");
457         StringItem stringItem2 = new StringItem("stringItem2");
458         ContactItem contactItem = new ContactItem("contactItem");
459         ContactItem contactItem2 = new ContactItem("contactItem2");
460
461         QuantityType q_1500W = QuantityType.valueOf("1500 W");
462         QuantityType q_1_5kW = QuantityType.valueOf("1.5 kW");
463         QuantityType q_10kW = QuantityType.valueOf("10 kW");
464
465         DecimalType d_1500 = DecimalType.valueOf("1500");
466         DecimalType d_2000 = DecimalType.valueOf("2000");
467         StringType s_1500 = StringType.valueOf("1500");
468         StringType s_foo = StringType.valueOf("foo");
469         StringType s_NULL = StringType.valueOf("NULL");
470
471         return Stream.of( //
472                 Arguments.of(stringItem, s_foo, "==", stringItem2, s_foo, true), //
473                 Arguments.of(stringItem, s_foo, "!=", stringItem2, s_foo, false), //
474                 Arguments.of(stringItem, s_foo, "==", stringItem2, s_NULL, false), //
475                 Arguments.of(stringItem, s_foo, "!=", stringItem2, s_NULL, true), //
476
477                 Arguments.of(decimalItem, d_1500, "==", decimalItem2, d_1500, true), //
478                 Arguments.of(decimalItem, d_1500, "==", decimalItem2, d_1500, true), //
479
480                 // UNDEF/NULL are equals regardless of item type
481                 Arguments.of(decimalItem, UnDefType.UNDEF, "==", stringItem, UnDefType.UNDEF, true), //
482                 Arguments.of(decimalItem, UnDefType.NULL, "==", stringItem, UnDefType.NULL, true), //
483                 Arguments.of(decimalItem, UnDefType.NULL, "==", stringItem, UnDefType.UNDEF, false), //
484
485                 Arguments.of(contactItem, OpenClosedType.OPEN, "==", contactItem2, OpenClosedType.OPEN, true), //
486                 Arguments.of(contactItem, OpenClosedType.OPEN, "==", contactItem2, OpenClosedType.CLOSED, false), //
487
488                 Arguments.of(decimalItem, d_1500, "==", decimalItem2, d_1500, true), //
489                 Arguments.of(decimalItem, d_1500, "<", decimalItem2, d_2000, true), //
490                 Arguments.of(decimalItem, d_1500, ">", decimalItem2, d_2000, false), //
491                 Arguments.of(decimalItem, d_1500, ">", stringItem, s_1500, false), //
492                 Arguments.of(powerItem, q_1500W, "<", powerItem2, q_10kW, true), //
493                 Arguments.of(powerItem, q_1500W, ">", powerItem2, q_10kW, false), //
494                 Arguments.of(powerItem, q_1500W, "==", powerItem2, q_1_5kW, true), //
495                 Arguments.of(powerItem, q_1500W, ">=", powerItem2, q_1_5kW, true), //
496                 Arguments.of(powerItem, q_1500W, ">", powerItem2, q_1_5kW, false), //
497
498                 // Incompatible types
499                 Arguments.of(decimalItem, d_1500, "==", stringItem, s_1500, false), //
500                 Arguments.of(powerItem, q_1500W, "==", decimalItem, d_1500, false), // DecimalType != QuantityType
501                 Arguments.of(decimalItem, d_1500, "==", powerItem, q_1500W, false) //
502         );
503     }
504
505     @ParameterizedTest
506     @MethodSource
507     public void testComparingItemWithOtherItem(GenericItem item, State state, String operator, GenericItem item2,
508             State state2, boolean expected) throws ItemNotFoundException {
509         String itemName = item.getName();
510         item.setState(state);
511
512         String itemName2 = item2.getName();
513         item2.setState(state2);
514
515         if (item.equals(item2)) {
516             // For test writers:
517             // When using the same items, it doesn't make sense for their states to be different
518             assertEquals(state, state2);
519         }
520
521         when(mockContext.getConfiguration())
522                 .thenReturn(new Configuration(Map.of("conditions", itemName + operator + itemName2)));
523         when(mockItemRegistry.getItem(itemName)).thenReturn(item);
524         when(mockItemRegistry.getItem(itemName2)).thenReturn(item2);
525
526         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
527
528         State inputData = new StringType("NewValue");
529         profile.onStateUpdateFromHandler(inputData);
530         verify(mockCallback, times(expected ? 1 : 0)).sendUpdate(eq(inputData));
531     }
532
533     public static Stream<Arguments> testComparingInputStateWithValue() {
534         NumberItem powerItem = new NumberItem("Number:Power", "ItemName", UNIT_PROVIDER);
535         NumberItem decimalItem = new NumberItem("ItemName");
536         StringItem stringItem = new StringItem("ItemName");
537         DimmerItem dimmerItem = new DimmerItem("ItemName");
538
539         QuantityType q_1500W = QuantityType.valueOf("1500 W");
540         DecimalType d_1500 = DecimalType.valueOf("1500");
541         StringType s_foo = StringType.valueOf("foo");
542
543         return Stream.of( //
544                 // We should be able to check that input state is/isn't UNDEF/NULL
545
546                 // First, when the input state is actually an UnDefType
547                 // An unquoted value UNDEF/NULL should be treated as an UnDefType
548                 Arguments.of(stringItem, UnDefType.UNDEF, "==", "UNDEF", true), //
549                 Arguments.of(dimmerItem, UnDefType.NULL, "==", "NULL", true), //
550                 Arguments.of(dimmerItem, UnDefType.NULL, "==", "UNDEF", false), //
551
552                 // A quoted value (String) isn't UnDefType
553                 Arguments.of(stringItem, UnDefType.UNDEF, "==", "'UNDEF'", false), //
554                 Arguments.of(stringItem, UnDefType.UNDEF, "!=", "'UNDEF'", true), //
555                 Arguments.of(stringItem, UnDefType.NULL, "==", "'NULL'", false), //
556                 Arguments.of(stringItem, UnDefType.NULL, "!=", "'NULL'", true), //
557
558                 // String values must be quoted
559                 Arguments.of(stringItem, s_foo, "==", "'foo'", true), //
560                 Arguments.of(stringItem, s_foo, "!=", "'foo'", false), //
561                 Arguments.of(stringItem, s_foo, "==", "'bar'", false), //
562                 // Unquoted string values are not compatible
563                 // always returns false
564                 Arguments.of(stringItem, s_foo, "==", "foo", false), //
565                 Arguments.of(stringItem, s_foo, "!=", "foo", true), // not quoted -> not equal to string
566
567                 Arguments.of(decimalItem, d_1500, "==", "1500", true), //
568                 Arguments.of(decimalItem, d_1500, "!=", "1500", false), //
569                 Arguments.of(decimalItem, d_1500, "==", "1000", false), //
570                 Arguments.of(decimalItem, d_1500, "!=", "1000", true), //
571                 Arguments.of(decimalItem, d_1500, ">", "1000", true), //
572                 Arguments.of(decimalItem, d_1500, ">=", "1000", true), //
573                 Arguments.of(decimalItem, d_1500, ">=", "1500", true), //
574                 Arguments.of(decimalItem, d_1500, "<", "1600", true), //
575                 Arguments.of(decimalItem, d_1500, "<=", "1600", true), //
576                 Arguments.of(decimalItem, d_1500, "<", "1000", false), //
577                 Arguments.of(decimalItem, d_1500, "<=", "1000", false), //
578                 Arguments.of(decimalItem, d_1500, "<", "1500", false), //
579                 Arguments.of(decimalItem, d_1500, "<=", "1500", true), //
580
581                 // named operators - must have a trailing space
582                 Arguments.of(decimalItem, d_1500, "LT ", "2000", true), //
583                 Arguments.of(decimalItem, d_1500, "LTE ", "1500", true), //
584                 Arguments.of(decimalItem, d_1500, " LTE ", "1500", true), //
585                 Arguments.of(decimalItem, d_1500, " LTE ", "1500", true), //
586
587                 Arguments.of(powerItem, q_1500W, "==", "1500 W", true), //
588                 Arguments.of(powerItem, q_1500W, "==", "'1500 W'", false), // QuantityType != String
589                 Arguments.of(powerItem, q_1500W, "==", "1.5 kW", true), //
590                 Arguments.of(powerItem, q_1500W, ">", "2000 mW", true) //
591         );
592     }
593
594     @ParameterizedTest
595     @MethodSource
596     public void testComparingInputStateWithValue(GenericItem linkedItem, State inputState, String operator,
597             String value, boolean expected) throws ItemNotFoundException {
598
599         String linkedItemName = linkedItem.getName();
600
601         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", operator + value)));
602         when(mockItemRegistry.getItem(linkedItemName)).thenReturn(linkedItem);
603         when(mockItemChannelLink.getItemName()).thenReturn(linkedItemName);
604
605         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
606
607         profile.onStateUpdateFromHandler(inputState);
608         verify(mockCallback, times(expected ? 1 : 0)).sendUpdate(eq(inputState));
609     }
610
611     @ParameterizedTest
612     @MethodSource("testComparingItemWithOtherItem")
613     public void testComparingInputStateWithItem(GenericItem linkedItem, State inputState, String operator,
614             GenericItem item, State state, boolean expected) throws ItemNotFoundException {
615         String linkedItemName = linkedItem.getName();
616
617         String itemName = item.getName();
618
619         when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", operator + itemName)));
620         when(mockItemRegistry.getItem(itemName)).thenReturn(item);
621         when(mockItemRegistry.getItem(linkedItemName)).thenReturn(linkedItem);
622         when(mockItemChannelLink.getItemName()).thenReturn(linkedItemName);
623
624         StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry);
625         item.setState(UnDefType.UNDEF);
626
627         profile.onStateUpdateFromHandler(inputState);
628         reset(mockCallback);
629         when(mockCallback.getItemChannelLink()).thenReturn(mockItemChannelLink);
630
631         item.setState(state);
632         profile.onStateUpdateFromHandler(inputState);
633         verify(mockCallback, times(expected ? 1 : 0)).sendUpdate(eq(inputState));
634     }
635 }