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.hue.internal.clip2;
15 import static org.junit.jupiter.api.Assertions.*;
17 import java.io.BufferedReader;
18 import java.io.FileReader;
19 import java.io.IOException;
20 import java.lang.reflect.Field;
21 import java.util.List;
22 import java.util.Optional;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.junit.jupiter.api.Test;
27 import org.openhab.binding.hue.internal.dto.clip2.ActionEntry;
28 import org.openhab.binding.hue.internal.dto.clip2.Alerts;
29 import org.openhab.binding.hue.internal.dto.clip2.Button;
30 import org.openhab.binding.hue.internal.dto.clip2.Dimming;
31 import org.openhab.binding.hue.internal.dto.clip2.Event;
32 import org.openhab.binding.hue.internal.dto.clip2.LightLevel;
33 import org.openhab.binding.hue.internal.dto.clip2.MetaData;
34 import org.openhab.binding.hue.internal.dto.clip2.MirekSchema;
35 import org.openhab.binding.hue.internal.dto.clip2.Motion;
36 import org.openhab.binding.hue.internal.dto.clip2.Power;
37 import org.openhab.binding.hue.internal.dto.clip2.ProductData;
38 import org.openhab.binding.hue.internal.dto.clip2.RelativeRotary;
39 import org.openhab.binding.hue.internal.dto.clip2.Resource;
40 import org.openhab.binding.hue.internal.dto.clip2.ResourceReference;
41 import org.openhab.binding.hue.internal.dto.clip2.Resources;
42 import org.openhab.binding.hue.internal.dto.clip2.Rotation;
43 import org.openhab.binding.hue.internal.dto.clip2.RotationEvent;
44 import org.openhab.binding.hue.internal.dto.clip2.Temperature;
45 import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
46 import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype;
47 import org.openhab.binding.hue.internal.dto.clip2.enums.BatteryStateType;
48 import org.openhab.binding.hue.internal.dto.clip2.enums.ButtonEventType;
49 import org.openhab.binding.hue.internal.dto.clip2.enums.DirectionType;
50 import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
51 import org.openhab.binding.hue.internal.dto.clip2.enums.RotationEventType;
52 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
53 import org.openhab.binding.hue.internal.dto.clip2.helper.Setters;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.HSBType;
56 import org.openhab.core.library.types.OnOffType;
57 import org.openhab.core.library.types.PercentType;
58 import org.openhab.core.library.types.QuantityType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.UnDefType;
62 import org.openhab.core.util.ColorUtil;
64 import com.google.gson.Gson;
65 import com.google.gson.JsonElement;
66 import com.google.gson.JsonObject;
67 import com.google.gson.JsonParser;
68 import com.google.gson.JsonSyntaxException;
71 * JUnit test for CLIP 2 DTOs.
73 * @author Andrew Fiddian-Green - Initial contribution
78 private static final Gson GSON = new Gson();
79 private static final Double MINIMUM_DIMMING_LEVEL = Double.valueOf(12.34f);
82 * Load the test JSON payload string from a file
84 private String load(String fileName) {
85 try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
86 BufferedReader reader = new BufferedReader(file)) {
87 StringBuilder builder = new StringBuilder();
89 while ((line = reader.readLine()) != null) {
90 builder.append(line).append("\n");
92 return builder.toString();
93 } catch (IOException e) {
101 String json = load(ResourceType.BUTTON.name().toLowerCase());
102 Resources resources = GSON.fromJson(json, Resources.class);
103 assertNotNull(resources);
104 List<Resource> list = resources.getResources();
106 assertEquals(43, list.size());
107 Resource item = list.get(0);
108 assertEquals(ResourceType.BUTTON, item.getType());
109 Button button = item.getButton();
110 assertNotNull(button);
111 assertEquals(ButtonEventType.SHORT_RELEASE, button.getLastEvent());
116 String json = load(ResourceType.DEVICE.name().toLowerCase());
117 Resources resources = GSON.fromJson(json, Resources.class);
118 assertNotNull(resources);
119 List<Resource> list = resources.getResources();
121 assertEquals(34, list.size());
122 boolean itemFound = false;
123 for (Resource item : list) {
124 assertEquals(ResourceType.DEVICE, item.getType());
125 ProductData productData = item.getProductData();
126 assertNotNull(productData);
127 if (productData.getProductArchetype() == Archetype.BRIDGE_V2) {
129 assertEquals("BSB002", productData.getModelId());
130 assertEquals("Signify Netherlands B.V.", productData.getManufacturerName());
131 assertEquals("Philips hue", productData.getProductName());
132 assertNull(productData.getHardwarePlatformType());
133 assertTrue(productData.getCertified());
134 assertEquals("1.53.1953188020", productData.getSoftwareVersion());
138 assertTrue(itemFound);
142 void testDevicePower() {
143 String json = load(ResourceType.DEVICE_POWER.name().toLowerCase());
144 Resources resources = GSON.fromJson(json, Resources.class);
145 assertNotNull(resources);
146 List<Resource> list = resources.getResources();
148 assertEquals(16, list.size());
149 Resource item = list.get(0);
150 assertEquals(ResourceType.DEVICE_POWER, item.getType());
151 Power power = item.getPowerState();
152 assertNotNull(power);
153 assertEquals(60, power.getBatteryLevel());
154 assertEquals(BatteryStateType.NORMAL, power.getBatteryState());
158 void testGroupedLight() {
159 String json = load(ResourceType.GROUPED_LIGHT.name().toLowerCase());
160 Resources resources = GSON.fromJson(json, Resources.class);
161 assertNotNull(resources);
162 List<Resource> list = resources.getResources();
164 assertEquals(15, list.size());
166 for (Resource item : list) {
167 assertEquals(ResourceType.GROUPED_LIGHT, item.getType());
169 switch (item.getId()) {
170 case "db4fd630-3798-40de-b642-c1ef464bf770":
172 assertEquals(OnOffType.OFF, item.getOnOffState());
173 assertEquals(PercentType.ZERO, item.getBrightnessState());
174 alert = item.getAlerts();
175 assertNotNull(alert);
176 for (ActionType actionValue : alert.getActionValues()) {
177 assertEquals(ActionType.BREATHE, actionValue);
180 case "9228d710-3c54-4ae4-8c88-bfe57d8fd220":
182 assertEquals(OnOffType.ON, item.getOnOffState());
183 assertEquals(PercentType.HUNDRED, item.getBrightnessState());
184 alert = item.getAlerts();
185 assertNotNull(alert);
186 for (ActionType actionValue : alert.getActionValues()) {
187 assertEquals(ActionType.BREATHE, actionValue);
193 assertEquals(2, itemsFound);
198 String json = load(ResourceType.LIGHT.name().toLowerCase());
199 Resources resources = GSON.fromJson(json, Resources.class);
200 assertNotNull(resources);
201 List<Resource> list = resources.getResources();
203 assertEquals(17, list.size());
204 int itemFoundCount = 0;
205 for (Resource item : list) {
206 assertEquals(ResourceType.LIGHT, item.getType());
207 MetaData metaData = item.getMetaData();
208 assertNotNull(metaData);
209 String name = metaData.getName();
212 if (name.contains("Bay Window Lamp")) {
214 assertEquals(ResourceType.LIGHT, item.getType());
215 assertEquals(OnOffType.OFF, item.getOnOffState());
216 state = item.getBrightnessState();
217 assertTrue(state instanceof PercentType);
218 assertEquals(0, ((PercentType) state).doubleValue(), 0.1);
219 item.setOnOff(OnOffType.ON);
220 state = item.getBrightnessState();
221 assertTrue(state instanceof PercentType);
222 assertEquals(93.0, ((PercentType) state).doubleValue(), 0.1);
223 assertEquals(UnDefType.UNDEF, item.getColorTemperaturePercentState());
224 state = item.getColorState();
225 assertTrue(state instanceof HSBType);
226 double[] xy = ColorUtil.hsbToXY((HSBType) state);
227 assertEquals(0.6367, xy[0], 0.01); // note: rounding errors !!
228 assertEquals(0.3503, xy[1], 0.01); // note: rounding errors !!
229 assertEquals(item.getBrightnessState(), ((HSBType) state).getBrightness());
230 Alerts alert = item.getAlerts();
231 assertNotNull(alert);
232 for (ActionType actionValue : alert.getActionValues()) {
233 assertEquals(ActionType.BREATHE, actionValue);
236 if (name.contains("Table Lamp A")) {
238 assertEquals(ResourceType.LIGHT, item.getType());
239 assertEquals(OnOffType.OFF, item.getOnOffState());
240 state = item.getBrightnessState();
241 assertTrue(state instanceof PercentType);
242 assertEquals(0, ((PercentType) state).doubleValue(), 0.1);
243 item.setOnOff(OnOffType.ON);
244 state = item.getBrightnessState();
245 assertTrue(state instanceof PercentType);
246 assertEquals(56.7, ((PercentType) state).doubleValue(), 0.1);
247 MirekSchema mirekSchema = item.getMirekSchema();
248 assertNotNull(mirekSchema);
249 assertEquals(153, mirekSchema.getMirekMinimum());
250 assertEquals(454, mirekSchema.getMirekMaximum());
252 // test color temperature percent value on light's own scale
253 state = item.getColorTemperaturePercentState();
254 assertTrue(state instanceof PercentType);
255 assertEquals(96.3, ((PercentType) state).doubleValue(), 0.1);
256 state = item.getColorTemperatureAbsoluteState();
257 assertTrue(state instanceof QuantityType<?>);
258 assertEquals(2257.3, ((QuantityType<?>) state).doubleValue(), 0.1);
260 // test color temperature percent value on the default (full) scale
261 MirekSchema temp = item.getMirekSchema();
262 item.setMirekSchema(MirekSchema.DEFAULT_SCHEMA);
263 state = item.getColorTemperaturePercentState();
264 assertTrue(state instanceof PercentType);
265 assertEquals(83.6, ((PercentType) state).doubleValue(), 0.1);
266 state = item.getColorTemperatureAbsoluteState();
267 assertTrue(state instanceof QuantityType<?>);
268 assertEquals(2257.3, ((QuantityType<?>) state).doubleValue(), 0.1);
269 item.setMirekSchema(temp);
271 // change colour temperature percent to zero
272 Setters.setColorTemperaturePercent(item, PercentType.ZERO, null);
273 assertEquals(PercentType.ZERO, item.getColorTemperaturePercentState());
274 state = item.getColorTemperatureAbsoluteState();
275 assertTrue(state instanceof QuantityType<?>);
276 assertEquals(6535.9, ((QuantityType<?>) state).doubleValue(), 0.1);
278 // change colour temperature percent to 100
279 Setters.setColorTemperaturePercent(item, PercentType.HUNDRED, null);
280 assertEquals(PercentType.HUNDRED, item.getColorTemperaturePercentState());
281 state = item.getColorTemperatureAbsoluteState();
282 assertTrue(state instanceof QuantityType<?>);
283 assertEquals(2202.6, ((QuantityType<?>) state).doubleValue(), 0.1);
285 // change colour temperature kelvin to 4000 K
286 Setters.setColorTemperatureAbsolute(item, QuantityType.valueOf("4000 K"), null);
287 state = item.getColorTemperaturePercentState();
288 assertTrue(state instanceof PercentType);
289 assertEquals(32.2, ((PercentType) state).doubleValue(), 0.1);
290 assertEquals(QuantityType.valueOf("4000 K"), item.getColorTemperatureAbsoluteState());
292 assertEquals(UnDefType.NULL, item.getColorState());
293 Alerts alert = item.getAlerts();
294 assertNotNull(alert);
295 for (ActionType actionValue : alert.getActionValues()) {
296 assertEquals(ActionType.BREATHE, actionValue);
300 assertEquals(2, itemFoundCount);
304 void testLightLevel() {
305 String json = load(ResourceType.LIGHT_LEVEL.name().toLowerCase());
306 Resources resources = GSON.fromJson(json, Resources.class);
307 assertNotNull(resources);
308 List<Resource> list = resources.getResources();
310 assertEquals(1, list.size());
311 Resource item = list.get(0);
312 assertEquals(ResourceType.LIGHT_LEVEL, item.getType());
313 Boolean enabled = item.getEnabled();
314 assertNotNull(enabled);
316 LightLevel lightLevel = item.getLightLevel();
317 assertNotNull(lightLevel);
318 assertEquals(12725, lightLevel.getLightLevel());
319 assertTrue(lightLevel.isLightLevelValid());
323 void testRelativeRotary() {
324 String json = load(ResourceType.RELATIVE_ROTARY.name().toLowerCase());
325 Resources resources = GSON.fromJson(json, Resources.class);
326 assertNotNull(resources);
327 List<Resource> list = resources.getResources();
329 assertEquals(1, list.size());
330 Resource item = list.get(0);
331 assertEquals(ResourceType.RELATIVE_ROTARY, item.getType());
332 RelativeRotary relativeRotary = item.getRelativeRotary();
333 assertNotNull(relativeRotary);
334 RotationEvent rotationEvent = relativeRotary.getLastEvent();
335 assertNotNull(rotationEvent);
336 assertEquals(RotationEventType.REPEAT, rotationEvent.getAction());
337 Rotation rotation = rotationEvent.getRotation();
338 assertNotNull(rotation);
339 assertEquals(DirectionType.CLOCK_WISE, rotation.getDirection());
340 assertEquals(400, rotation.getDuration());
341 assertEquals(30, rotation.getSteps());
342 assertEquals(new DecimalType(30), relativeRotary.getStepsState());
343 assertEquals(new StringType(ButtonEventType.REPEAT.name()), relativeRotary.getActionState());
347 void testResourceMerging() {
348 // create resource one
349 Resource one = new Resource(ResourceType.LIGHT).setId("AARDVARK");
351 // preset the minimum dimming level
353 Dimming dimming = new Dimming().setMinimumDimmingLevel(MINIMUM_DIMMING_LEVEL);
354 Field dimming2 = one.getClass().getDeclaredField("dimming");
355 dimming2.setAccessible(true);
356 dimming2.set(one, dimming);
357 } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
360 Setters.setColorXy(one, HSBType.RED, null);
361 Setters.setDimming(one, PercentType.HUNDRED, null);
362 assertTrue(one.getColorState() instanceof HSBType);
363 assertEquals(PercentType.HUNDRED, one.getBrightnessState());
364 assertTrue(HSBType.RED.closeTo((HSBType) one.getColorState(), 0.01));
366 // switching off should change HSB and Brightness
367 one.setOnOff(OnOffType.OFF);
368 assertEquals(0, ((HSBType) one.getColorState()).getBrightness().doubleValue(), 0.01);
369 assertEquals(PercentType.ZERO, one.getBrightnessState());
370 one.setOnOff(OnOffType.ON);
372 // setting brightness to zero should change it to the minimum dimming level
373 Setters.setDimming(one, PercentType.ZERO, null);
374 assertEquals(MINIMUM_DIMMING_LEVEL, ((HSBType) one.getColorState()).getBrightness().doubleValue(), 0.01);
375 assertEquals(MINIMUM_DIMMING_LEVEL, ((PercentType) one.getBrightnessState()).doubleValue(), 0.01);
376 one.setOnOff(OnOffType.ON);
378 // null its Dimming field
380 Field dimming = one.getClass().getDeclaredField("dimming");
381 dimming.setAccessible(true);
382 dimming.set(one, null);
383 } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
387 // confirm that brightness is no longer valid, and therefore that color has also changed
388 assertEquals(UnDefType.NULL, one.getBrightnessState());
389 assertTrue(one.getColorState() instanceof HSBType);
390 assertTrue((new HSBType(DecimalType.ZERO, PercentType.HUNDRED, new PercentType(50)))
391 .closeTo((HSBType) one.getColorState(), 0.01));
393 PercentType testBrightness = new PercentType(42);
395 // create resource two
396 Resource two = new Resource(ResourceType.DEVICE).setId("ALLIGATOR");
398 Setters.setDimming(two, testBrightness, null);
399 assertEquals(UnDefType.NULL, two.getColorState());
400 assertEquals(testBrightness, two.getBrightnessState());
403 Setters.setResource(one, two);
405 // confirm that brightness and color are both once more valid
406 assertEquals("AARDVARK", one.getId());
407 assertEquals(ResourceType.LIGHT, one.getType());
408 assertEquals(testBrightness, one.getBrightnessState());
409 assertTrue(one.getColorState() instanceof HSBType);
410 assertTrue((new HSBType(DecimalType.ZERO, PercentType.HUNDRED, testBrightness))
411 .closeTo((HSBType) one.getColorState(), 0.01));
415 void testRoomGroup() {
416 String json = load(ResourceType.ROOM.name().toLowerCase());
417 Resources resources = GSON.fromJson(json, Resources.class);
418 assertNotNull(resources);
419 List<Resource> list = resources.getResources();
421 assertEquals(6, list.size());
422 Resource item = list.get(0);
423 assertEquals(ResourceType.ROOM, item.getType());
424 List<ResourceReference> children = item.getChildren();
425 assertEquals(2, children.size());
426 ResourceReference child = children.get(0);
427 assertNotNull(child);
428 assertEquals("0d47bd3d-d82b-4a21-893c-299bff18e22a", child.getId());
429 assertEquals(ResourceType.DEVICE, child.getType());
430 List<ResourceReference> services = item.getServiceReferences();
431 assertEquals(1, services.size());
432 ResourceReference service = services.get(0);
433 assertNotNull(service);
434 assertEquals("08947162-67be-4ed5-bfce-f42dade42416", service.getId());
435 assertEquals(ResourceType.GROUPED_LIGHT, service.getType());
440 String json = load(ResourceType.SCENE.name().toLowerCase());
441 Resources resources = GSON.fromJson(json, Resources.class);
442 assertNotNull(resources);
443 List<Resource> list = resources.getResources();
445 assertEquals(123, list.size());
446 Resource item = list.get(0);
447 List<ActionEntry> actions = item.getActions();
448 assertNotNull(actions);
449 assertEquals(3, actions.size());
450 ActionEntry actionEntry = actions.get(0);
451 assertNotNull(actionEntry);
452 Resource action = actionEntry.getAction();
453 assertNotNull(action);
454 assertEquals(OnOffType.ON, action.getOnOffState());
458 void testSensor2Motion() {
459 String json = load(ResourceType.MOTION.name().toLowerCase());
460 Resources resources = GSON.fromJson(json, Resources.class);
461 assertNotNull(resources);
462 List<Resource> list = resources.getResources();
464 assertEquals(1, list.size());
465 Resource item = list.get(0);
466 assertEquals(ResourceType.MOTION, item.getType());
467 Boolean enabled = item.getEnabled();
468 assertNotNull(enabled);
470 Motion motion = item.getMotion();
471 assertNotNull(motion);
472 assertTrue(motion.isMotion());
473 assertTrue(motion.isMotionValid());
477 void testSetGetPureColors() {
478 Resource resource = new Resource(ResourceType.LIGHT);
479 assertNotNull(resource);
481 HSBType cyan = new HSBType("180,100,100");
482 HSBType yellow = new HSBType("60,100,100");
483 HSBType magenta = new HSBType("300,100,100");
485 for (HSBType color : Set.of(HSBType.WHITE, HSBType.RED, HSBType.GREEN, HSBType.BLUE, cyan, yellow, magenta)) {
486 Setters.setColorXy(resource, color, null);
487 State state = resource.getColorState();
488 assertTrue(state instanceof HSBType);
489 assertTrue(color.closeTo((HSBType) state, 0.01));
494 void testSseLightOrGroupEvent() {
495 String json = load("event");
496 List<Event> eventList = GSON.fromJson(json, Event.EVENT_LIST_TYPE);
497 assertNotNull(eventList);
498 assertEquals(3, eventList.size());
499 Event event = eventList.get(0);
500 List<Resource> resources = event.getData();
501 assertEquals(9, resources.size());
502 for (Resource r : resources) {
503 ResourceType type = r.getType();
504 assertTrue(ResourceType.LIGHT == type || ResourceType.GROUPED_LIGHT == type);
509 void testSseSceneEvent() {
510 String json = load("event");
511 List<Event> eventList = GSON.fromJson(json, Event.EVENT_LIST_TYPE);
512 assertNotNull(eventList);
513 assertEquals(3, eventList.size());
514 Event event = eventList.get(2);
515 List<Resource> resources = event.getData();
516 assertEquals(6, resources.size());
517 Resource resource = resources.get(1);
518 assertEquals(ResourceType.SCENE, resource.getType());
519 JsonObject status = resource.getStatus();
520 assertNotNull(status);
521 JsonElement active = status.get("active");
522 assertNotNull(active);
523 assertTrue(active.isJsonPrimitive());
524 assertEquals("inactive", active.getAsString());
525 Optional<Boolean> isActive = resource.getSceneActive();
526 assertTrue(isActive.isPresent());
527 assertEquals(Boolean.FALSE, isActive.get());
531 void testTemperature() {
532 String json = load(ResourceType.TEMPERATURE.name().toLowerCase());
533 Resources resources = GSON.fromJson(json, Resources.class);
534 assertNotNull(resources);
535 List<Resource> list = resources.getResources();
537 assertEquals(1, list.size());
538 Resource item = list.get(0);
539 assertEquals(ResourceType.TEMPERATURE, item.getType());
540 Temperature temperature = item.getTemperature();
541 assertNotNull(temperature);
542 assertEquals(17.2, temperature.getTemperature(), 0.1);
543 assertTrue(temperature.isTemperatureValid());
547 void testValidJson() {
548 for (ResourceType res : ResourceType.values()) {
549 if (!ResourceType.SSE_TYPES.contains(res)) {
551 String file = res.name().toLowerCase();
552 String json = load(file);
553 JsonElement jsonElement = JsonParser.parseString(json);
554 assertTrue(jsonElement.isJsonObject());
555 } catch (JsonSyntaxException e) {
563 void testZigbeeStatus() {
564 String json = load(ResourceType.ZIGBEE_CONNECTIVITY.name().toLowerCase());
565 Resources resources = GSON.fromJson(json, Resources.class);
566 assertNotNull(resources);
567 List<Resource> list = resources.getResources();
569 assertEquals(35, list.size());
570 Resource item = list.get(0);
571 assertEquals(ResourceType.ZIGBEE_CONNECTIVITY, item.getType());
572 ZigbeeStatus zigbeeStatus = item.getZigbeeStatus();
573 assertNotNull(zigbeeStatus);
574 assertEquals("Connected", zigbeeStatus.toString());
578 void testZoneGroup() {
579 String json = load(ResourceType.ZONE.name().toLowerCase());
580 Resources resources = GSON.fromJson(json, Resources.class);
581 assertNotNull(resources);
582 List<Resource> list = resources.getResources();
584 assertEquals(7, list.size());
585 Resource item = list.get(0);
586 assertEquals(ResourceType.ZONE, item.getType());
587 List<ResourceReference> children = item.getChildren();
588 assertEquals(1, children.size());
589 ResourceReference child = children.get(0);
590 assertNotNull(child);
591 assertEquals("bcad47a0-3f1f-498c-a8aa-3cf389965219", child.getId());
592 assertEquals(ResourceType.LIGHT, child.getType());
593 List<ResourceReference> services = item.getServiceReferences();
594 assertEquals(1, services.size());
595 ResourceReference service = services.get(0);
596 assertNotNull(service);
597 assertEquals("db4fd630-3798-40de-b642-c1ef464bf770", service.getId());
598 assertEquals(ResourceType.GROUPED_LIGHT, service.getType());