2 * Copyright (c) 2010-2022 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.hdpowerview;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
18 import java.io.IOException;
19 import java.nio.file.Files;
20 import java.nio.file.Paths;
21 import java.util.List;
22 import java.util.regex.Pattern;
23 import java.util.stream.Collectors;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.junit.jupiter.api.Test;
28 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
29 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
30 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
31 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
36 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
37 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
38 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
39 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
40 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonParseException;
49 * Unit tests for HD PowerView binding.
51 * @author Andrew Fiddian-Green - Initial contribution
52 * @author Jacob Laursen - Add support for scene groups
55 public class HDPowerViewJUnitTests {
57 private static final Pattern VALID_IP_V4_ADDRESS = Pattern
58 .compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b");
60 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
63 * load a test JSON string from a file.
65 private String loadJson(String fileName) {
67 return Files.readAllLines(Paths.get(String.format("src/test/resources/%s.json", fileName))).stream()
68 .collect(Collectors.joining());
69 } catch (IOException e) {
76 * Run a series of ONLINE tests on the communication with a hub.
78 * @param hubIPAddress must be a valid hub IP address to run the
79 * tests on; or an INVALID IP address to
81 * @param allowShadeMovementCommands set to true if you accept that the tests
82 * shall physically move the shades
85 public void testOnlineCommunication() {
87 * NOTE: in order to actually run these tests you must have a hub physically
88 * available, and its IP address must be correctly configured in the
89 * "hubIPAddress" string constant e.g. "192.168.1.123"
91 String hubIPAddress = "192.168.1.xxx";
94 * NOTE: set allowShadeMovementCommands = true if you accept physically moving
95 * the shades during these tests
97 boolean allowShadeMovementCommands = false;
99 if (VALID_IP_V4_ADDRESS.matcher(hubIPAddress).matches()) {
100 // ==== initialize stuff ====
101 HttpClient client = new HttpClient();
102 assertNotNull(client);
104 // ==== start the client ====
107 assertTrue(client.isStarted());
108 } catch (Exception e) {
109 fail(e.getMessage());
112 HDPowerViewWebTargets webTargets = new HDPowerViewWebTargets(client, hubIPAddress);
113 assertNotNull(webTargets);
116 ShadePosition shadePos = null;
117 Shades shadesX = null;
119 // ==== get all shades ====
121 shadesX = webTargets.getShades();
122 assertNotNull(shadesX);
123 List<ShadeData> shadesData = shadesX.shadeData;
124 assertNotNull(shadesData);
126 if (shadesData != null) {
127 assertTrue(!shadesData.isEmpty());
129 shadeData = shadesData.get(0);
130 assertNotNull(shadeData);
131 assertTrue(shadeData.getName().length() > 0);
132 shadePos = shadeData.positions;
133 assertNotNull(shadePos);
134 ShadeData shadeZero = shadesData.get(0);
135 assertNotNull(shadeZero);
136 shadeId = shadeZero.id;
137 assertNotEquals(0, shadeId);
139 for (ShadeData shadexData : shadesData) {
140 String shadeName = shadexData.getName();
141 assertNotNull(shadeName);
144 } catch (HubException e) {
145 fail(e.getMessage());
148 // ==== get all scenes ====
151 Scenes scenes = webTargets.getScenes();
152 assertNotNull(scenes);
154 List<Scene> scenesData = scenes.sceneData;
155 assertNotNull(scenesData);
157 if (scenesData != null) {
158 assertTrue(!scenesData.isEmpty());
159 Scene sceneZero = scenesData.get(0);
160 assertNotNull(sceneZero);
161 sceneId = sceneZero.id;
162 assertTrue(sceneId > 0);
164 for (Scene scene : scenesData) {
165 String sceneName = scene.getName();
166 assertNotNull(sceneName);
169 } catch (HubException e) {
170 fail(e.getMessage());
173 // ==== refresh a specific shade ====
174 ShadeData shadeData = null;
176 assertNotEquals(0, shadeId);
177 shadeData = webTargets.refreshShadePosition(shadeId);
178 } catch (HubException e) {
179 fail(e.getMessage());
182 // ==== move a specific shade ====
184 assertNotEquals(0, shadeId);
186 if (shadeData != null) {
187 ShadePosition positions = shadeData.positions;
188 assertNotNull(positions);
189 Integer capabilitiesValue = shadeData.capabilities;
190 assertNotNull(capabilitiesValue);
192 if (positions != null && capabilitiesValue != null) {
193 Capabilities capabilities = db.getCapabilities(capabilitiesValue.intValue());
195 State pos = positions.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
196 assertEquals(PercentType.class, pos.getClass());
198 int position = ((PercentType) pos).intValue();
199 position = position + ((position <= 10) ? 5 : -5);
201 ShadePosition targetPosition = new ShadePosition().setPosition(capabilities,
202 PRIMARY_ZERO_IS_CLOSED, position);
203 assertNotNull(targetPosition);
205 if (allowShadeMovementCommands) {
206 webTargets.moveShade(shadeId, targetPosition);
208 ShadeData newData = webTargets.getShade(shadeId);
209 ShadePosition actualPosition = newData.positions;
210 assertNotNull(actualPosition);
211 if (actualPosition != null) {
212 assertEquals(targetPosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED),
213 actualPosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED));
218 } catch (HubException e) {
219 fail(e.getMessage());
222 // ==== activate a specific scene ====
223 if (allowShadeMovementCommands) {
225 assertNotNull(sceneId);
226 webTargets.activateScene(sceneId);
227 } catch (HubProcessingException | HubMaintenanceException e) {
228 fail(e.getMessage());
232 // ==== test stop command ====
233 if (allowShadeMovementCommands) {
235 assertNotNull(sceneId);
236 webTargets.stopShade(shadeId);
237 } catch (HubException e) {
238 fail(e.getMessage());
242 // ==== stop the client ====
243 if (client.isRunning()) {
246 } catch (Exception e) {
247 fail(e.getMessage());
254 * Test parsing of ShadePosition (shade fully up).
258 public void testShadePositionParsingFullyUp() {
259 Capabilities capabilities = db.getCapabilities(0);
260 ShadePosition test = new ShadePosition().setPosition(capabilities, PRIMARY_ZERO_IS_CLOSED, 0);
262 State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
263 assertEquals(PercentType.class, pos.getClass());
264 assertEquals(0, ((PercentType) pos).intValue());
265 pos = test.getState(capabilities, VANE_TILT_COORDS);
266 assertTrue(UnDefType.UNDEF.equals(pos));
270 * Test parsing of ShadePosition (shade fully down (method 1)).
274 public void testShadePositionParsingShadeFullyDown1() {
275 Capabilities capabilities = db.getCapabilities(0);
276 ShadePosition test = new ShadePosition().setPosition(capabilities, PRIMARY_ZERO_IS_CLOSED, 100);
278 State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
279 assertEquals(PercentType.class, pos.getClass());
280 assertEquals(100, ((PercentType) pos).intValue());
281 pos = test.getState(capabilities, VANE_TILT_COORDS);
282 assertEquals(PercentType.class, pos.getClass());
283 assertEquals(0, ((PercentType) pos).intValue());
287 * Test parsing of ShadePosition (shade fully down (method 2)).
291 public void testShadePositionParsingShadeFullyDown2() {
292 Capabilities capabilities = db.getCapabilities(0);
293 ShadePosition test = new ShadePosition().setPosition(capabilities, VANE_TILT_COORDS, 0);
295 State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
296 assertEquals(PercentType.class, pos.getClass());
297 assertEquals(100, ((PercentType) pos).intValue());
298 pos = test.getState(capabilities, VANE_TILT_COORDS);
299 assertEquals(PercentType.class, pos.getClass());
300 assertEquals(0, ((PercentType) pos).intValue());
304 * Test parsing of ShadePosition (shade fully down (method 2) and vane fully open).
308 public void testShadePositionParsingShadeFullyDownVaneOpen() {
309 Capabilities capabilities = db.getCapabilities(0);
310 ShadePosition test = new ShadePosition().setPosition(capabilities, VANE_TILT_COORDS, 100);
312 State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
313 assertEquals(PercentType.class, pos.getClass());
314 assertEquals(100, ((PercentType) pos).intValue());
315 pos = test.getState(capabilities, VANE_TILT_COORDS);
316 assertEquals(PercentType.class, pos.getClass());
317 assertEquals(100, ((PercentType) pos).intValue());
321 * Test generic JSON shades response.
324 public void shadeResponseIsParsedCorrectly() throws JsonParseException {
325 final Gson gson = new Gson();
327 String json = loadJson("shades");
328 assertNotEquals("", json);
329 shades = gson.fromJson(json, Shades.class);
330 assertNotNull(shades);
334 * Test generic JSON scene response.
337 public void sceneResponseIsParsedCorrectly() throws JsonParseException {
338 final Gson gson = new Gson();
339 String json = loadJson("scenes");
340 assertNotEquals("", json);
342 Scenes scenes = gson.fromJson(json, Scenes.class);
343 assertNotNull(scenes);
344 if (scenes != null) {
345 List<Scene> sceneData = scenes.sceneData;
346 assertNotNull(sceneData);
347 if (sceneData != null) {
348 assertEquals(4, sceneData.size());
349 Scene scene = sceneData.get(0);
350 assertEquals("Door Open", scene.getName());
351 assertEquals(18097, scene.id);
357 * Test generic JSON scene collection response.
360 public void sceneCollectionResponseIsParsedCorrectly() throws JsonParseException {
361 final Gson gson = new Gson();
362 String json = loadJson("sceneCollections");
363 assertNotEquals("", json);
365 SceneCollections sceneCollections = gson.fromJson(json, SceneCollections.class);
366 assertNotNull(sceneCollections);
368 if (sceneCollections != null) {
369 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
370 assertNotNull(sceneCollectionData);
371 if (sceneCollectionData != null) {
372 assertEquals(1, sceneCollectionData.size());
374 SceneCollection sceneCollection = sceneCollectionData.get(0);
375 assertEquals("Børn op", sceneCollection.getName());
376 assertEquals(27119, sceneCollection.id);
382 * Test the JSON parsing for a duette top down bottom up shade.
385 public void duetteTopDownBottomUpShadeIsParsedCorrectly() throws JsonParseException {
386 final Gson gson = new Gson();
387 String json = loadJson("duette");
388 assertNotEquals("", json);
390 Shades shades = gson.fromJson(json, Shades.class);
391 assertNotNull(shades);
392 if (shades != null) {
393 List<ShadeData> shadesData = shades.shadeData;
394 assertNotNull(shadesData);
396 if (shadesData != null) {
397 assertEquals(1, shadesData.size());
398 ShadeData shadeData = shadesData.get(0);
399 assertNotNull(shadeData);
401 assertEquals("Gardin 1", shadeData.getName());
402 assertEquals(63778, shadeData.id);
404 ShadePosition shadePos = shadeData.positions;
405 assertNotNull(shadePos);
407 if (shadePos != null) {
408 Integer capabilitiesValue = shadeData.capabilities;
409 assertNotNull(capabilitiesValue);
410 if (capabilitiesValue != null) {
411 assertEquals(7, capabilitiesValue.intValue());
413 Capabilities capabilities = db.getCapabilities(capabilitiesValue);
415 State pos = shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
416 assertEquals(PercentType.class, pos.getClass());
417 assertEquals(59, ((PercentType) pos).intValue());
419 pos = shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN);
420 assertEquals(PercentType.class, pos.getClass());
421 assertEquals(35, ((PercentType) pos).intValue());
423 pos = shadePos.getState(capabilities, VANE_TILT_COORDS);
424 assertEquals(UnDefType.class, pos.getClass());
426 assertEquals(3, shadeData.batteryStatus);
428 assertEquals(4, shadeData.signalStrength);
430 assertEquals(8, shadeData.type);
432 assertTrue(db.isTypeInDatabase(shadeData.type));
433 assertTrue(db.isCapabilitiesInDatabase(capabilitiesValue.intValue()));
435 assertEquals(db.getType(shadeData.type).getCapabilities(), capabilitiesValue.intValue());
437 assertTrue(db.getCapabilities(capabilitiesValue.intValue()).supportsSecondary());
438 assertNotEquals(db.getType(shadeData.type).getCapabilities(), capabilitiesValue.intValue() + 1);
440 // ==== when changing position1, position2 value is not changed (vice-versa) ====
441 ShadePosition shadePosition = shadeData.positions;
442 assertNotNull(shadePosition);
443 if (shadePosition != null) {
444 // ==== position2 ====
445 State position2Old = shadePosition.getState(capabilities, SECONDARY_ZERO_IS_OPEN);
446 shadePosition.setPosition(capabilities, PRIMARY_ZERO_IS_CLOSED, 99);
447 State position2New = shadePosition.getState(capabilities, SECONDARY_ZERO_IS_OPEN);
448 assertEquals(PercentType.class, position2Old.getClass());
449 assertEquals(PercentType.class, position2New.getClass());
450 assertEquals(((PercentType) position2Old).intValue(),
451 ((PercentType) position2New).intValue());
453 // ==== position2 ====
454 State position1Old = shadePosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
455 shadePosition.setPosition(capabilities, SECONDARY_ZERO_IS_OPEN, 99);
456 State position1New = shadePosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
457 assertEquals(PercentType.class, position1Old.getClass());
458 assertEquals(PercentType.class, position1New.getClass());
459 assertEquals(((PercentType) position1Old).intValue(),
460 ((PercentType) position1New).intValue());
469 * General tests of the database of known types.
472 public void testKnownTypesDatabase() {
473 assertTrue(db.isTypeInDatabase(4));
474 assertTrue(db.isCapabilitiesInDatabase(0));
476 assertTrue(db.getCapabilities(6).isPrimaryStateInverted());
477 assertTrue(db.getCapabilities(7).supportsSecondary());
479 assertEquals(db.getType(4).getCapabilities(), 0);
480 assertEquals(db.getType(-1).getCapabilities(), -1);
482 assertFalse(db.isTypeInDatabase(99));
483 assertFalse(db.isCapabilitiesInDatabase(99));
485 assertFalse(db.getCapabilities(0).isPrimaryStateInverted());
486 assertFalse(db.getCapabilities(-1).isPrimaryStateInverted());
487 assertFalse(db.getCapabilities(99).isPrimaryStateInverted());
489 assertFalse(db.getCapabilities(0).supportsSecondary());
490 assertFalse(db.getCapabilities(-1).supportsSecondary());
491 assertFalse(db.getCapabilities(99).supportsSecondary());
495 * On dual rail shades, it should not be possible to drive the upper rail below the lower rail, or vice-versa. So
496 * the binding code applies constraints on setting such positions. This test checks that the constraint code is
500 public void testDualRailConstraints() {
501 ShadePosition shade = new ShadePosition();
502 Capabilities caps = db.getCapabilities(7);
504 // ==== OK !! primary at bottom, secondary at top ====
505 shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0);
506 assertEquals(PercentType.HUNDRED, shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
507 assertEquals(PercentType.ZERO, shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
509 // ==== OK !! primary at middle, secondary at top ====
510 shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 50).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0);
511 assertEquals(new PercentType(50), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
512 assertEquals(PercentType.ZERO, shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
514 // ==== OK !! primary at middle, secondary at middle ====
515 shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 50).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 50);
516 assertEquals(new PercentType(50), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
517 assertEquals(new PercentType(50), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
519 // ==== IMPOSSIBLE !! secondary at middle, primary above => test the constraining code ====
520 shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
521 shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 40).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 25);
522 assertEquals(new PercentType(40), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
523 assertEquals(new PercentType(40), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
525 // ==== OK !! secondary at middle, primary below ====
526 shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
527 shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 50).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 75);
528 assertEquals(new PercentType(50), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
529 assertEquals(new PercentType(75), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
531 // ==== IMPOSSIBLE !! primary at middle, secondary below => test the constraining code ====
532 shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
533 shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 60).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 75);
534 assertEquals(new PercentType(60), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
535 assertEquals(new PercentType(60), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
537 // ==== OK !! primary at middle, secondary above ====
538 shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
539 shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 60).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 25);
540 assertEquals(new PercentType(60), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
541 assertEquals(new PercentType(25), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));