2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.knx.internal.itests;
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;
20 import java.util.Arrays;
21 import java.util.HashSet;
22 import java.util.Objects;
25 import javax.measure.quantity.Temperature;
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;
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;
63 * Integration test to check conversion from raw KNX frame data to OH data types and back.
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.
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.
75 * @see DummyKNXNetworkLink
77 * @author Holger Friedrich - Initial contribution
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;
87 * helper method for integration tests
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
96 void helper(String dpt, byte[] rawData, Type ohReferenceData, byte[] maxDistance, byte[] bitmask) {
98 DummyKNXNetworkLink link = new DummyKNXNetworkLink();
99 ProcessCommunicator pc = new ProcessCommunicatorImpl(link);
100 DummyProcessListener processListener = new DummyProcessListener();
101 pc.addProcessListener(processListener);
103 GroupAddress groupAddress = new GroupAddress(2, 4, 6);
104 Datapoint datapoint = new CommandDP(groupAddress, "dummy GA", 0,
105 DPTUtil.NORMALIZED_DPT.getOrDefault(dpt, dpt));
107 // 0) check usage of helper()
108 assertEquals(true, rawData.length > 0);
109 if (maxDistance.length == 0) {
110 maxDistance = new byte[rawData.length];
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);
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())) {
126 "test for DPT {} uses type {} which is not contained in DPT_TYPE_MAP, sending may not be allowed",
127 dpt, ohReferenceData.getClass());
131 // 1) check if the decoder works (rawData to known good type ohReferenceData)
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.
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);
144 assertEquals(ohReferenceData, ohData, "comparing reference to decoded value: failed for DPT " + dpt
145 + ", check ValueEncoder.decode()");
148 // 2) check the encoding (ohData to raw data)
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);
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);
167 // 3) Check date provided by Calimero library as input via loopback, it should match the initial data
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);
181 } catch (KNXException e) {
182 LOGGER.warn("exception occurred", e.toString());
183 assertEquals("", e.toString());
187 void helper(String dpt, byte[] rawData, Type ohReferenceData) {
188 helper(dpt, rawData, ohReferenceData, new byte[0], new byte[0]);
193 // for now only the DPTs for general use, others omitted
194 // TODO add tests for more subtypes
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);
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);
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);
225 helper("1.024", new byte[] { 0 }, OnOffType.OFF);
226 helper("1.024", new byte[] { 1 }, OnOffType.ON);
231 for (int subType = 1; subType <= 12; subType++) {
232 helper("2." + String.format("%03d", subType), new byte[] { 3 }, new DecimalType(3));
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 });
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);
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));
268 helper("5.010", new byte[] { 42 }, new DecimalType(42));
269 helper("5.010", new byte[] { (byte) 0xff }, new DecimalType(255));
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));
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));
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));
297 // TODO add tests for more subtypes
298 helper("9.001", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<Temperature>("1 °C"));
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
306 // note: local timezone is set when creating DateTimeType, for example "1970-01-01Thh:mm:ss.000+0100"
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
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
318 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x31, (byte) 0x1e, 0 }, DecimalType.class))
319 .startsWith("1970-01-05T17:30:00.000+"));
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 });
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"));
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"));
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³"));
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));
366 // TODO add tests for more subtypes
367 helper("14.068", new byte[] { (byte) 0x3f, (byte) 0x80, 0, 0 }, new QuantityType<Temperature>("1 °C"));
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"));
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));
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));
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 });
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"));
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"));
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));
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));
438 // test default String representation of bitfield (incomplete)
439 helper("21.001", new byte[] { 5 }, new StringType("overridden, out of service"));
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));
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));
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"));
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));
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"));
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"));
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));
492 void testColorDpts() {
494 helper("232.600", new byte[] { 123, 45, 67 }, ColorUtil.rgbToHsb(new int[] { 123, 45, 67 }));
496 helper("232.60000", new byte[] { 123, 45, 67 }, new HSBType("173.6, 17.6, 26.3"));
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 },
505 // TODO check brightness
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]);
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
537 helper("254.600", new byte[] { 0x0f, 0x0e, 0x0d },
538 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps"));
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;
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;
555 assertEquals(false, wrapper.testsMissing, "add tests for new DPT main types");