]> git.basschouten.com Git - openhab-addons.git/blob
3ec8bb91dc81a0f8f23fce2db21d9d8d7e6abf22
[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.binding.knx.internal.itests;
14
15 import static org.junit.jupiter.api.Assertions.assertEquals;
16 import static org.junit.jupiter.api.Assertions.assertNotNull;
17 import static org.junit.jupiter.api.Assertions.assertNull;
18 import static org.junit.jupiter.api.Assertions.assertTrue;
19
20 import java.util.Arrays;
21 import java.util.HashSet;
22 import java.util.Objects;
23 import java.util.Set;
24
25 import javax.measure.quantity.Temperature;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.junit.jupiter.api.AfterAll;
29 import org.junit.jupiter.api.Assertions;
30 import org.junit.jupiter.api.Test;
31 import org.openhab.binding.knx.internal.client.DummyKNXNetworkLink;
32 import org.openhab.binding.knx.internal.client.DummyProcessListener;
33 import org.openhab.binding.knx.internal.dpt.DPTUtil;
34 import org.openhab.binding.knx.internal.dpt.ValueDecoder;
35 import org.openhab.binding.knx.internal.dpt.ValueEncoder;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.HSBType;
39 import org.openhab.core.library.types.IncreaseDecreaseType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.OpenClosedType;
42 import org.openhab.core.library.types.PercentType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StopMoveType;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.library.types.UpDownType;
47 import org.openhab.core.types.Type;
48 import org.openhab.core.util.ColorUtil;
49 import org.openhab.core.util.HexUtils;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import tuwien.auto.calimero.DataUnitBuilder;
54 import tuwien.auto.calimero.GroupAddress;
55 import tuwien.auto.calimero.KNXException;
56 import tuwien.auto.calimero.datapoint.CommandDP;
57 import tuwien.auto.calimero.datapoint.Datapoint;
58 import tuwien.auto.calimero.dptxlator.TranslatorTypes;
59 import tuwien.auto.calimero.process.ProcessCommunicator;
60 import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
61
62 /**
63  * Integration test to check conversion from raw KNX frame data to OH data types and back.
64  *
65  * This test checks
66  * <ul>
67  * <li>if OH can properly decode raw data payload from KNX frames using {@link ValueDecoder#decode()},
68  * <li>if OH can properly encode the data for handover to Calimero using {@link ValueEncoder#encode()},
69  * <li>if Calimero supports and correctly handles the data conversion to raw bytes for sending.
70  * </ul>
71  *
72  * In addition, it checks if newly integrated releases of Calimero introduce new DPT types not yet
73  * handled by this test. However, new subtypes are not detected.
74  *
75  * @see DummyKNXNetworkLink
76  * @see DummyClient
77  * @author Holger Friedrich - Initial contribution
78  *
79  */
80 @NonNullByDefault
81 public class Back2BackTest {
82     public static final Logger LOGGER = LoggerFactory.getLogger(Back2BackTest.class);
83     static Set<Integer> dptTested = new HashSet<>();
84     boolean testsMissing = false;
85
86     /**
87      * helper method for integration tests
88      *
89      * @param dpt DPT type, e.g. "251.600", see 03_07_02-Datapoint-Types-v02.02.01-AS.pdf
90      * @param rawData byte array containing raw data, known content
91      * @param ohReferenceData OpenHAB data type, initialized to known good value
92      * @param maxDistance byte array containing maximal deviations when comparing byte arrays (rawData against created
93      *            KNX frame), may be empty if no deviation is considered
94      * @param bitmask to mask certain bits in the raw to raw comparison, required for multi-valued KNX frames
95      */
96     void helper(String dpt, byte[] rawData, Type ohReferenceData, byte[] maxDistance, byte[] bitmask) {
97         try {
98             DummyKNXNetworkLink link = new DummyKNXNetworkLink();
99             ProcessCommunicator pc = new ProcessCommunicatorImpl(link);
100             DummyProcessListener processListener = new DummyProcessListener();
101             pc.addProcessListener(processListener);
102
103             GroupAddress groupAddress = new GroupAddress(2, 4, 6);
104             Datapoint datapoint = new CommandDP(groupAddress, "dummy GA", 0,
105                     DPTUtil.NORMALIZED_DPT.getOrDefault(dpt, dpt));
106
107             // 0) check usage of helper()
108             assertEquals(true, rawData.length > 0);
109             if (maxDistance.length == 0) {
110                 maxDistance = new byte[rawData.length];
111             }
112             assertEquals(rawData.length, maxDistance.length, "incorrect length of maxDistance array");
113             if (bitmask.length == 0) {
114                 bitmask = new byte[rawData.length];
115                 Arrays.fill(bitmask, (byte) 0xff);
116             }
117             assertEquals(rawData.length, bitmask.length, "incorrect length of bitmask array");
118             int mainType = Integer.parseUnsignedInt(dpt.substring(0, dpt.indexOf('.')));
119             dptTested.add(Integer.valueOf(mainType));
120             // check if OH would be able to send out a frame, given the type
121             Set<Integer> knownWorking = Set.of(1, 3, 5);
122             if (!knownWorking.contains(mainType)) {
123                 Set<Class<? extends Type>> allowedTypes = DPTUtil.getAllowedTypes("" + mainType);
124                 if (!allowedTypes.contains(ohReferenceData.getClass())) {
125                     LOGGER.warn(
126                             "test for DPT {} uses type {} which is not contained in DPT_TYPE_MAP, sending may not be allowed",
127                             dpt, ohReferenceData.getClass());
128                 }
129             }
130
131             // 1) check if the decoder works (rawData to known good type ohReferenceData)
132             //
133             // This test is based on known raw data. The mapping to openHAB type is known and confirmed.
134             // In this test, only ValueDecoder.decode() is involved.
135
136             // raw data of the DPT on application layer, without all headers from the layers below
137             // see 03_07_02-Datapoint-Types-v02.02.01-AS.pdf
138             Type ohData = (Type) ValueDecoder.decode(dpt, rawData, ohReferenceData.getClass());
139             assertNotNull(ohData, "could not decode frame data for DPT " + dpt);
140             if ((ohReferenceData instanceof HSBType hsbReferenceData) && (ohData instanceof HSBType hsbData)) {
141                 assertTrue(hsbReferenceData.closeTo(hsbData, 0.001),
142                         "comparing reference to decoded value for DPT " + dpt);
143             } else {
144                 assertEquals(ohReferenceData, ohData, "comparing reference to decoded value: failed for DPT " + dpt
145                         + ", check ValueEncoder.decode()");
146             }
147
148             // 2) check the encoding (ohData to raw data)
149             //
150             // Test approach is to a) encode the value into String format using ValueEncoder.encode(),
151             // b) pass it to Calimero for conversion into a raw representation, and
152             // c) finally grab raw data bytes from a custom KNXNetworkLink implementation
153             String enc = ValueEncoder.encode(ohData, dpt);
154             pc.write(datapoint, enc);
155
156             byte[] frame = link.getLastFrame();
157             assertNotNull(frame);
158             // remove header; for compact frames extract data byte from header
159             frame = DataUnitBuilder.extractASDU(frame);
160             assertEquals(rawData.length, frame.length,
161                     "unexpected length of KNX frame: " + HexUtils.bytesToHex(frame, " "));
162             for (int i = 0; i < rawData.length; i++) {
163                 assertEquals(rawData[i] & bitmask[i] & 0xff, frame[i] & bitmask[i] & 0xff, maxDistance[i],
164                         "unexpected content in encoded data, " + i);
165             }
166
167             // 3) Check date provided by Calimero library as input via loopback, it should match the initial data
168             //
169             // Deviations in some bytes of the frame may be possible due to data conversion, e.g. for HSBType.
170             // This is why maxDistance is used.
171             byte[] input = processListener.getLastFrame();
172             LOGGER.info("loopback {}", HexUtils.bytesToHex(input, " "));
173             assertNotNull(input);
174             assertEquals(rawData.length, input.length, "unexpected length of loopback frame");
175             for (int i = 0; i < rawData.length; i++) {
176                 assertEquals(rawData[i] & bitmask[i] & 0xff, input[i] & bitmask[i] & 0xff, maxDistance[i],
177                         "unexpected content in loopback data, " + i);
178             }
179
180             pc.close();
181         } catch (KNXException e) {
182             LOGGER.warn("exception occurred", e.toString());
183             assertEquals("", e.toString());
184         }
185     }
186
187     void helper(String dpt, byte[] rawData, Type ohReferenceData) {
188         helper(dpt, rawData, ohReferenceData, new byte[0], new byte[0]);
189     }
190
191     @Test
192     void testDpt1() {
193         helper("1.001", new byte[] { 0 }, OnOffType.OFF);
194         helper("1.001", new byte[] { 1 }, OnOffType.ON);
195         helper("1.001", new byte[] { 0 }, OpenClosedType.CLOSED);
196         helper("1.001", new byte[] { 1 }, OpenClosedType.OPEN);
197         helper("1.002", new byte[] { 0 }, OnOffType.OFF);
198         helper("1.002", new byte[] { 1 }, OnOffType.ON);
199         helper("1.002", new byte[] { 0 }, OpenClosedType.CLOSED);
200         helper("1.002", new byte[] { 1 }, OpenClosedType.OPEN);
201         helper("1.003", new byte[] { 0 }, OnOffType.OFF);
202         helper("1.003", new byte[] { 1 }, OnOffType.ON);
203         helper("1.003", new byte[] { 0 }, OpenClosedType.CLOSED);
204         helper("1.003", new byte[] { 1 }, OpenClosedType.OPEN);
205         helper("1.004", new byte[] { 0 }, OnOffType.OFF);
206         helper("1.004", new byte[] { 1 }, OnOffType.ON);
207         helper("1.004", new byte[] { 0 }, OpenClosedType.CLOSED);
208         helper("1.004", new byte[] { 1 }, OpenClosedType.OPEN);
209         helper("1.005", new byte[] { 0 }, OnOffType.OFF);
210         helper("1.005", new byte[] { 1 }, OnOffType.ON);
211         helper("1.005", new byte[] { 0 }, OpenClosedType.CLOSED);
212         helper("1.005", new byte[] { 1 }, OpenClosedType.OPEN);
213         helper("1.006", new byte[] { 0 }, OnOffType.OFF);
214         helper("1.006", new byte[] { 1 }, OnOffType.ON);
215         helper("1.006", new byte[] { 0 }, OpenClosedType.CLOSED);
216         helper("1.006", new byte[] { 1 }, OpenClosedType.OPEN);
217         helper("1.007", new byte[] { 0 }, OnOffType.OFF);
218         helper("1.007", new byte[] { 1 }, OnOffType.ON);
219         helper("1.007", new byte[] { 0 }, OpenClosedType.CLOSED);
220         helper("1.007", new byte[] { 1 }, OpenClosedType.OPEN);
221         helper("1.008", new byte[] { 0 }, UpDownType.UP);
222         helper("1.008", new byte[] { 1 }, UpDownType.DOWN);
223         // NOTE: This is how DPT 1.009 is defined: 0: open, 1: closed
224         // For historical reasons it is defined the other way on OH
225         helper("1.009", new byte[] { 0 }, OnOffType.OFF);
226         helper("1.009", new byte[] { 1 }, OnOffType.ON);
227         helper("1.009", new byte[] { 0 }, OpenClosedType.CLOSED);
228         helper("1.009", new byte[] { 1 }, OpenClosedType.OPEN);
229         helper("1.010", new byte[] { 0 }, StopMoveType.STOP);
230         helper("1.010", new byte[] { 1 }, StopMoveType.MOVE);
231         helper("1.011", new byte[] { 0 }, OnOffType.OFF);
232         helper("1.011", new byte[] { 1 }, OnOffType.ON);
233         helper("1.011", new byte[] { 0 }, OpenClosedType.CLOSED);
234         helper("1.011", new byte[] { 1 }, OpenClosedType.OPEN);
235         helper("1.012", new byte[] { 0 }, OnOffType.OFF);
236         helper("1.012", new byte[] { 1 }, OnOffType.ON);
237         helper("1.012", new byte[] { 0 }, OpenClosedType.CLOSED);
238         helper("1.012", new byte[] { 1 }, OpenClosedType.OPEN);
239         helper("1.013", new byte[] { 0 }, OnOffType.OFF);
240         helper("1.013", new byte[] { 1 }, OnOffType.ON);
241         helper("1.013", new byte[] { 0 }, OpenClosedType.CLOSED);
242         helper("1.013", new byte[] { 1 }, OpenClosedType.OPEN);
243         helper("1.014", new byte[] { 0 }, OnOffType.OFF);
244         helper("1.014", new byte[] { 1 }, OnOffType.ON);
245         helper("1.014", new byte[] { 0 }, OpenClosedType.CLOSED);
246         helper("1.014", new byte[] { 1 }, OpenClosedType.OPEN);
247         helper("1.015", new byte[] { 0 }, OnOffType.OFF);
248         helper("1.015", new byte[] { 1 }, OnOffType.ON);
249         helper("1.015", new byte[] { 0 }, OpenClosedType.CLOSED);
250         helper("1.015", new byte[] { 1 }, OpenClosedType.OPEN);
251         helper("1.016", new byte[] { 0 }, OnOffType.OFF);
252         helper("1.016", new byte[] { 1 }, OnOffType.ON);
253         helper("1.016", new byte[] { 0 }, OpenClosedType.CLOSED);
254         helper("1.016", new byte[] { 1 }, OpenClosedType.OPEN);
255         // DPT 1.017 is a special case, "trigger" has no "value", both 0 and 1 shall trigger
256         helper("1.017", new byte[] { 0 }, OnOffType.OFF);
257         helper("1.017", new byte[] { 0 }, OpenClosedType.CLOSED);
258         // Calimero maps it always to 0
259         // helper("1.017", new byte[] { 1 }, OnOffType.ON);
260         helper("1.018", new byte[] { 0 }, OnOffType.OFF);
261         helper("1.018", new byte[] { 1 }, OnOffType.ON);
262         helper("1.018", new byte[] { 0 }, OpenClosedType.CLOSED);
263         helper("1.018", new byte[] { 1 }, OpenClosedType.OPEN);
264         helper("1.019", new byte[] { 0 }, OnOffType.OFF);
265         helper("1.019", new byte[] { 1 }, OnOffType.ON);
266         helper("1.019", new byte[] { 0 }, OpenClosedType.CLOSED);
267         helper("1.019", new byte[] { 1 }, OpenClosedType.OPEN);
268
269         helper("1.021", new byte[] { 0 }, OnOffType.OFF);
270         helper("1.021", new byte[] { 1 }, OnOffType.ON);
271         helper("1.021", new byte[] { 0 }, OpenClosedType.CLOSED);
272         helper("1.021", new byte[] { 1 }, OpenClosedType.OPEN);
273         // DPT 1.022 is mapped to decimal, Calimero does not follow the recommendation
274         // from KNX spec to add offset 1
275         helper("1.022", new byte[] { 0 }, DecimalType.valueOf("0"));
276         helper("1.022", new byte[] { 1 }, DecimalType.valueOf("1"));
277         helper("1.023", new byte[] { 0 }, OnOffType.OFF);
278         helper("1.023", new byte[] { 1 }, OnOffType.ON);
279         helper("1.023", new byte[] { 0 }, OpenClosedType.CLOSED);
280         helper("1.023", new byte[] { 1 }, OpenClosedType.OPEN);
281         helper("1.024", new byte[] { 0 }, OnOffType.OFF);
282         helper("1.024", new byte[] { 1 }, OnOffType.ON);
283         helper("1.024", new byte[] { 0 }, OpenClosedType.CLOSED);
284         helper("1.024", new byte[] { 1 }, OpenClosedType.OPEN);
285
286         helper("1.100", new byte[] { 0 }, OnOffType.OFF);
287         helper("1.100", new byte[] { 1 }, OnOffType.ON);
288         helper("1.100", new byte[] { 0 }, OpenClosedType.CLOSED);
289         helper("1.100", new byte[] { 1 }, OpenClosedType.OPEN);
290
291         helper("1.1200", new byte[] { 0 }, OnOffType.OFF);
292         helper("1.1200", new byte[] { 1 }, OnOffType.ON);
293         helper("1.1200", new byte[] { 0 }, OpenClosedType.CLOSED);
294         helper("1.1200", new byte[] { 1 }, OpenClosedType.OPEN);
295         helper("1.1201", new byte[] { 0 }, OnOffType.OFF);
296         helper("1.1201", new byte[] { 1 }, OnOffType.ON);
297         helper("1.1201", new byte[] { 0 }, OpenClosedType.CLOSED);
298         helper("1.1201", new byte[] { 1 }, OpenClosedType.OPEN);
299     }
300
301     @Test
302     void testDpt2() {
303         for (int subType = 1; subType <= 12; subType++) {
304             helper("2." + String.format("%03d", subType), new byte[] { 3 }, new DecimalType(3));
305         }
306     }
307
308     @Test
309     void testDpt3() {
310         // DPT 3.007 and DPT 3.008 consist of a control bit (1 bit) and stepsize (3 bit)
311         // if stepsize is 0, OH will ignore the command
312         byte controlBit = 1 << 3;
313         // loop all other step sizes and check only the control bit
314         for (byte i = 1; i < 8; i++) {
315             helper("3.007", new byte[] { i }, IncreaseDecreaseType.DECREASE, new byte[0], new byte[] { controlBit });
316             helper("3.007", new byte[] { (byte) (i + controlBit) }, IncreaseDecreaseType.INCREASE, new byte[0],
317                     new byte[] { controlBit });
318             helper("3.008", new byte[] { i }, UpDownType.UP, new byte[0], new byte[] { controlBit });
319             helper("3.008", new byte[] { (byte) (i + controlBit) }, UpDownType.DOWN, new byte[0],
320                     new byte[] { controlBit });
321         }
322
323         // check if OH ignores incoming frames with mask 0 (mapped to UndefType)
324         Assertions.assertFalse(ValueDecoder.decode("3.007", new byte[] { 0 },
325                 IncreaseDecreaseType.class) instanceof IncreaseDecreaseType);
326         Assertions.assertFalse(ValueDecoder.decode("3.007", new byte[] { controlBit },
327                 IncreaseDecreaseType.class) instanceof IncreaseDecreaseType);
328         Assertions.assertFalse(ValueDecoder.decode("3.008", new byte[] { 0 }, UpDownType.class) instanceof UpDownType);
329         Assertions.assertFalse(
330                 ValueDecoder.decode("3.008", new byte[] { controlBit }, UpDownType.class) instanceof UpDownType);
331     }
332
333     @Test
334     void testDpt5() {
335         helper("5.001", new byte[] { 0 }, new QuantityType<>("0 %"));
336         helper("5.001", new byte[] { (byte) 0xff }, new QuantityType<>("100 %"));
337         // fallback: PercentType
338         helper("5.001", new byte[] { 0 }, new PercentType(0));
339         helper("5.001", new byte[] { (byte) 0x80 }, new PercentType(50));
340         helper("5.001", new byte[] { (byte) 0xff }, new PercentType(100));
341
342         helper("5.003", new byte[] { 0 }, new QuantityType<>("0 °"));
343         helper("5.003", new byte[] { (byte) 0xff }, new QuantityType<>("360 °"));
344         helper("5.004", new byte[] { 0 }, new QuantityType<>("0 %"));
345         helper("5.004", new byte[] { (byte) 0x64 }, new QuantityType<>("100 %"));
346         helper("5.004", new byte[] { (byte) 0xff }, new QuantityType<>("255 %"));
347         // PercentType cannot encode values >100%, not supported for 5.004
348         helper("5.005", new byte[] { 42 }, new DecimalType(42));
349         helper("5.005", new byte[] { (byte) 0xff }, new DecimalType(255));
350         helper("5.006", new byte[] { 0 }, new DecimalType(0));
351         helper("5.006", new byte[] { 42 }, new DecimalType(42));
352         helper("5.006", new byte[] { (byte) 0xfe }, new DecimalType(254));
353
354         helper("5.010", new byte[] { 42 }, new DecimalType(42));
355         helper("5.010", new byte[] { (byte) 0xff }, new DecimalType(255));
356     }
357
358     @Test
359     void testDpt6() {
360         helper("6.001", new byte[] { 0 }, new QuantityType<>("0 %"));
361         helper("6.001", new byte[] { (byte) 0x7f }, new QuantityType<>("127 %"));
362         helper("6.001", new byte[] { (byte) 0xff }, new QuantityType<>("-1 %"));
363         // PercentType cannot encode values >100% or <0%, not supported for 6.001
364
365         helper("6.010", new byte[] { 0 }, new DecimalType(0));
366         helper("6.010", new byte[] { (byte) 0x7f }, new DecimalType(127));
367         helper("6.010", new byte[] { (byte) 0xff }, new DecimalType(-1));
368     }
369
370     @Test
371     void testDpt7() {
372         // TODO add tests for more subtypes
373         helper("7.001", new byte[] { 0, 42 }, new DecimalType(42));
374         helper("7.001", new byte[] { (byte) 0xff, (byte) 0xff }, new DecimalType(65535));
375     }
376
377     @Test
378     void testDpt8() {
379         helper("8.001", new byte[] { (byte) 0x7f, (byte) 0xff }, new DecimalType(32767));
380         helper("8.001", new byte[] { (byte) 0x80, (byte) 0x00 }, new DecimalType(-32768));
381         helper("8.002", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 ms"));
382         helper("8.002", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 ms"));
383         helper("8.002", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
384         helper("8.003", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-327680 ms"));
385         helper("8.003", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("327670 ms"));
386         helper("8.003", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
387         helper("8.004", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-3276800 ms"));
388         helper("8.004", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("3276700 ms"));
389         helper("8.004", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
390         helper("8.005", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 s"));
391         helper("8.005", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 s"));
392         helper("8.005", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 s"));
393         helper("8.006", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 min"));
394         helper("8.006", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 min"));
395         helper("8.006", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 min"));
396         helper("8.007", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 h"));
397         helper("8.007", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 h"));
398         helper("8.007", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 h"));
399
400         helper("8.011", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 °"));
401         helper("8.011", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 °"));
402         helper("8.011", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 °"));
403         helper("8.012", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 m"));
404         helper("8.012", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 m"));
405         helper("8.012", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 m"));
406     }
407
408     @Test
409     void testDpt9() {
410         // TODO add tests for more subtypes
411         helper("9.001", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<Temperature>("1 °C"));
412     }
413
414     @Test
415     void testDpt10() {
416         // TODO check handling of DPT10: date is not set to current date, but 1970-01-01 + offset if day is given
417         // maybe we should change the semantics and use current date + offset if day is given
418
419         // note: local timezone is set when creating DateTimeType, for example "1970-01-01Thh:mm:ss.000+0100"
420
421         // no-day
422         assertTrue(Objects
423                 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, 0 }, DecimalType.class))
424                 .startsWith("1970-01-01T17:30:00.000+"));
425         // Thursday, this is correct for 1970-01-01
426         assertTrue(Objects
427                 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, 0 }, DecimalType.class))
428                 .startsWith("1970-01-01T17:30:00.000+"));
429         // Monday -> 1970-01-05
430         assertTrue(Objects
431                 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x31, (byte) 0x1e, 0 }, DecimalType.class))
432                 .startsWith("1970-01-05T17:30:00.000+"));
433
434         // Thursday, otherwise first byte of encoded data will not match
435         helper("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"));
436         helper("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"), new byte[0],
437                 new byte[] { (byte) 0x1f, (byte) 0xff, (byte) 0xff });
438     }
439
440     @Test
441     void testDpt11() {
442         // note: local timezone and dst is set when creating DateTimeType, for example "2019-06-12T00:00:00.000+0200"
443         helper("11.001", new byte[] { (byte) 12, 6, 19 }, new DateTimeType("2019-06-12"));
444     }
445
446     @Test
447     void testDpt12() {
448         helper("12.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
449                 new DecimalType("4294967294"));
450         helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 s"));
451         helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 min"));
452         helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 min"));
453         helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 h"));
454         helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 h"));
455         helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("60 min"));
456
457         helper("12.1200", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 l"));
458         helper("12.1200", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
459                 new QuantityType<>("4294967294 l"));
460         helper("12.1201", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 m³"));
461         helper("12.1201", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
462                 new QuantityType<>("4294967294 m³"));
463     }
464
465     @Test
466     void testDpt13() {
467         helper("13.001", new byte[] { 0, 0, 0, 0 }, new DecimalType(0));
468         helper("13.001", new byte[] { 0, 0, 0, 42 }, new DecimalType(42));
469         helper("13.001", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
470                 new DecimalType(2147483647));
471         // KNX representation typically uses two's complement
472         helper("13.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }, new DecimalType(-1));
473         helper("13.001", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 }, new DecimalType(-2147483648));
474         helper("13.002", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 m³/h"));
475         helper("13.002", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
476                 new QuantityType<>("-2147483648 m³/h"));
477         helper("13.002", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
478                 new QuantityType<>("2147483647 m³/h"));
479
480         helper("13.010", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 Wh"));
481         helper("13.010", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
482                 new QuantityType<>("-2147483648 Wh"));
483         helper("13.010", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
484                 new QuantityType<>("2147483647 Wh"));
485         helper("13.011", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 VAh"));
486         helper("13.011", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
487                 new QuantityType<>("-2147483648 VAh"));
488         helper("13.011", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
489                 new QuantityType<>("2147483647 VAh"));
490         helper("13.012", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 varh"));
491         helper("13.012", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
492                 new QuantityType<>("-2147483648 varh"));
493         helper("13.012", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
494                 new QuantityType<>("2147483647 varh"));
495         helper("13.013", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 kWh"));
496         helper("13.013", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
497                 new QuantityType<>("-2147483648 kWh"));
498         helper("13.013", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
499                 new QuantityType<>("2147483647 kWh"));
500         helper("13.014", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 VAh"));
501         helper("13.014", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
502                 new QuantityType<>("-2147483648000 VAh"));
503         helper("13.014", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
504                 new QuantityType<>("2147483647000 VAh"));
505         helper("13.015", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 kvarh"));
506         helper("13.015", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
507                 new QuantityType<>("-2147483648 kvarh"));
508         helper("13.015", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
509                 new QuantityType<>("2147483647 kvarh"));
510         helper("13.016", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 MWh"));
511         helper("13.016", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
512                 new QuantityType<>("-2147483648 MWh"));
513         helper("13.016", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
514                 new QuantityType<>("2147483647 MWh"));
515
516         helper("13.100", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 s"));
517         helper("13.100", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
518                 new QuantityType<>("-2147483648 s"));
519         helper("13.100", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
520                 new QuantityType<>("2147483647 s"));
521
522         helper("13.1200", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 l"));
523         helper("13.1200", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
524                 new QuantityType<>("-2147483648 l"));
525         helper("13.1200", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
526                 new QuantityType<>("2147483647 l"));
527         helper("13.1201", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 m³"));
528         helper("13.1201", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
529                 new QuantityType<>("-2147483648 m³"));
530         helper("13.1201", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
531                 new QuantityType<>("2147483647 m³"));
532     }
533
534     @Test
535     void testDpt14() {
536         // TODO add tests for more subtypes
537         helper("14.068", new byte[] { (byte) 0x3f, (byte) 0x80, 0, 0 }, new QuantityType<Temperature>("1 °C"));
538     }
539
540     @Test
541     void testDpt16() {
542         helper("16.000", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
543                 0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
544         helper("16.001", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
545                 0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
546     }
547
548     @Test
549     void testDpt17() {
550         helper("17.001", new byte[] { 0 }, new DecimalType(0));
551         helper("17.001", new byte[] { 42 }, new DecimalType(42));
552         helper("17.001", new byte[] { 63 }, new DecimalType(63));
553     }
554
555     @Test
556     void testDpt18() {
557         // scene, activate 0..63
558         helper("18.001", new byte[] { 0 }, new DecimalType(0));
559         helper("18.001", new byte[] { 42 }, new DecimalType(42));
560         helper("18.001", new byte[] { 63 }, new DecimalType(63));
561         // scene, learn += 0x80
562         helper("18.001", new byte[] { (byte) (0x80 + 0) }, new DecimalType(0x80));
563         helper("18.001", new byte[] { (byte) (0x80 + 42) }, new DecimalType(0x80 + 42));
564         helper("18.001", new byte[] { (byte) (0x80 + 63) }, new DecimalType(0x80 + 63));
565     }
566
567     @Test
568     void testDpt19() {
569         // 2019-01-15 17:30:00
570         helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
571                 new DateTimeType("2019-01-15T17:30:00"));
572         helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
573                 new DateTimeType("2019-01-15T17:30:00"));
574         // 2019-07-15 17:30:00
575         helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
576                 new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
577         helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
578                 new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
579     }
580
581     @Test
582     void testDpt20() {
583         // test default String representation of enum (incomplete)
584         helper("20.001", new byte[] { 0 }, new StringType("autonomous"));
585         helper("20.001", new byte[] { 1 }, new StringType("slave"));
586         helper("20.001", new byte[] { 2 }, new StringType("master"));
587
588         helper("20.002", new byte[] { 0 }, new StringType("building in use"));
589         helper("20.002", new byte[] { 1 }, new StringType("building not used"));
590         helper("20.002", new byte[] { 2 }, new StringType("building protection"));
591
592         // test DecimalType representation of enum
593         int[] subTypes = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 20, 21, 100, 101, 102, 103, 104, 105,
594                 106, 107, 108, 109, 110, 111, 112, 113, 114, 120, 121, 122, 600, 601, 602, 603, 604, 605, 606, 607, 608,
595                 609, 610, 801, 802, 803, 804, 1000, 1001, 1002, 1003, 1004, 1005, 1200, 1202 };
596         for (int subType : subTypes) {
597             helper("20." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
598         }
599         // once these DPTs are available in Calimero, add to check above
600         int[] unsupportedSubTypes = new int[] { 22, 115, 611, 612, 613, 1203, 1204, 1205, 1206, 1207, 1208, 1209 };
601         for (int subType : unsupportedSubTypes) {
602             assertNull(ValueDecoder.decode("20." + String.format("%03d", subType), new byte[] { 0 }, StringType.class));
603         }
604     }
605
606     @Test
607     void testDpt21() {
608         // test default String representation of bitfield (incomplete)
609         helper("21.001", new byte[] { 5 }, new StringType("overridden, out of service"));
610
611         // test DecimalType representation of bitfield
612         int[] subTypes = new int[] { 1, 2, 100, 101, 102, 103, 104, 105, 106, 601, 1000, 1001, 1002, 1010 };
613         for (int subType : subTypes) {
614             helper("21." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
615         }
616         // once these DPTs are available in Calimero, add to check above
617         assertNull(ValueDecoder.decode("21.1200", new byte[] { 0 }, StringType.class));
618         assertNull(ValueDecoder.decode("21.1201", new byte[] { 0 }, StringType.class));
619     }
620
621     @Test
622     void testDpt22() {
623         // test default String representation of bitfield (incomplete)
624         helper("22.101", new byte[] { 1, 0 }, new StringType("heating mode"));
625         helper("22.101", new byte[] { 1, 2 }, new StringType("heating mode, heating eco mode"));
626
627         // test DecimalType representation of bitfield
628         helper("22.101", new byte[] { 0, 2 }, new DecimalType(2));
629         helper("22.1000", new byte[] { 0, 2 }, new DecimalType(2));
630         // once these DPTs are available in Calimero, add to check above
631         assertNull(ValueDecoder.decode("22.100", new byte[] { 0, 2 }, StringType.class));
632         assertNull(ValueDecoder.decode("22.1010", new byte[] { 0, 2 }, StringType.class));
633     }
634
635     @Test
636     void testDpt28() {
637         // null terminated strings, UTF8
638         helper("28.001", new byte[] { 0x31, 0x32, 0x33, 0x34, 0x0 }, new StringType("1234"));
639         helper("28.001", new byte[] { (byte) 0xce, (byte) 0xb5, 0x34, 0x0 }, new StringType("\u03b54"));
640     }
641
642     @Test
643     void testDpt29() {
644         helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
645         helper("29.010", new byte[] { (byte) 0x80, 0, 0, 0, 0, 0, 0, 0 },
646                 new QuantityType<>("-9223372036854775808 Wh"));
647         helper("29.010", new byte[] { (byte) 0xff, 0, 0, 0, 0, 0, 0, 0 }, new QuantityType<>("-72057594037927936 Wh"));
648         helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
649         helper("29.011", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 VAh"));
650         helper("29.012", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 varh"));
651     }
652
653     @Test
654     void testDpt229() {
655         // special DPT for metering, allows several units and different scaling
656         // -> Calimero uses scaling, but always encodes as dimensionless value
657         final int dimensionlessCounter = 0b10111010;
658         helper("229.001", new byte[] { 0, 0, 0, 0, (byte) dimensionlessCounter, 0 }, new DecimalType(0));
659     }
660
661     @Test
662     void testColorDpts() {
663         // HSB
664         helper("232.600", new byte[] { 123, 45, 67 }, ColorUtil.rgbToHsb(new int[] { 123, 45, 67 }));
665         // RGB, MDT specific
666         helper("232.60000", new byte[] { 123, 45, 67 }, new HSBType("173.6, 17.6, 26.3"));
667
668         // xyY
669         int x = (int) (14.65 * 65535.0 / 100.0);
670         int y = (int) (11.56 * 65535.0 / 100.0);
671         // encoding is always xy and brightness (C+B, 0x03), do not test other combinations
672         helper("242.600", new byte[] { (byte) ((x >> 8) & 0xff), (byte) (x & 0xff), (byte) ((y >> 8) & 0xff),
673                 (byte) (y & 0xff), (byte) 0x28, 0x3 }, new HSBType("220,90,50"), new byte[] { 0, 8, 0, 8, 0, 0 },
674                 new byte[0]);
675         // TODO check brightness
676
677         // RGBW, only RGB part
678         helper("251.600", new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e }, new HSBType("207, 23, 19"),
679                 new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
680         // RGBW, only RGB part
681         helper("251.600", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00, 0x0e },
682                 new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
683         // RGBW, only W part
684         helper("251.600", new byte[] { 0x0, 0x0, 0x0, 0x1A, 0x00, 0x01 }, new PercentType("10.2"));
685         // RGBW, all
686         helper("251.60600", new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xff, 0x00, 0x0f },
687                 new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 2, 0, 0 }, new byte[0]);
688         // RGBW, mixed
689         int[] rgbw = new int[] { 240, 0x0, 0x0, 0x0f };
690         HSBType hsb = ColorUtil.rgbToHsb(rgbw);
691         helper("251.60600", new byte[] { (byte) rgbw[0], (byte) rgbw[1], (byte) rgbw[2], (byte) rgbw[3], 0x00, 0x0f },
692                 hsb, new byte[] { 2, 2, 2, 2, 0, 0 }, new byte[0]);
693     }
694
695     @Test
696     void testColorTransitionDpts() {
697         // DPT 243.600 DPT_Colour_Transition_xyY
698         // time(2) y(2) x(2), %brightness(1), flags(1)
699         helper("243.600", new byte[] { 0, 5, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
700                 new StringType("(0.9922, 0.4961) 16.5 % 0.5 s"));
701         // DPT 249.600 DPT_Brightness_Colour_Temperature_Transition
702         // time(2) colortemp(2), brightness(1), flags(1)
703         helper("249.600", new byte[] { 0, 5, 0, 40, 127, 7 }, new StringType("49.8 % 40 K 0.5 s"));
704         // DPT 250.600 DPT_Brightness_Colour_Temperature_Control
705         // cct(1) cb(1) flags(1)
706         helper("250.600", new byte[] { 0x0f, 0x0e, 3 }, new StringType("CT increase 7 steps BRT increase 6 steps"));
707         // DPT 252.600 DPT_Relative_Control_RGBW
708         // r(1) g(1) b(1) w(1) flags(1)
709         helper("252.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x0c, 0x0f },
710                 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps W increase 4 steps"));
711         // DPT 253.600 DPT_Relative_Control_xyY
712         // cs(1) ct(1) cb(1) flags(1)
713         helper("253.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x7 },
714                 new StringType("x increase 7 steps y increase 6 steps Y increase 5 steps"));
715         // DPT 254.600 DPT_Relative_Control_RGB
716         // cr(1) cg(1) cb(1)
717         helper("254.600", new byte[] { 0x0f, 0x0e, 0x0d },
718                 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps"));
719     }
720
721     @Test
722     @AfterAll
723     static void checkForMissingMainTypes() {
724         // checks if we have itests for all main DPT types supported by Calimero library,
725         // data is collected within method helper()
726         var wrapper = new Object() {
727             boolean testsMissing = false;
728         };
729         TranslatorTypes.getAllMainTypes().forEach((i, t) -> {
730             if (!dptTested.contains(i)) {
731                 LOGGER.warn("missing tests for main DPT type " + i);
732                 wrapper.testsMissing = true;
733             }
734         });
735         assertEquals(false, wrapper.testsMissing, "add tests for new DPT main types");
736     }
737 }