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