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