]> git.basschouten.com Git - openhab-addons.git/blob
7c633bf8dd5de5519c284d00fcc9821e9f939b6e
[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[] { (byte) (subType % 4) },
304                     new DecimalType(subType % 4));
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         helper("6.020", new byte[] { 9 }, StringType.valueOf("0/0/0/0/1 0"));
370     }
371
372     @Test
373     void testDpt7() {
374         helper("7.001", new byte[] { 0, 42 }, new DecimalType(42));
375         helper("7.001", new byte[] { (byte) 0xff, (byte) 0xff }, new DecimalType(65535));
376         helper("7.002", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 ms"));
377         helper("7.002", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
378         helper("7.002", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 ms"));
379         helper("7.003", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
380         helper("7.003", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<>("1000 ms"));
381         helper("7.003", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("2550 ms"));
382         helper("7.003", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("655350 ms"));
383         helper("7.004", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
384         helper("7.004", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("25500 ms"));
385         helper("7.004", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("6553500 ms"));
386         helper("7.005", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 s"));
387         helper("7.005", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 s"));
388         helper("7.005", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 s"));
389         helper("7.006", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 min"));
390         helper("7.006", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 min"));
391         helper("7.006", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 min"));
392         helper("7.006", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("3932100 s"));
393         helper("7.007", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 h"));
394         helper("7.007", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 h"));
395         helper("7.007", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("918000 s"));
396         helper("7.007", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 h"));
397
398         helper("7.010", new byte[] { 0, 42 }, new DecimalType(42));
399         helper("7.010", new byte[] { (byte) 0xff, (byte) 0xff }, new DecimalType(65535));
400         helper("7.011", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 mm"));
401         helper("7.011", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 mm"));
402         helper("7.011", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 mm"));
403         helper("7.012", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 mA"));
404         helper("7.012", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 mA"));
405         helper("7.012", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 mA"));
406         helper("7.013", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 lx"));
407         helper("7.013", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 lx"));
408         helper("7.013", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 lx"));
409
410         helper("7.600", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 K"));
411         helper("7.600", new byte[] { (byte) 0x00, (byte) 0xff }, new QuantityType<>("255 K"));
412         helper("7.600", new byte[] { (byte) 0xff, (byte) 0xff }, new QuantityType<>("65535 K"));
413     }
414
415     @Test
416     void testDpt8() {
417         helper("8.001", new byte[] { (byte) 0x7f, (byte) 0xff }, new DecimalType(32767));
418         helper("8.001", new byte[] { (byte) 0x80, (byte) 0x00 }, new DecimalType(-32768));
419         helper("8.002", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 ms"));
420         helper("8.002", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 ms"));
421         helper("8.002", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
422         helper("8.003", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-327680 ms"));
423         helper("8.003", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("327670 ms"));
424         helper("8.003", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
425         helper("8.004", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-3276800 ms"));
426         helper("8.004", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("3276700 ms"));
427         helper("8.004", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
428         helper("8.005", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 s"));
429         helper("8.005", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 s"));
430         helper("8.005", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 s"));
431         helper("8.006", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 min"));
432         helper("8.006", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 min"));
433         helper("8.006", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 min"));
434         helper("8.007", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 h"));
435         helper("8.007", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 h"));
436         helper("8.007", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 h"));
437
438         helper("8.010", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-327.68 %"));
439         helper("8.011", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 °"));
440         helper("8.011", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 °"));
441         helper("8.011", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 °"));
442         helper("8.012", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 m"));
443         helper("8.012", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 m"));
444         helper("8.012", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 m"));
445     }
446
447     @Test
448     void testDpt9() {
449         // special float with sign, 4-bit exponent, and mantissa in two's complement notation
450         // ref: KNX spec, 03_07_02-Datapoint-Types
451         // FIXME according to spec, value 0x7fff shall be regarded as "invalid data"
452         // TODO add tests for clipping at lower boundary (e.g. absolute zero)
453         helper("9.001", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<>("1 °C"));
454         helper("9.001", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 °C"));
455         helper("9.001", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 °C"));
456         helper("9.001", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 °C"));
457         // lower values than absolute zero will be set to abs. zero (-273 °C)
458         // helper("9.001", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-273 °C"));
459         helper("9.002", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<>("1 K"));
460         helper("9.002", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 K"));
461         helper("9.002", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 K"));
462         helper("9.002", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 K"));
463         helper("9.002", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 K"));
464         helper("9.003", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 K/h"));
465         helper("9.003", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 K/h"));
466         helper("9.003", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 K/h"));
467         helper("9.003", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 K/h"));
468         helper("9.004", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 lx"));
469         helper("9.004", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 lx"));
470         helper("9.004", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 lx"));
471         // no negative values allowed for DPTs 9.004-9.008
472         helper("9.005", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 m/s"));
473         helper("9.005", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 m/s"));
474         helper("9.005", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 m/s"));
475         helper("9.005", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 m/s"));
476         helper("9.005", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 m/s"));
477         helper("9.005", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 m/s"));
478         helper("9.006", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 Pa"));
479         helper("9.006", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 Pa"));
480         helper("9.006", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 Pa"));
481         helper("9.007", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 %"));
482         helper("9.007", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 %"));
483         helper("9.007", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 %"));
484         helper("9.008", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 ppm"));
485         helper("9.008", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 ppm"));
486         helper("9.008", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ppm"));
487         helper("9.009", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 m³/h"));
488         helper("9.009", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 m³/h"));
489         helper("9.009", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 m³/h"));
490         helper("9.009", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 m³/h"));
491         helper("9.010", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 s"));
492         helper("9.010", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 s"));
493         helper("9.010", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 s"));
494         helper("9.010", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 s"));
495         helper("9.011", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 ms"));
496         helper("9.011", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 ms"));
497         helper("9.011", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 ms"));
498         helper("9.011", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 ms"));
499
500         helper("9.020", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 mV"));
501         helper("9.020", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 mV"));
502         helper("9.020", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 mV"));
503         helper("9.020", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 mV"));
504         helper("9.021", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 mA"));
505         helper("9.021", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 mA"));
506         helper("9.021", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 mA"));
507         helper("9.021", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 mA"));
508         helper("9.022", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 W/m²"));
509         helper("9.022", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 W/m²"));
510         helper("9.022", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 W/m²"));
511         helper("9.022", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 W/m²"));
512         helper("9.023", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 K/%"));
513         helper("9.023", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 K/%"));
514         helper("9.023", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 K/%"));
515         helper("9.023", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 K/%"));
516         helper("9.024", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 kW"));
517         helper("9.024", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 kW"));
518         helper("9.024", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 kW"));
519         helper("9.024", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 kW"));
520         helper("9.025", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 l/h"));
521         helper("9.025", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 l/h"));
522         helper("9.025", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 l/h"));
523         helper("9.025", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 l/h"));
524         helper("9.026", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 l/m²"));
525         helper("9.026", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 l/m²"));
526         helper("9.026", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 l/m²"));
527         helper("9.026", new byte[] { (byte) 0xf8, (byte) 0x00 }, new QuantityType<>("-671088.64 l/m²"));
528         helper("9.027", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 °F"));
529         helper("9.027", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 °F"));
530         helper("9.027", new byte[] { (byte) 0x87, (byte) 0x9c }, new QuantityType<>("-1 °F"));
531         // lower values than absolute zero will be set to abs. zero (-459.6 °F)
532         helper("9.028", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 km/h"));
533         helper("9.028", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 km/h"));
534         helper("9.028", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 km/h"));
535         // no negative values allowed for DPTs 9.028-9.030
536         helper("9.029", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 g/m³"));
537         helper("9.029", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 g/m³"));
538         helper("9.029", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 g/m³"));
539         helper("9.030", new byte[] { (byte) 0x07, (byte) 0xff }, new QuantityType<>("20.47 µg/m³"));
540         helper("9.030", new byte[] { (byte) 0x7f, (byte) 0xfe }, new QuantityType<>("670433.28 µg/m³"));
541         helper("9.030", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 µg/m³"));
542     }
543
544     @Test
545     void testDpt10() {
546         // TODO check handling of DPT10: date is not set to current date, but 1970-01-01 + offset if day is given
547         // maybe we should change the semantics and use current date + offset if day is given
548
549         // note: local timezone is set when creating DateTimeType, for example "1970-01-01Thh:mm:ss.000+0100"
550
551         // no-day
552         assertTrue(Objects
553                 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, 0 }, DecimalType.class))
554                 .startsWith("1970-01-01T17:30:00.000+"));
555         // Thursday, this is correct for 1970-01-01
556         assertTrue(Objects
557                 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, 0 }, DecimalType.class))
558                 .startsWith("1970-01-01T17:30:00.000+"));
559         // Monday -> 1970-01-05
560         assertTrue(Objects
561                 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x31, (byte) 0x1e, 0 }, DecimalType.class))
562                 .startsWith("1970-01-05T17:30:00.000+"));
563
564         // Thursday, otherwise first byte of encoded data will not match
565         helper("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"));
566         helper("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"), new byte[0],
567                 new byte[] { (byte) 0x1f, (byte) 0xff, (byte) 0xff });
568     }
569
570     @Test
571     void testDpt11() {
572         // note: local timezone and dst is set when creating DateTimeType, for example "2019-06-12T00:00:00.000+0200"
573         helper("11.001", new byte[] { (byte) 12, 6, 19 }, new DateTimeType("2019-06-12"));
574     }
575
576     @Test
577     void testDpt12() {
578         helper("12.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
579                 new DecimalType("4294967294"));
580         helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 s"));
581         helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 min"));
582         helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 min"));
583         helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 h"));
584         helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 h"));
585         helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("60 min"));
586
587         helper("12.1200", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 l"));
588         helper("12.1200", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
589                 new QuantityType<>("4294967294 l"));
590         helper("12.1201", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 m³"));
591         helper("12.1201", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
592                 new QuantityType<>("4294967294 m³"));
593     }
594
595     @Test
596     void testDpt13() {
597         helper("13.001", new byte[] { 0, 0, 0, 0 }, new DecimalType(0));
598         helper("13.001", new byte[] { 0, 0, 0, 42 }, new DecimalType(42));
599         helper("13.001", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
600                 new DecimalType(2147483647));
601         // KNX representation typically uses two's complement
602         helper("13.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }, new DecimalType(-1));
603         helper("13.001", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 }, new DecimalType(-2147483648));
604         helper("13.002", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 m³/h"));
605         helper("13.002", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
606                 new QuantityType<>("-2147483648 m³/h"));
607         helper("13.002", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
608                 new QuantityType<>("2147483647 m³/h"));
609
610         helper("13.010", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 Wh"));
611         helper("13.010", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
612                 new QuantityType<>("-2147483648 Wh"));
613         helper("13.010", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
614                 new QuantityType<>("2147483647 Wh"));
615         helper("13.011", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 VAh"));
616         helper("13.011", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
617                 new QuantityType<>("-2147483648 VAh"));
618         helper("13.011", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
619                 new QuantityType<>("2147483647 VAh"));
620         helper("13.012", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 varh"));
621         helper("13.012", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
622                 new QuantityType<>("-2147483648 varh"));
623         helper("13.012", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
624                 new QuantityType<>("2147483647 varh"));
625         helper("13.013", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 kWh"));
626         helper("13.013", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
627                 new QuantityType<>("-2147483648 kWh"));
628         helper("13.013", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
629                 new QuantityType<>("2147483647 kWh"));
630         helper("13.014", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 VAh"));
631         helper("13.014", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
632                 new QuantityType<>("-2147483648000 VAh"));
633         helper("13.014", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
634                 new QuantityType<>("2147483647000 VAh"));
635         helper("13.015", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 kvarh"));
636         helper("13.015", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
637                 new QuantityType<>("-2147483648 kvarh"));
638         helper("13.015", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
639                 new QuantityType<>("2147483647 kvarh"));
640         helper("13.016", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 MWh"));
641         helper("13.016", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
642                 new QuantityType<>("-2147483648 MWh"));
643         helper("13.016", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
644                 new QuantityType<>("2147483647 MWh"));
645
646         helper("13.100", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 s"));
647         helper("13.100", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
648                 new QuantityType<>("-2147483648 s"));
649         helper("13.100", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
650                 new QuantityType<>("2147483647 s"));
651
652         helper("13.1200", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 l"));
653         helper("13.1200", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
654                 new QuantityType<>("-2147483648 l"));
655         helper("13.1200", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
656                 new QuantityType<>("2147483647 l"));
657         helper("13.1201", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 m³"));
658         helper("13.1201", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
659                 new QuantityType<>("-2147483648 m³"));
660         helper("13.1201", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
661                 new QuantityType<>("2147483647 m³"));
662     }
663
664     @Test
665     void testDpt14() {
666         helper("14.000", new byte[] { (byte) 0x3f, (byte) 0x80, 0, 0 }, new QuantityType<>("1 m/s²"));
667         helper("14.000", F32_MINUS_ONE, new QuantityType<>("-1 m/s²"));
668         helper("14.001", F32_MINUS_ONE, new QuantityType<>("-1 rad/s²"));
669         helper("14.002", F32_MINUS_ONE, new QuantityType<>("-1 J/mol"));
670         helper("14.003", F32_MINUS_ONE, new QuantityType<>("-1 /s"));
671         helper("14.004", F32_MINUS_ONE, new QuantityType<>("-1 mol"));
672         helper("14.005", F32_MINUS_ONE, new DecimalType("-1"));
673         helper("14.006", F32_MINUS_ONE, new QuantityType<>("-1 rad"));
674         helper("14.007", F32_MINUS_ONE, new QuantityType<>("-1 °"));
675         helper("14.008", F32_MINUS_ONE, new QuantityType<>("-1 J*s"));
676         helper("14.009", F32_MINUS_ONE, new QuantityType<>("-1 rad/s"));
677         helper("14.010", F32_MINUS_ONE, new QuantityType<>("-1 m²"));
678         helper("14.011", F32_MINUS_ONE, new QuantityType<>("-1 F"));
679         helper("14.012", F32_MINUS_ONE, new QuantityType<>("-1 C/m²"));
680         helper("14.013", F32_MINUS_ONE, new QuantityType<>("-1 C/m³"));
681         helper("14.014", F32_MINUS_ONE, new QuantityType<>("-1 m²/N"));
682         helper("14.015", F32_MINUS_ONE, new QuantityType<>("-1 S"));
683         helper("14.016", F32_MINUS_ONE, new QuantityType<>("-1 S/m"));
684         helper("14.017", F32_MINUS_ONE, new QuantityType<>("-1 kg/m³"));
685         helper("14.018", F32_MINUS_ONE, new QuantityType<>("-1 C"));
686         helper("14.019", F32_MINUS_ONE, new QuantityType<>("-1 A"));
687         helper("14.020", F32_MINUS_ONE, new QuantityType<>("-1 A/m²"));
688         helper("14.021", F32_MINUS_ONE, new QuantityType<>("-1 C*m"));
689         helper("14.022", F32_MINUS_ONE, new QuantityType<>("-1 C/m²"));
690         helper("14.023", F32_MINUS_ONE, new QuantityType<>("-1 V/m"));
691         helper("14.024", F32_MINUS_ONE, new QuantityType<>("-1 V*m")); // SI unit is Vm
692         helper("14.025", F32_MINUS_ONE, new QuantityType<>("-1 C/m²"));
693         helper("14.026", F32_MINUS_ONE, new QuantityType<>("-1 C/m²"));
694         helper("14.027", F32_MINUS_ONE, new QuantityType<>("-1 V"));
695         helper("14.028", F32_MINUS_ONE, new QuantityType<>("-1 V"));
696         helper("14.029", F32_MINUS_ONE, new QuantityType<>("-1 A*m²"));
697         helper("14.030", F32_MINUS_ONE, new QuantityType<>("-1 V"));
698         helper("14.031", F32_MINUS_ONE, new QuantityType<>("-1 J"));
699         helper("14.032", F32_MINUS_ONE, new QuantityType<>("-1 N"));
700         helper("14.033", F32_MINUS_ONE, new QuantityType<>("-1 /s"));
701         helper("14.034", F32_MINUS_ONE, new QuantityType<>("-1 rad/s"));
702         helper("14.035", F32_MINUS_ONE, new QuantityType<>("-1 J/K"));
703         helper("14.036", F32_MINUS_ONE, new QuantityType<>("-1 W"));
704         helper("14.037", F32_MINUS_ONE, new QuantityType<>("-1 J"));
705         helper("14.038", F32_MINUS_ONE, new QuantityType<>("-1 Ohm"));
706         helper("14.039", F32_MINUS_ONE, new QuantityType<>("-1 m"));
707         helper("14.040", F32_MINUS_ONE, new QuantityType<>("-1 J"));
708         helper("14.041", F32_MINUS_ONE, new QuantityType<>("-1 cd/m²"));
709         helper("14.042", F32_MINUS_ONE, new QuantityType<>("-1 lm"));
710         helper("14.043", F32_MINUS_ONE, new QuantityType<>("-1 cd"));
711         helper("14.044", F32_MINUS_ONE, new QuantityType<>("-1 A/m"));
712         helper("14.045", F32_MINUS_ONE, new QuantityType<>("-1 Wb"));
713         helper("14.046", F32_MINUS_ONE, new QuantityType<>("-1 T"));
714         helper("14.047", F32_MINUS_ONE, new QuantityType<>("-1 A*m²"));
715         helper("14.048", F32_MINUS_ONE, new QuantityType<>("-1 T"));
716         helper("14.049", F32_MINUS_ONE, new QuantityType<>("-1 A/m"));
717         helper("14.050", F32_MINUS_ONE, new QuantityType<>("-1 A"));
718         helper("14.051", F32_MINUS_ONE, new QuantityType<>("-1 kg"));
719         helper("14.052", F32_MINUS_ONE, new QuantityType<>("-1 kg/s"));
720         helper("14.053", F32_MINUS_ONE, new QuantityType<>("-1 N/s"));
721         helper("14.054", F32_MINUS_ONE, new QuantityType<>("-1 rad"));
722         helper("14.055", F32_MINUS_ONE, new QuantityType<>("-1 °"));
723         helper("14.056", F32_MINUS_ONE, new QuantityType<>("-1 W"));
724         helper("14.057", F32_MINUS_ONE, new DecimalType("-1"));
725         helper("14.058", F32_MINUS_ONE, new QuantityType<>("-1 Pa"));
726         helper("14.059", F32_MINUS_ONE, new QuantityType<>("-1 Ohm"));
727         helper("14.060", F32_MINUS_ONE, new QuantityType<>("-1 Ohm"));
728         helper("14.061", F32_MINUS_ONE, new QuantityType<>("-1 Ohm*m"));
729         helper("14.062", F32_MINUS_ONE, new QuantityType<>("-1 H"));
730         helper("14.063", F32_MINUS_ONE, new QuantityType<>("-1 sr"));
731         helper("14.064", F32_MINUS_ONE, new QuantityType<>("-1 W/m²"));
732         helper("14.065", F32_MINUS_ONE, new QuantityType<>("-1 m/s"));
733         helper("14.066", F32_MINUS_ONE, new QuantityType<>("-1 Pa"));
734         helper("14.067", F32_MINUS_ONE, new QuantityType<>("-1 N/m"));
735         helper("14.068", new byte[] { (byte) 0x3f, (byte) 0x80, 0, 0 }, new QuantityType<>("1 °C"));
736         helper("14.068", F32_MINUS_ONE, new QuantityType<>("-1 °C"));
737         helper("14.069", F32_MINUS_ONE, new QuantityType<>("-1 K"));
738         helper("14.070", F32_MINUS_ONE, new QuantityType<>("-1 K"));
739         helper("14.071", F32_MINUS_ONE, new QuantityType<>("-1 J/K"));
740         helper("14.072", F32_MINUS_ONE, new QuantityType<>("-1 W/m/K"));
741         helper("14.073", F32_MINUS_ONE, new QuantityType<>("-1 V/K"));
742         helper("14.074", F32_MINUS_ONE, new QuantityType<>("-1 s"));
743         helper("14.075", F32_MINUS_ONE, new QuantityType<>("-1 N*m"));
744         helper("14.076", F32_MINUS_ONE, new QuantityType<>("-1 m³"));
745         helper("14.077", F32_MINUS_ONE, new QuantityType<>("-1 m³/s"));
746         helper("14.078", F32_MINUS_ONE, new QuantityType<>("-1 N"));
747         helper("14.079", F32_MINUS_ONE, new QuantityType<>("-1 J"));
748         helper("14.080", F32_MINUS_ONE, new QuantityType<>("-1 VA"));
749
750         helper("14.1200", F32_MINUS_ONE, new QuantityType<>("-1 m³/h"));
751         helper("14.1201", F32_MINUS_ONE, new QuantityType<>("-1 l/s"));
752     }
753
754     @Test
755     void testDpt16() {
756         helper("16.000", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
757                 0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
758         helper("16.001", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
759                 0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
760     }
761
762     @Test
763     void testDpt17() {
764         helper("17.001", new byte[] { 0 }, new DecimalType(0));
765         helper("17.001", new byte[] { 42 }, new DecimalType(42));
766         helper("17.001", new byte[] { 63 }, new DecimalType(63));
767     }
768
769     @Test
770     void testDpt18() {
771         // scene, activate 0..63
772         helper("18.001", new byte[] { 0 }, new DecimalType(0));
773         helper("18.001", new byte[] { 42 }, new DecimalType(42));
774         helper("18.001", new byte[] { 63 }, new DecimalType(63));
775         // scene, learn += 0x80
776         helper("18.001", new byte[] { (byte) (0x80 + 0) }, new DecimalType(0x80));
777         helper("18.001", new byte[] { (byte) (0x80 + 42) }, new DecimalType(0x80 + 42));
778         helper("18.001", new byte[] { (byte) (0x80 + 63) }, new DecimalType(0x80 + 63));
779     }
780
781     @Test
782     void testDpt19() {
783         // 2019-01-15 17:30:00
784         helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
785                 new DateTimeType("2019-01-15T17:30:00"));
786         helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
787                 new DateTimeType("2019-01-15T17:30:00"));
788         // 2019-07-15 17:30:00
789         helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
790                 new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
791         helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
792                 new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
793         // TODO add tests for incompletly filled frames (e.g. containing only date or time)
794     }
795
796     @Test
797     void testDpt20() {
798         // test default String representation of enum (incomplete)
799         helper("20.001", new byte[] { 0 }, new StringType("autonomous"));
800         helper("20.001", new byte[] { 1 }, new StringType("slave"));
801         helper("20.001", new byte[] { 2 }, new StringType("master"));
802
803         helper("20.002", new byte[] { 0 }, new StringType("building in use"));
804         helper("20.002", new byte[] { 1 }, new StringType("building not used"));
805         helper("20.002", new byte[] { 2 }, new StringType("building protection"));
806
807         // test DecimalType representation of enum
808         int[] subTypes = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 20, 21, 22, 100, 101, 102, 103, 104,
809                 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 120, 121, 122, 600, 601, 602, 603, 604, 605, 606,
810                 607, 608, 609, 610, 611, 612, 613, 801, 802, 803, 804, 1000, 1001, 1002, 1003, 1004, 1005, 1200, 1202,
811                 1203, 1204, 1205, 1206, 1207, 1208, 1209 };
812         for (int subType : subTypes) {
813             helper("20." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
814         }
815         // once these DPTs are available in Calimero, add to check above
816         int[] unsupportedSubTypes = new int[] {};
817         for (int subType : unsupportedSubTypes) {
818             assertNull(ValueDecoder.decode("20." + String.format("%03d", subType), new byte[] { 0 }, StringType.class));
819         }
820     }
821
822     @Test
823     void testDpt21() {
824         // test default String representation of bitfield (incomplete)
825         helper("21.001", new byte[] { 5 }, new StringType("overridden, out of service"));
826
827         // test DecimalType representation of bitfield
828         int[] subTypes = new int[] { 1, 2, 100, 101, 102, 103, 104, 105, 106, 601, 1000, 1001, 1002, 1010, 1200, 1201 };
829         for (int subType : subTypes) {
830             helper("21." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
831         }
832     }
833
834     @Test
835     void testDpt22() {
836         // test default String representation of bitfield (incomplete)
837         helper("22.101", new byte[] { 1, 0 }, new StringType("heating mode"));
838         helper("22.101", new byte[] { 1, 2 }, new StringType("heating mode, heating eco mode"));
839
840         // test DecimalType representation of bitfield
841         helper("22.100", new byte[] { 0, 2 }, new DecimalType(2));
842         helper("22.101", new byte[] { 0, 2 }, new DecimalType(2));
843         helper("22.1000", new byte[] { 0, 2 }, new DecimalType(2));
844         helper("22.1010", new byte[] { 0, 2 }, new DecimalType(2));
845     }
846
847     @Test
848     void testDpt28() {
849         // null terminated strings, UTF8
850         helper("28.001", new byte[] { 0x31, 0x32, 0x33, 0x34, 0x0 }, new StringType("1234"));
851         helper("28.001", new byte[] { (byte) 0xce, (byte) 0xb5, 0x34, 0x0 }, new StringType("\u03b54"));
852     }
853
854     @Test
855     void testDpt29() {
856         helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
857         helper("29.010", new byte[] { (byte) 0x80, 0, 0, 0, 0, 0, 0, 0 },
858                 new QuantityType<>("-9223372036854775808 Wh"));
859         helper("29.010", new byte[] { (byte) 0xff, 0, 0, 0, 0, 0, 0, 0 }, new QuantityType<>("-72057594037927936 Wh"));
860         helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
861         helper("29.011", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 VAh"));
862         helper("29.012", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 varh"));
863     }
864
865     @Test
866     void testDpt229() {
867         // special DPT for metering, allows several units and different scaling
868         // -> Calimero uses scaling, but always encodes as dimensionless value
869         final int dimensionlessCounter = 0b10111010;
870         helper("229.001", new byte[] { 0, 0, 0, 0, (byte) dimensionlessCounter, 0 }, new DecimalType(0));
871     }
872
873     @Test
874     void testColorDpts() {
875         // HSB
876         helper("232.600", new byte[] { 123, 45, 67 }, ColorUtil.rgbToHsb(new int[] { 123, 45, 67 }));
877         // RGB, MDT specific
878         helper("232.60000", new byte[] { 123, 45, 67 }, new HSBType("173.6, 17.6, 26.3"));
879
880         // xyY
881         int x = (int) (14.65 * 65535.0 / 100.0);
882         int y = (int) (11.56 * 65535.0 / 100.0);
883         // encoding is always xy and brightness (C+B, 0x03), do not test other combinations
884         helper("242.600", new byte[] { (byte) ((x >> 8) & 0xff), (byte) (x & 0xff), (byte) ((y >> 8) & 0xff),
885                 (byte) (y & 0xff), (byte) 0x28, 0x3 }, new HSBType("220,90,50"), new byte[] { 0, 8, 0, 8, 0, 0 },
886                 new byte[0]);
887         // TODO check brightness
888
889         // RGBW, only RGB part
890         helper("251.600", new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e }, new HSBType("207, 23, 19"),
891                 new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
892         // RGBW, only RGB part
893         helper("251.600", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00, 0x0e },
894                 new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
895         // RGBW, only W part
896         helper("251.600", new byte[] { 0x0, 0x0, 0x0, 0x1A, 0x00, 0x01 }, new PercentType("10.2"));
897         // RGBW, all
898         helper("251.60600", new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xff, 0x00, 0x0f },
899                 new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 2, 0, 0 }, new byte[0]);
900         // RGBW, mixed
901         int[] rgbw = new int[] { 240, 0x0, 0x0, 0x0f };
902         HSBType hsb = ColorUtil.rgbToHsb(rgbw);
903         helper("251.60600", new byte[] { (byte) rgbw[0], (byte) rgbw[1], (byte) rgbw[2], (byte) rgbw[3], 0x00, 0x0f },
904                 hsb, new byte[] { 2, 2, 2, 2, 0, 0 }, new byte[0]);
905     }
906
907     @Test
908     void testColorTransitionDpts() {
909         // DPT 243.600 DPT_Colour_Transition_xyY
910         // time(2) y(2) x(2), %brightness(1), flags(1)
911         helper("243.600", new byte[] { 0, 5, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
912                 new StringType("(0.9922, 0.4961) 16.5 % 500 ms"));
913         helper("243.600", new byte[] { (byte) 0x02, (byte) 0x00, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
914                 new StringType("(0.9922, 0.4961) 16.5 % 51200 ms"));
915         helper("243.600", new byte[] { (byte) 0x40, (byte) 0x00, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
916                 new StringType("(0.9922, 0.4961) 16.5 % 1638400 ms"));
917         helper("243.600", new byte[] { (byte) 0xff, (byte) 0xff, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
918                 new StringType("(0.9922, 0.4961) 16.5 % 6553500 ms"));
919         // DPT 249.600 DPT_Brightness_Colour_Temperature_Transition
920         // time(2) colortemp(2), brightness(1), flags(1)
921         helper("249.600", new byte[] { 0, 5, 0, 40, 127, 7 }, new StringType("49.8 % 40 K 0.5 s"));
922         helper("249.600", new byte[] { (byte) 0xff, (byte) 0xfa, 0, 40, 127, 7 }, new StringType("49.8 % 40 K 6553 s"));
923         helper("249.600", new byte[] { (byte) 0xff, (byte) 0xff, 0, 40, 127, 7 },
924                 new StringType("49.8 % 40 K 6553.5 s"));
925         // DPT 250.600 DPT_Brightness_Colour_Temperature_Control
926         // cct(1) cb(1) flags(1)
927         helper("250.600", new byte[] { 0x0f, 0x0e, 3 }, new StringType("CT increase 7 steps BRT increase 6 steps"));
928         // DPT 252.600 DPT_Relative_Control_RGBW
929         // r(1) g(1) b(1) w(1) flags(1)
930         helper("252.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x0c, 0x0f },
931                 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps W increase 4 steps"));
932         // DPT 253.600 DPT_Relative_Control_xyY
933         // cs(1) ct(1) cb(1) flags(1)
934         helper("253.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x7 },
935                 new StringType("x increase 7 steps y increase 6 steps Y increase 5 steps"));
936         // DPT 254.600 DPT_Relative_Control_RGB
937         // cr(1) cg(1) cb(1)
938         helper("254.600", new byte[] { 0x0f, 0x0e, 0x0d },
939                 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps"));
940     }
941
942     @Test
943     @AfterAll
944     static void checkForMissingMainTypes() {
945         // checks if we have itests for all main DPT types supported by Calimero library,
946         // data is collected within method helper()
947         var wrapper = new Object() {
948             boolean testsMissing = false;
949         };
950         TranslatorTypes.getAllMainTypes().forEach((i, t) -> {
951             if (!dptTested.contains(i)) {
952                 LOGGER.warn("missing tests for main DPT type " + i);
953                 wrapper.testsMissing = true;
954             }
955         });
956         assertEquals(false, wrapper.testsMissing, "add tests for new DPT main types");
957     }
958 }