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