]> git.basschouten.com Git - openhab-addons.git/blob
9260e56a9f3db4582e1618d02bb4a7cf7701e24a
[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.binding.velux.test;
14
15 import static org.junit.jupiter.api.Assertions.*;
16
17 import org.eclipse.jdt.annotation.NonNullByDefault;
18 import org.junit.jupiter.api.MethodOrderer;
19 import org.junit.jupiter.api.Order;
20 import org.junit.jupiter.api.Test;
21 import org.junit.jupiter.api.TestMethodOrder;
22 import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters;
23 import org.openhab.binding.velux.internal.bridge.slip.SCgetHouseStatus;
24 import org.openhab.binding.velux.internal.bridge.slip.SCgetProduct;
25 import org.openhab.binding.velux.internal.bridge.slip.SCgetProductStatus;
26 import org.openhab.binding.velux.internal.bridge.slip.SCrunProductCommand;
27 import org.openhab.binding.velux.internal.things.VeluxExistingProducts;
28 import org.openhab.binding.velux.internal.things.VeluxKLFAPI;
29 import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
30 import org.openhab.binding.velux.internal.things.VeluxProduct;
31 import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
32 import org.openhab.binding.velux.internal.things.VeluxProduct.ProductState;
33 import org.openhab.binding.velux.internal.things.VeluxProductName;
34 import org.openhab.binding.velux.internal.things.VeluxProductPosition;
35 import org.openhab.binding.velux.internal.things.VeluxProductPosition.PositionType;
36 import org.openhab.binding.velux.internal.things.VeluxProductType.ActuatorType;
37 import org.openhab.core.library.types.PercentType;
38
39 /**
40  * JUnit test suite to check the proper parsing of actuator notification packets, and to confirm that the existing
41  * products database is working correctly.
42  *
43  * @author Andrew Fiddian-Green - Initial contribution.
44  */
45 @NonNullByDefault
46 @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
47 public class TestNotificationsAndDatabase {
48     // validation parameters
49     private static final byte PRODUCT_INDEX_A = 6;
50     private static final byte PRODUCT_INDEX_B = 0;
51     private static final int MAIN_POSITION_A = 0xC800;
52     private static final int MAIN_POSITION_B = 0x4600;
53     private static final int VANE_POSITION_A = 0x634f;
54     private static final int TARGET_POSITION = 0xB800;
55     private static final int STATE_SOMFY = 0x2D;
56
57     private static final int UNKNOWN_POSITION = VeluxProductPosition.VPP_VELUX_UNKNOWN;
58     private static final int IGNORE_POSITION = VeluxProductPosition.VPP_VELUX_IGNORE;
59     private static final int STATE_DONE = VeluxProduct.ProductState.DONE.value;
60
61     private static final ActuatorType ACTUATOR_TYPE_SOMFY = ActuatorType.BLIND_17;
62     private static final ActuatorType ACTUATOR_TYPE_VELUX = ActuatorType.WINDOW_4_1;
63     private static final ActuatorType ACTUATOR_TYPE_UNDEF = ActuatorType.UNDEFTYPE;
64
65     // existing products database
66     private static final VeluxExistingProducts EXISTING_PRODUCTS = new VeluxExistingProducts();
67
68     private static byte[] toByteArray(String input) {
69         String[] data = input.split(" ");
70         byte[] result = new byte[data.length];
71         for (int i = 0; i < data.length; i++) {
72             result[i] = Integer.decode("0x" + data[i]).byteValue();
73         }
74         return result;
75     }
76
77     private VeluxExistingProducts getExistingProducts() {
78         return EXISTING_PRODUCTS;
79     }
80
81     /**
82      * Confirm the existing products database is initialised.
83      */
84     @Test
85     @Order(1)
86     public void testInitialized() {
87         assertEquals(0, getExistingProducts().getNoMembers());
88     }
89
90     /**
91      * Test the 'supportsVanePosition()' method for two types of products.
92      */
93     @Test
94     @Order(2)
95     public void testSupportsVanePosition() {
96         VeluxProduct product = new VeluxProduct();
97         assertFalse(product.supportsVanePosition());
98         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
99         assertTrue(product.supportsVanePosition());
100     }
101
102     /**
103      * Test the SCgetProduct command by checking for the correct parsing of a 'GW_GET_NODE_INFORMATION_NTF' notification
104      * packet. Note: this packet is from a Somfy roller shutter with main and vane position.
105      */
106     @Test
107     @Order(3)
108     public void testSCgetProduct() {
109         // initialise the test parameters
110         final String packet = """
111                 06 00 06 00 48 6F 62 62 79 6B 61 6D 65 72 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
112                  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
113                  00 00 00 00 00 01 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00 2D C8 00 C8 00 F7 FF F7 FF 00 00 F7 FF 00\
114                  00 4F 00 4A EA 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
115                 """;
116         final Command command = VeluxKLFAPI.Command.GW_GET_NODE_INFORMATION_NTF;
117
118         // initialise the BCP
119         SCgetProduct bcp = new SCgetProduct();
120         bcp.setProductId(PRODUCT_INDEX_A);
121
122         // set the packet response
123         bcp.setResponse(command.getShort(), toByteArray(packet), false);
124
125         // check BCP status
126         assertTrue(bcp.isCommunicationSuccessful());
127         assertTrue(bcp.isCommunicationFinished());
128
129         // initialise the product
130         VeluxProduct product = bcp.getProduct();
131
132         // check positive assertions
133         assertEquals(STATE_SOMFY, product.getState());
134         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
135         assertEquals(MAIN_POSITION_A, product.getTarget());
136         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
137         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
138         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
139         assertNull(product.getFunctionalParameters());
140         assertTrue(product.supportsVanePosition());
141         assertTrue(product.isSomfyProduct());
142         assertEquals(ProductState.DONE, product.getProductState());
143
144         // check negative assertions
145         assertNotEquals(VANE_POSITION_A, product.getVanePosition());
146
147         // register in existing products database
148         VeluxExistingProducts existingProducts = getExistingProducts();
149         assertTrue(existingProducts.register(product));
150         assertTrue(existingProducts.isRegistered(product));
151         assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex()));
152         assertEquals(1, existingProducts.getNoMembers());
153
154         // confirm that a dummy product is NOT in the database
155         assertFalse(existingProducts.isRegistered(new ProductBridgeIndex(99)));
156
157         // check dirty flag
158         assertTrue(existingProducts.isDirty());
159         existingProducts.resetDirtyFlag();
160         assertFalse(existingProducts.isDirty());
161
162         // re-registering the same product should return false
163         assertFalse(existingProducts.register(product));
164
165         // updating again with the same data should NOT set the dirty flag
166         assertTrue(existingProducts.update(product));
167         assertFalse(existingProducts.isDirty());
168
169         // check that the product in the database is indeed the one just created
170         VeluxProduct existing = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A));
171         assertEquals(product, existing);
172         assertEquals(1, existingProducts.getNoMembers());
173         assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex()));
174     }
175
176     /**
177      * Confirm that the product in the existing database has the same values as the product created and added in test 3.
178      */
179     @Test
180     @Order(4)
181     public void testExistingUnknownVanePosition() {
182         VeluxExistingProducts existingProducts = getExistingProducts();
183         VeluxProduct product = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A));
184
185         // confirm the product details
186         assertEquals(STATE_SOMFY, product.getState());
187         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
188         assertEquals(MAIN_POSITION_A, product.getTarget());
189         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
190         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
191         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
192         assertNull(product.getFunctionalParameters());
193     }
194
195     /**
196      * Test the SCgetProductStatus command by checking for the correct parsing of a 'GW_STATUS_REQUEST_NTF' notification
197      * packet. Note: this packet is from a Somfy roller shutter with main and vane position.
198      */
199     @Test
200     @Order(5)
201     public void testSCgetProductStatus() {
202         // initialise the test parameters
203         final String packet = """
204                 00 D8 01 06 00 01 01 02 00 C8 00 03 63 4F 00 00 00 00 00 00 00 00 00 00 00 00 00\
205                  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
206                 """;
207         final Command command = VeluxKLFAPI.Command.GW_STATUS_REQUEST_NTF;
208
209         // initialise the BCP
210         SCgetProductStatus bcp = new SCgetProductStatus();
211         bcp.setProductId(PRODUCT_INDEX_A);
212
213         // set the packet response
214         bcp.setResponse(command.getShort(), toByteArray(packet), false);
215
216         // check BCP status
217         assertTrue(bcp.isCommunicationSuccessful());
218         assertTrue(bcp.isCommunicationFinished());
219
220         // initialise the product
221         VeluxProduct product = bcp.getProduct();
222
223         // change actuator type
224         assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType());
225         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
226
227         // check positive assertions
228         assertEquals(STATE_DONE, product.getState());
229         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
230         assertEquals(IGNORE_POSITION, product.getTarget());
231         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
232         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
233         assertEquals(VANE_POSITION_A, product.getVanePosition());
234         assertNotNull(product.getFunctionalParameters());
235         assertEquals(ProductState.DONE, product.getProductState());
236
237         // test updating the existing product in the database
238         VeluxExistingProducts existingProducts = getExistingProducts();
239         assertTrue(existingProducts.update(product));
240         assertTrue(existingProducts.isDirty());
241
242         // updating again with the same data should NOT set the dirty flag
243         existingProducts.resetDirtyFlag();
244         assertTrue(existingProducts.update(product));
245         assertFalse(existingProducts.isDirty());
246     }
247
248     /**
249      * Confirm that the product in the existing database has the same values as the product created and updated to the
250      * database in test 5.
251      */
252     @Test
253     @Order(6)
254     public void testExistingValidVanePosition() {
255         VeluxExistingProducts existingProducts = getExistingProducts();
256         VeluxProduct product = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A));
257
258         // confirm the product details
259         assertEquals(STATE_DONE, product.getState());
260         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
261         assertEquals(MAIN_POSITION_A, product.getTarget());
262         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
263         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
264         assertEquals(VANE_POSITION_A, product.getVanePosition());
265         assertNotNull(product.getFunctionalParameters());
266         assertEquals(ProductState.DONE, product.getProductState());
267     }
268
269     /**
270      * Test the SCgetHouseStatus command by checking for the correct parsing of a 'GW_NODE_STATE_POSITION_CHANGED_NTF'
271      * notification packet. Note: this packet is from a Somfy roller shutter with main and vane position.
272      */
273     @Test
274     @Order(7)
275     public void testSCgetHouseStatus() {
276         // initialise the test parameters
277         final String packet = "06 2D C8 00 B8 00 F7 FF F7 FF 00 00 F7 FF 00 00 4A E5 00 00";
278         final short command = VeluxKLFAPI.Command.GW_NODE_STATE_POSITION_CHANGED_NTF.getShort();
279
280         // initialise the BCP
281         SCgetHouseStatus bcp = new SCgetHouseStatus();
282
283         // set the packet response
284         bcp.setResponse(command, toByteArray(packet), false);
285
286         // check BCP status
287         assertTrue(bcp.isCommunicationSuccessful());
288         assertTrue(bcp.isCommunicationFinished());
289
290         // initialise the product
291         VeluxProduct product = bcp.getProduct();
292
293         // change actuator type
294         assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType());
295         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
296
297         // check positive assertions
298         assertEquals(STATE_SOMFY, product.getState());
299         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
300         assertEquals(TARGET_POSITION, product.getTarget());
301         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
302         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
303         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
304         assertNull(product.getFunctionalParameters());
305
306         // check negative assertions
307         assertNotEquals(VANE_POSITION_A, product.getVanePosition());
308
309         VeluxExistingProducts existingProducts = getExistingProducts();
310         existingProducts.update(product);
311     }
312
313     /**
314      * Confirm that the product in the existing database has the same values as the product created and updated to the
315      * database in test 7.
316      */
317     @Test
318     @Order(8)
319     public void testExistingValidVanePositionWithNewTargetValue() {
320         VeluxExistingProducts existingProducts = getExistingProducts();
321         VeluxProduct product = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A));
322
323         // confirm the product details
324         assertEquals(STATE_SOMFY, product.getState());
325         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
326         assertEquals(TARGET_POSITION, product.getTarget());
327         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
328         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
329         assertEquals(VANE_POSITION_A, product.getVanePosition());
330         assertNotNull(product.getFunctionalParameters());
331     }
332
333     /**
334      * Test the SCgetProduct by checking for the correct parsing of a 'GW_GET_NODE_INFORMATION_NTF' notification packet.
335      * Note: this packet is from a Velux roof window without vane position.
336      */
337     @Test
338     @Order(9)
339     public void testSCgetProductOnVelux() {
340         // initialise the test parameters
341         final String packet = """
342                 00 00 00 00 53 68 65 64 20 57 69 6E 64 6F 77 00 00 00 00 00 00 00 00 00 00 00 00 00\
343                  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
344                  00 00 00 00 00 00 00 00 01 01 01 03 07 00 01 16 56 24 5C 26 14 19 00 FC 05 46 00 46 00 F7 FF F7\
345                  FF F7 FF F7 FF 00 00 4F 05 B3 5F 01 D8 03 B2 1C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
346                 """;
347         final short command = VeluxKLFAPI.Command.GW_GET_NODE_INFORMATION_NTF.getShort();
348
349         // initialise the BCP
350         SCgetProduct bcp = new SCgetProduct();
351         bcp.setProductId(PRODUCT_INDEX_B);
352
353         // set the packet response
354         bcp.setResponse(command, toByteArray(packet), false);
355
356         // check BCP status
357         assertTrue(bcp.isCommunicationSuccessful());
358         assertTrue(bcp.isCommunicationFinished());
359
360         // initialise the product
361         VeluxProduct product = bcp.getProduct();
362
363         // check positive assertions
364         assertEquals(STATE_DONE, product.getState());
365         assertEquals(MAIN_POSITION_B, product.getCurrentPosition());
366         assertEquals(MAIN_POSITION_B, product.getTarget());
367         assertEquals(PRODUCT_INDEX_B, product.getBridgeProductIndex().toInt());
368         assertEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType());
369         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
370         assertNull(product.getFunctionalParameters());
371         assertFalse(product.isSomfyProduct());
372
373         // check negative assertions
374         assertFalse(product.supportsVanePosition());
375         assertNotEquals(VANE_POSITION_A, product.getVanePosition());
376
377         // register in existing products database
378         VeluxExistingProducts existingProducts = getExistingProducts();
379         assertTrue(existingProducts.register(product));
380         assertTrue(existingProducts.isRegistered(product));
381         assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex()));
382         assertEquals(2, existingProducts.getNoMembers());
383
384         // check that the product in the database is indeed the one just created
385         VeluxProduct existing = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_B));
386         assertEquals(product, existing);
387         assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex()));
388     }
389
390     /**
391      * Confirm that the modified products list is functioning.
392      */
393     @Test
394     @Order(10)
395     public void testModifiedList() {
396         VeluxExistingProducts existingProducts = getExistingProducts();
397         VeluxProduct[] modified = existingProducts.valuesOfModified();
398
399         // confirm that the list contains two entries
400         assertEquals(2, modified.length);
401
402         // confirm the product details for the Somfy product
403         VeluxProduct product = modified[0];
404         assertEquals(STATE_SOMFY, product.getState());
405         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
406         assertEquals(TARGET_POSITION, product.getTarget());
407         assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt());
408         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
409         assertEquals(VANE_POSITION_A, product.getVanePosition());
410         assertTrue(product.isSomfyProduct());
411         assertNotNull(product.getFunctionalParameters());
412
413         // confirm the product details for the Velux product
414         product = modified[1];
415         assertEquals(STATE_DONE, product.getState());
416         assertEquals(MAIN_POSITION_B, product.getCurrentPosition());
417         assertEquals(MAIN_POSITION_B, product.getTarget());
418         assertEquals(PRODUCT_INDEX_B, product.getBridgeProductIndex().toInt());
419         assertEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType());
420         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
421         assertNull(product.getFunctionalParameters());
422         assertFalse(product.isSomfyProduct());
423
424         // reset the dirty flag
425         existingProducts.resetDirtyFlag();
426         assertFalse(existingProducts.isDirty());
427
428         // confirm modified list is now empty again
429         modified = existingProducts.valuesOfModified();
430         assertEquals(0, modified.length);
431     }
432
433     /**
434      * Test actuator type setting.
435      */
436     @Test
437     @Order(11)
438     public void testActuatorTypeSetting() {
439         VeluxProduct product = new VeluxProduct();
440         assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType());
441
442         // set actuator type
443         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
444         assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType());
445
446         // try to set it again
447         product.setActuatorType(ACTUATOR_TYPE_VELUX);
448         assertNotEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType());
449
450         // try with a clean product
451         product = new VeluxProduct();
452         product.setActuatorType(ACTUATOR_TYPE_VELUX);
453         assertEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType());
454     }
455
456     /**
457      * Test the SCrunProduct command by creating packet for a given main position and vane position, and checking the
458      * created packet is as expected.
459      */
460     @Test
461     @Order(12)
462     public void testSCrunProductA() {
463         final String expectedString = """
464                 02 1C 08 05 00 20 00 90 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00\
465                  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
466                  00 00 00 00 00 00 00 00 00\
467                 """;
468         final byte[] expectedPacket = toByteArray(expectedString);
469         final int targetMainPosition = 0x9000;
470         final int targetVanePosition = 0xA000;
471
472         // initialise the product to be commanded
473         VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0,
474                 0, null, Command.UNDEFTYPE);
475         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
476         product.setCurrentPosition(targetMainPosition);
477         product.setVanePosition(targetVanePosition);
478
479         // create the run product command, and initialise it from the test product's state values
480         SCrunProductCommand bcp = new SCrunProductCommand();
481         bcp.setNodeIdAndParameters(product.getBridgeProductIndex().toInt(),
482                 new VeluxProductPosition(product.getCurrentPosition()), product.getFunctionalParameters());
483
484         // get the resulting data packet
485         byte[] actualPacket = bcp.getRequestDataAsArrayOfBytes();
486
487         // check the packet lengths are the same
488         assertEquals(expectedPacket.length, actualPacket.length);
489
490         // check the packet contents are identical (note start at i = 2 because session id won't match)
491         boolean identical = true;
492         for (int i = 2; i < expectedPacket.length; i++) {
493             if (actualPacket[i] != expectedPacket[i]) {
494                 identical = false;
495             }
496         }
497         assertTrue(identical);
498
499         // check the resulting updater product state is 'executing' with the new values
500         product = bcp.getProduct();
501         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
502         assertEquals(ProductState.EXECUTING, product.getProductState());
503         assertEquals(targetMainPosition, product.getCurrentPosition());
504         assertEquals(targetMainPosition, product.getTarget());
505         assertEquals(targetMainPosition, product.getDisplayPosition());
506         assertEquals(targetVanePosition, product.getVanePosition());
507         assertEquals(targetVanePosition, product.getVaneDisplayPosition());
508     }
509
510     /**
511      * Test the SCrunProduct command by creating a packet with some bad values, and checking the product is as expected.
512      */
513     @Test
514     @Order(12)
515     public void testSCrunProductB() {
516         SCrunProductCommand bcp = new SCrunProductCommand();
517
518         final int errorMainPosition = 0xffff;
519         VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0,
520                 0, null, Command.UNDEFTYPE);
521         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
522         product.setCurrentPosition(errorMainPosition);
523
524         bcp.setNodeIdAndParameters(product.getBridgeProductIndex().toInt(),
525                 new VeluxProductPosition(product.getCurrentPosition()), product.getFunctionalParameters());
526         product = bcp.getProduct();
527         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
528
529         assertEquals(ProductState.EXECUTING, product.getProductState());
530         assertEquals(IGNORE_POSITION, product.getCurrentPosition());
531         assertEquals(IGNORE_POSITION, product.getTarget());
532         assertEquals(UNKNOWN_POSITION, product.getDisplayPosition());
533         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
534         assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition());
535     }
536
537     /**
538      * Test the actuator state.
539      */
540     @Test
541     @Order(14)
542     public void testActuatorState() {
543         VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0,
544                 0, null, Command.UNDEFTYPE);
545         int[] inputStates = { 0, 1, 2, 3, 4, 5, 0x2c, 0x2d, 0xff, 0x80 };
546         ProductState[] expected = { ProductState.NON_EXECUTING, ProductState.ERROR, ProductState.NOT_USED,
547                 ProductState.WAITING_FOR_POWER, ProductState.EXECUTING, ProductState.DONE, ProductState.EXECUTING,
548                 ProductState.DONE, ProductState.UNKNOWN, ProductState.MANUAL };
549         for (int i = 0; i < inputStates.length; i++) {
550             product.setState(inputStates[i]);
551             assertEquals(expected[i], product.getProductState());
552         }
553     }
554
555     /**
556      * Test actuator positions and display positions.
557      */
558     @Test
559     @Order(15)
560     public void testActuatorPositions() {
561         VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0,
562                 0, null, Command.UNDEFTYPE);
563
564         product.setCurrentPosition(MAIN_POSITION_A);
565         product.setTarget(TARGET_POSITION);
566         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
567         product.setVanePosition(VANE_POSITION_A);
568
569         // state uninitialised
570         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
571         assertEquals(MAIN_POSITION_A, product.getDisplayPosition());
572         assertEquals(TARGET_POSITION, product.getTarget());
573         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
574         assertEquals(VANE_POSITION_A, product.getVanePosition());
575
576         // state = done
577         product.setState(ProductState.DONE.value);
578         assertEquals(MAIN_POSITION_A, product.getDisplayPosition());
579         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
580         assertEquals(TARGET_POSITION, product.getTarget());
581         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
582         assertEquals(VANE_POSITION_A, product.getVanePosition());
583
584         // state = not used
585         product.setState(ProductState.NOT_USED.value);
586         assertEquals(MAIN_POSITION_A, product.getDisplayPosition());
587         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
588         assertEquals(TARGET_POSITION, product.getTarget());
589         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
590         assertEquals(VANE_POSITION_A, product.getVanePosition());
591
592         // state = executing
593         product.setState(ProductState.EXECUTING.value);
594         assertEquals(TARGET_POSITION, product.getDisplayPosition());
595         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
596         assertEquals(TARGET_POSITION, product.getTarget());
597         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
598         assertEquals(VANE_POSITION_A, product.getVanePosition());
599
600         // state = manual + excuting
601         product.setState(ProductState.MANUAL.value + ProductState.EXECUTING.value);
602         assertEquals(UNKNOWN_POSITION, product.getDisplayPosition());
603         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
604         assertEquals(TARGET_POSITION, product.getTarget());
605         assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition());
606         assertEquals(VANE_POSITION_A, product.getVanePosition());
607
608         // state = error
609         product.setState(ProductState.ERROR.value);
610         assertEquals(UNKNOWN_POSITION, product.getDisplayPosition());
611         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
612         assertEquals(TARGET_POSITION, product.getTarget());
613         assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition());
614         assertEquals(VANE_POSITION_A, product.getVanePosition());
615     }
616
617     /**
618      * Test the SCgetHouseStatus command by checking for the correct parsing of a 'GW_NODE_STATE_POSITION_CHANGED_NTF'
619      * notification packet. Note: this packet is from a Velux roller shutter with main and vane position.
620      */
621     @Test
622     @Order(16)
623     public void testSCgetHouseStatusOnVelux() {
624         // initialise the test parameters
625         final String packet = "00 2D C8 00 B8 00 F7 FF F7 FF 00 00 F7 FF 00 00 4A E5 00 00";
626         final short command = VeluxKLFAPI.Command.GW_NODE_STATE_POSITION_CHANGED_NTF.getShort();
627
628         // initialise the BCP
629         SCgetHouseStatus bcp = new SCgetHouseStatus();
630
631         // set the packet response
632         bcp.setResponse(command, toByteArray(packet), false);
633
634         // check BCP status
635         assertTrue(bcp.isCommunicationSuccessful());
636         assertTrue(bcp.isCommunicationFinished());
637
638         // initialise the product
639         VeluxProduct product = bcp.getProduct();
640
641         // change actuator type
642         assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType());
643         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
644
645         // check negative assertions
646         assertNotEquals(VANE_POSITION_A, product.getVanePosition());
647
648         // test updating the existing product in the database
649         VeluxExistingProducts existingProducts = getExistingProducts();
650
651         // process this as a receive only command for a Velux product => update IS applied
652         existingProducts.resetDirtyFlag();
653         assertTrue(existingProducts.update(product));
654         assertTrue(existingProducts.isDirty());
655
656         // process this as an information request command for a Velux product => update IS applied
657         existingProducts.resetDirtyFlag();
658         product.setCurrentPosition(MAIN_POSITION_B);
659         assertTrue(existingProducts.update(product));
660         assertTrue(existingProducts.isDirty());
661     }
662
663     /**
664      * Test updating logic with various states applied.
665      */
666     @Test
667     @Order(17)
668     public void testUpdatingLogic() {
669         VeluxExistingProducts existingProducts = getExistingProducts();
670         ProductBridgeIndex index = new ProductBridgeIndex(PRODUCT_INDEX_A);
671         VeluxProduct product = existingProducts.get(index).clone();
672
673         assertEquals(ProductState.DONE, product.getProductState());
674
675         // state = done
676         assertEquals(MAIN_POSITION_A, product.getDisplayPosition());
677         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
678         assertEquals(TARGET_POSITION, product.getTarget());
679         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
680         assertEquals(VANE_POSITION_A, product.getVanePosition());
681
682         // state = not used
683         product.setState(ProductState.NOT_USED.value);
684         product.setCurrentPosition(MAIN_POSITION_A - 1);
685         product.setTarget(TARGET_POSITION - 1);
686         product.setVanePosition(VANE_POSITION_A - 1);
687         existingProducts.update(product);
688         product = existingProducts.get(index).clone();
689         assertEquals(MAIN_POSITION_A, product.getDisplayPosition());
690         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
691         assertEquals(TARGET_POSITION, product.getTarget());
692         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
693         assertEquals(VANE_POSITION_A, product.getVanePosition());
694
695         // state = manual + excuting
696         product.setState(ProductState.MANUAL.value + ProductState.EXECUTING.value);
697         product.setCurrentPosition(MAIN_POSITION_A - 1);
698         product.setTarget(TARGET_POSITION - 1);
699         product.setVanePosition(VANE_POSITION_A - 1);
700         existingProducts.update(product);
701         product = existingProducts.get(index).clone();
702         assertEquals(UNKNOWN_POSITION, product.getDisplayPosition());
703         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
704         assertEquals(TARGET_POSITION, product.getTarget());
705         assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition());
706         assertEquals(VANE_POSITION_A, product.getVanePosition());
707
708         // state = error
709         product.setState(ProductState.ERROR.value);
710         product.setCurrentPosition(MAIN_POSITION_A - 1);
711         product.setTarget(TARGET_POSITION - 1);
712         product.setVanePosition(VANE_POSITION_A - 1);
713         existingProducts.update(product);
714         product = existingProducts.get(index).clone();
715         assertEquals(UNKNOWN_POSITION, product.getDisplayPosition());
716         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
717         assertEquals(TARGET_POSITION, product.getTarget());
718         assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition());
719         assertEquals(VANE_POSITION_A, product.getVanePosition());
720
721         // state = executing
722         product.setState(ProductState.EXECUTING.value);
723         product.setCurrentPosition(MAIN_POSITION_A - 1);
724         product.setTarget(TARGET_POSITION - 1);
725         product.setVanePosition(VANE_POSITION_A - 1);
726         existingProducts.update(product);
727         product = existingProducts.get(index).clone();
728         assertNotEquals(TARGET_POSITION, product.getDisplayPosition());
729         assertNotEquals(MAIN_POSITION_A, product.getCurrentPosition());
730         assertNotEquals(TARGET_POSITION, product.getTarget());
731         assertNotEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
732         assertNotEquals(VANE_POSITION_A, product.getVanePosition());
733
734         // state = done
735         product.setState(ProductState.EXECUTING.value);
736         product.setCurrentPosition(MAIN_POSITION_A);
737         product.setTarget(TARGET_POSITION);
738         product.setVanePosition(VANE_POSITION_A);
739         existingProducts.update(product);
740         product = existingProducts.get(index).clone();
741         assertEquals(TARGET_POSITION, product.getDisplayPosition());
742         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
743         assertEquals(TARGET_POSITION, product.getTarget());
744         assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition());
745         assertEquals(VANE_POSITION_A, product.getVanePosition());
746     }
747
748     /**
749      * Test updating the existing product in the database with special exceptions.
750      */
751     @Test
752     @Order(18)
753     public void testSpecialExceptions() {
754         VeluxExistingProducts existingProducts = getExistingProducts();
755         VeluxProduct existing = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A));
756
757         VeluxProduct product;
758
759         // process this as a receive only command for a Somfy product => update IS applied
760         product = new VeluxProduct(existing.getProductName(), existing.getBridgeProductIndex(), existing.getState(),
761                 existing.getCurrentPosition(), existing.getTarget(), existing.getFunctionalParameters(),
762                 Command.GW_OPENHAB_RECEIVEONLY);
763         existingProducts.resetDirtyFlag();
764         product.setState(ProductState.DONE.value);
765         product.setCurrentPosition(MAIN_POSITION_B);
766         assertTrue(existingProducts.update(product));
767         assertTrue(existingProducts.isDirty());
768
769         // process this as an information request command for a Somfy product => update IS applied
770         product = new VeluxProduct(existing.getProductName(), existing.getBridgeProductIndex(), existing.getState(),
771                 existing.getCurrentPosition(), existing.getTarget(), existing.getFunctionalParameters(),
772                 Command.GW_GET_NODE_INFORMATION_REQ);
773         existingProducts.resetDirtyFlag();
774         product.setCurrentPosition(MAIN_POSITION_A);
775         assertTrue(existingProducts.update(product));
776         assertTrue(existingProducts.isDirty());
777
778         // process this as a receive only command for a Somfy product with bad data => update NOT applied
779         product = new VeluxProduct(existing.getProductName(), existing.getBridgeProductIndex(), existing.getState(),
780                 existing.getCurrentPosition(), existing.getTarget(), existing.getFunctionalParameters(),
781                 Command.GW_OPENHAB_RECEIVEONLY);
782         existingProducts.resetDirtyFlag();
783         product.setCurrentPosition(UNKNOWN_POSITION);
784         product.setTarget(UNKNOWN_POSITION);
785         assertTrue(existingProducts.update(product));
786         assertFalse(existingProducts.isDirty());
787     }
788
789     /**
790      * Test VeluxProductPosition
791      */
792     @Test
793     @Order(19)
794     public void testVeluxProductPosition() {
795         VeluxProductPosition position;
796         int target;
797
798         // on and inside range limits
799         assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MIN).isValid());
800         assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MAX).isValid());
801         assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MAX - 1).isValid());
802         assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MIN + 1).isValid());
803
804         // outside range limits
805         assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MIN - 1).isValid());
806         assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MAX + 1).isValid());
807         assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_IGNORE).isValid());
808         assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_DEFAULT).isValid());
809         assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_STOP).isValid());
810         assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_UNKNOWN).isValid());
811
812         // 80% absolute position
813         position = new VeluxProductPosition(new PercentType(80));
814         assertEquals(0xA000, position.getPositionAsVeluxType());
815         assertTrue(position.isValid());
816
817         // 80% absolute position
818         position = new VeluxProductPosition(new PercentType(80), false);
819         assertEquals(0xA000, position.getPositionAsVeluxType());
820         assertTrue(position.isValid());
821
822         // 80% inverted absolute position (i.e. 20%)
823         position = new VeluxProductPosition(new PercentType(80), true);
824         assertEquals(0x2800, position.getPositionAsVeluxType());
825         assertTrue(position.isValid());
826
827         // 80% positive relative position
828         target = VeluxProductPosition.VPP_VELUX_RELATIVE_ORIGIN
829                 + (VeluxProductPosition.VPP_VELUX_RELATIVE_RANGE * 8 / 10);
830         position = new VeluxProductPosition(new PercentType(80)).overridePositionType(PositionType.OFFSET_POSITIVE);
831         assertTrue(position.isValid());
832         assertEquals(target, position.getPositionAsVeluxType());
833
834         // 80% negative relative position
835         target = VeluxProductPosition.VPP_VELUX_RELATIVE_ORIGIN
836                 - (VeluxProductPosition.VPP_VELUX_RELATIVE_RANGE * 8 / 10);
837         position = new VeluxProductPosition(new PercentType(80)).overridePositionType(PositionType.OFFSET_NEGATIVE);
838         assertTrue(position.isValid());
839         assertEquals(target, position.getPositionAsVeluxType());
840     }
841
842     /**
843      * Test SCrunProductResult results
844      */
845     @Test
846     @Order(20)
847     public void testSCrunProductResults() {
848         SCrunProductCommand bcp = new SCrunProductCommand();
849
850         // create a dummy product to get some functional parameters from
851         VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0,
852                 0, null, Command.UNDEFTYPE);
853         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
854         product.setVanePosition(VANE_POSITION_A);
855         final FunctionalParameters functionalParameters = product.getFunctionalParameters();
856
857         boolean ok;
858
859         // test setting both main and vane position
860         ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, new VeluxProductPosition(MAIN_POSITION_A),
861                 functionalParameters);
862         assertTrue(ok);
863         product = bcp.getProduct();
864         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
865         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
866         assertEquals(VANE_POSITION_A, product.getVanePosition());
867
868         // test setting vane position only
869         ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, null, functionalParameters);
870         assertTrue(ok);
871         product = bcp.getProduct();
872         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
873         assertEquals(IGNORE_POSITION, product.getCurrentPosition());
874         assertEquals(VANE_POSITION_A, product.getVanePosition());
875
876         // test setting main position only
877         ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, new VeluxProductPosition(MAIN_POSITION_A), null);
878         assertTrue(ok);
879         product = bcp.getProduct();
880         product.setActuatorType(ACTUATOR_TYPE_SOMFY);
881         assertEquals(MAIN_POSITION_A, product.getCurrentPosition());
882         assertEquals(UNKNOWN_POSITION, product.getVanePosition());
883
884         // test setting neither
885         ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, null, null);
886         assertFalse(ok);
887     }
888
889     /**
890      * Test SCgetProductStatus exceptional error state processing.
891      */
892     @Test
893     @Order(21)
894     public void testErrorStateMapping() {
895         // initialise the test parameters
896         final String packet = """
897                 0F A3 01 06 01 00 01 02 00 9A 36 03 00 00 00 00 00 00 00 00 00 00 00 00 00\
898                  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\
899                  00 00 00 00 00\
900                 """;
901         final Command command = VeluxKLFAPI.Command.GW_STATUS_REQUEST_NTF;
902
903         // initialise the BCP
904         SCgetProductStatus bcp = new SCgetProductStatus();
905         bcp.setProductId(PRODUCT_INDEX_A);
906
907         // set the packet response
908         bcp.setResponse(command.getShort(), toByteArray(packet), false);
909
910         // check BCP status
911         assertTrue(bcp.isCommunicationSuccessful());
912         assertTrue(bcp.isCommunicationFinished());
913
914         // check the product state
915         assertEquals(ProductState.UNKNOWN.value, bcp.getProduct().getState());
916     }
917 }