2 * Copyright (c) 2010-2024 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 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);
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);
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);
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);
303 for (int subType = 1; subType <= 12; subType++) {
304 helper("2." + String.format("%03d", subType), new byte[] { 3 }, new DecimalType(3));
310 // DPT 3.007 and DPT 3.008 consist of a control bit (1 bit) and stepsize (3 bit)
311 // if stepsize is 0, OH will ignore the command
312 byte controlBit = 1 << 3;
313 // loop all other step sizes and check only the control bit
314 for (byte i = 1; i < 8; i++) {
315 helper("3.007", new byte[] { i }, IncreaseDecreaseType.DECREASE, new byte[0], new byte[] { controlBit });
316 helper("3.007", new byte[] { (byte) (i + controlBit) }, IncreaseDecreaseType.INCREASE, new byte[0],
317 new byte[] { controlBit });
318 helper("3.008", new byte[] { i }, UpDownType.UP, new byte[0], new byte[] { controlBit });
319 helper("3.008", new byte[] { (byte) (i + controlBit) }, UpDownType.DOWN, new byte[0],
320 new byte[] { controlBit });
323 // check if OH ignores incoming frames with mask 0 (mapped to UndefType)
324 Assertions.assertFalse(ValueDecoder.decode("3.007", new byte[] { 0 },
325 IncreaseDecreaseType.class) instanceof IncreaseDecreaseType);
326 Assertions.assertFalse(ValueDecoder.decode("3.007", new byte[] { controlBit },
327 IncreaseDecreaseType.class) instanceof IncreaseDecreaseType);
328 Assertions.assertFalse(ValueDecoder.decode("3.008", new byte[] { 0 }, UpDownType.class) instanceof UpDownType);
329 Assertions.assertFalse(
330 ValueDecoder.decode("3.008", new byte[] { controlBit }, UpDownType.class) instanceof UpDownType);
335 helper("5.001", new byte[] { 0 }, new QuantityType<>("0 %"));
336 helper("5.001", new byte[] { (byte) 0xff }, new QuantityType<>("100 %"));
337 // fallback: PercentType
338 helper("5.001", new byte[] { 0 }, new PercentType(0));
339 helper("5.001", new byte[] { (byte) 0x80 }, new PercentType(50));
340 helper("5.001", new byte[] { (byte) 0xff }, new PercentType(100));
342 helper("5.003", new byte[] { 0 }, new QuantityType<>("0 °"));
343 helper("5.003", new byte[] { (byte) 0xff }, new QuantityType<>("360 °"));
344 helper("5.004", new byte[] { 0 }, new QuantityType<>("0 %"));
345 helper("5.004", new byte[] { (byte) 0x64 }, new QuantityType<>("100 %"));
346 helper("5.004", new byte[] { (byte) 0xff }, new QuantityType<>("255 %"));
347 // PercentType cannot encode values >100%, not supported for 5.004
348 helper("5.005", new byte[] { 42 }, new DecimalType(42));
349 helper("5.005", new byte[] { (byte) 0xff }, new DecimalType(255));
350 helper("5.006", new byte[] { 0 }, new DecimalType(0));
351 helper("5.006", new byte[] { 42 }, new DecimalType(42));
352 helper("5.006", new byte[] { (byte) 0xfe }, new DecimalType(254));
354 helper("5.010", new byte[] { 42 }, new DecimalType(42));
355 helper("5.010", new byte[] { (byte) 0xff }, new DecimalType(255));
360 helper("6.001", new byte[] { 0 }, new QuantityType<>("0 %"));
361 helper("6.001", new byte[] { (byte) 0x7f }, new QuantityType<>("127 %"));
362 helper("6.001", new byte[] { (byte) 0xff }, new QuantityType<>("-1 %"));
363 // PercentType cannot encode values >100% or <0%, not supported for 6.001
365 helper("6.010", new byte[] { 0 }, new DecimalType(0));
366 helper("6.010", new byte[] { (byte) 0x7f }, new DecimalType(127));
367 helper("6.010", new byte[] { (byte) 0xff }, new DecimalType(-1));
372 // TODO add tests for more subtypes
373 helper("7.001", new byte[] { 0, 42 }, new DecimalType(42));
374 helper("7.001", new byte[] { (byte) 0xff, (byte) 0xff }, new DecimalType(65535));
379 helper("8.001", new byte[] { (byte) 0x7f, (byte) 0xff }, new DecimalType(32767));
380 helper("8.001", new byte[] { (byte) 0x80, (byte) 0x00 }, new DecimalType(-32768));
381 helper("8.002", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 ms"));
382 helper("8.002", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 ms"));
383 helper("8.002", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
384 helper("8.003", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-327680 ms"));
385 helper("8.003", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("327670 ms"));
386 helper("8.003", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
387 helper("8.004", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-3276800 ms"));
388 helper("8.004", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("3276700 ms"));
389 helper("8.004", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 ms"));
390 helper("8.005", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 s"));
391 helper("8.005", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 s"));
392 helper("8.005", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 s"));
393 helper("8.006", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 min"));
394 helper("8.006", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 min"));
395 helper("8.006", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 min"));
396 helper("8.007", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 h"));
397 helper("8.007", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 h"));
398 helper("8.007", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 h"));
400 helper("8.011", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 °"));
401 helper("8.011", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 °"));
402 helper("8.011", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 °"));
403 helper("8.012", new byte[] { (byte) 0x80, (byte) 0x00 }, new QuantityType<>("-32768 m"));
404 helper("8.012", new byte[] { (byte) 0x7f, (byte) 0xff }, new QuantityType<>("32767 m"));
405 helper("8.012", new byte[] { (byte) 0x00, (byte) 0x00 }, new QuantityType<>("0 m"));
410 // TODO add tests for more subtypes
411 helper("9.001", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<Temperature>("1 °C"));
416 // TODO check handling of DPT10: date is not set to current date, but 1970-01-01 + offset if day is given
417 // maybe we should change the semantics and use current date + offset if day is given
419 // note: local timezone is set when creating DateTimeType, for example "1970-01-01Thh:mm:ss.000+0100"
423 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, 0 }, DecimalType.class))
424 .startsWith("1970-01-01T17:30:00.000+"));
425 // Thursday, this is correct for 1970-01-01
427 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, 0 }, DecimalType.class))
428 .startsWith("1970-01-01T17:30:00.000+"));
429 // Monday -> 1970-01-05
431 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x31, (byte) 0x1e, 0 }, DecimalType.class))
432 .startsWith("1970-01-05T17:30:00.000+"));
434 // Thursday, otherwise first byte of encoded data will not match
435 helper("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"));
436 helper("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"), new byte[0],
437 new byte[] { (byte) 0x1f, (byte) 0xff, (byte) 0xff });
442 // note: local timezone and dst is set when creating DateTimeType, for example "2019-06-12T00:00:00.000+0200"
443 helper("11.001", new byte[] { (byte) 12, 6, 19 }, new DateTimeType("2019-06-12"));
448 helper("12.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
449 new DecimalType("4294967294"));
450 helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 s"));
451 helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 min"));
452 helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 min"));
453 helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 h"));
454 helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 h"));
455 helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("60 min"));
457 helper("12.1200", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 l"));
458 helper("12.1200", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
459 new QuantityType<>("4294967294 l"));
460 helper("12.1201", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 m³"));
461 helper("12.1201", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
462 new QuantityType<>("4294967294 m³"));
467 helper("13.001", new byte[] { 0, 0, 0, 0 }, new DecimalType(0));
468 helper("13.001", new byte[] { 0, 0, 0, 42 }, new DecimalType(42));
469 helper("13.001", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
470 new DecimalType(2147483647));
471 // KNX representation typically uses two's complement
472 helper("13.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }, new DecimalType(-1));
473 helper("13.001", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 }, new DecimalType(-2147483648));
474 helper("13.002", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 m³/h"));
475 helper("13.002", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
476 new QuantityType<>("-2147483648 m³/h"));
477 helper("13.002", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
478 new QuantityType<>("2147483647 m³/h"));
480 helper("13.010", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 Wh"));
481 helper("13.010", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
482 new QuantityType<>("-2147483648 Wh"));
483 helper("13.010", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
484 new QuantityType<>("2147483647 Wh"));
485 helper("13.011", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 VAh"));
486 helper("13.011", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
487 new QuantityType<>("-2147483648 VAh"));
488 helper("13.011", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
489 new QuantityType<>("2147483647 VAh"));
490 helper("13.012", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 varh"));
491 helper("13.012", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
492 new QuantityType<>("-2147483648 varh"));
493 helper("13.012", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
494 new QuantityType<>("2147483647 varh"));
495 helper("13.013", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 kWh"));
496 helper("13.013", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
497 new QuantityType<>("-2147483648 kWh"));
498 helper("13.013", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
499 new QuantityType<>("2147483647 kWh"));
500 helper("13.014", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 VAh"));
501 helper("13.014", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
502 new QuantityType<>("-2147483648000 VAh"));
503 helper("13.014", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
504 new QuantityType<>("2147483647000 VAh"));
505 helper("13.015", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 kvarh"));
506 helper("13.015", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
507 new QuantityType<>("-2147483648 kvarh"));
508 helper("13.015", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
509 new QuantityType<>("2147483647 kvarh"));
510 helper("13.016", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 MWh"));
511 helper("13.016", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
512 new QuantityType<>("-2147483648 MWh"));
513 helper("13.016", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
514 new QuantityType<>("2147483647 MWh"));
516 helper("13.100", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 s"));
517 helper("13.100", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
518 new QuantityType<>("-2147483648 s"));
519 helper("13.100", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
520 new QuantityType<>("2147483647 s"));
522 helper("13.1200", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 l"));
523 helper("13.1200", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
524 new QuantityType<>("-2147483648 l"));
525 helper("13.1200", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
526 new QuantityType<>("2147483647 l"));
527 helper("13.1201", new byte[] { 0, 0, 0, 0 }, new QuantityType<>("0 m³"));
528 helper("13.1201", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 },
529 new QuantityType<>("-2147483648 m³"));
530 helper("13.1201", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
531 new QuantityType<>("2147483647 m³"));
536 // TODO add tests for more subtypes
537 helper("14.068", new byte[] { (byte) 0x3f, (byte) 0x80, 0, 0 }, new QuantityType<Temperature>("1 °C"));
542 helper("16.000", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
543 0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
544 helper("16.001", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
545 0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
550 helper("17.001", new byte[] { 0 }, new DecimalType(0));
551 helper("17.001", new byte[] { 42 }, new DecimalType(42));
552 helper("17.001", new byte[] { 63 }, new DecimalType(63));
557 // scene, activate 0..63
558 helper("18.001", new byte[] { 0 }, new DecimalType(0));
559 helper("18.001", new byte[] { 42 }, new DecimalType(42));
560 helper("18.001", new byte[] { 63 }, new DecimalType(63));
561 // scene, learn += 0x80
562 helper("18.001", new byte[] { (byte) (0x80 + 0) }, new DecimalType(0x80));
563 helper("18.001", new byte[] { (byte) (0x80 + 42) }, new DecimalType(0x80 + 42));
564 helper("18.001", new byte[] { (byte) (0x80 + 63) }, new DecimalType(0x80 + 63));
569 // 2019-01-15 17:30:00
570 helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
571 new DateTimeType("2019-01-15T17:30:00"));
572 helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
573 new DateTimeType("2019-01-15T17:30:00"));
574 // 2019-07-15 17:30:00
575 helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
576 new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
577 helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
578 new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
583 // test default String representation of enum (incomplete)
584 helper("20.001", new byte[] { 0 }, new StringType("autonomous"));
585 helper("20.001", new byte[] { 1 }, new StringType("slave"));
586 helper("20.001", new byte[] { 2 }, new StringType("master"));
588 helper("20.002", new byte[] { 0 }, new StringType("building in use"));
589 helper("20.002", new byte[] { 1 }, new StringType("building not used"));
590 helper("20.002", new byte[] { 2 }, new StringType("building protection"));
592 // test DecimalType representation of enum
593 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,
594 106, 107, 108, 109, 110, 111, 112, 113, 114, 120, 121, 122, 600, 601, 602, 603, 604, 605, 606, 607, 608,
595 609, 610, 801, 802, 803, 804, 1000, 1001, 1002, 1003, 1004, 1005, 1200, 1202 };
596 for (int subType : subTypes) {
597 helper("20." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
599 // once these DPTs are available in Calimero, add to check above
600 int[] unsupportedSubTypes = new int[] { 22, 115, 611, 612, 613, 1203, 1204, 1205, 1206, 1207, 1208, 1209 };
601 for (int subType : unsupportedSubTypes) {
602 assertNull(ValueDecoder.decode("20." + String.format("%03d", subType), new byte[] { 0 }, StringType.class));
608 // test default String representation of bitfield (incomplete)
609 helper("21.001", new byte[] { 5 }, new StringType("overridden, out of service"));
611 // test DecimalType representation of bitfield
612 int[] subTypes = new int[] { 1, 2, 100, 101, 102, 103, 104, 105, 106, 601, 1000, 1001, 1002, 1010 };
613 for (int subType : subTypes) {
614 helper("21." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
616 // once these DPTs are available in Calimero, add to check above
617 assertNull(ValueDecoder.decode("21.1200", new byte[] { 0 }, StringType.class));
618 assertNull(ValueDecoder.decode("21.1201", new byte[] { 0 }, StringType.class));
623 // test default String representation of bitfield (incomplete)
624 helper("22.101", new byte[] { 1, 0 }, new StringType("heating mode"));
625 helper("22.101", new byte[] { 1, 2 }, new StringType("heating mode, heating eco mode"));
627 // test DecimalType representation of bitfield
628 helper("22.101", new byte[] { 0, 2 }, new DecimalType(2));
629 helper("22.1000", new byte[] { 0, 2 }, new DecimalType(2));
630 // once these DPTs are available in Calimero, add to check above
631 assertNull(ValueDecoder.decode("22.100", new byte[] { 0, 2 }, StringType.class));
632 assertNull(ValueDecoder.decode("22.1010", new byte[] { 0, 2 }, StringType.class));
637 // null terminated strings, UTF8
638 helper("28.001", new byte[] { 0x31, 0x32, 0x33, 0x34, 0x0 }, new StringType("1234"));
639 helper("28.001", new byte[] { (byte) 0xce, (byte) 0xb5, 0x34, 0x0 }, new StringType("\u03b54"));
644 helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
645 helper("29.010", new byte[] { (byte) 0x80, 0, 0, 0, 0, 0, 0, 0 },
646 new QuantityType<>("-9223372036854775808 Wh"));
647 helper("29.010", new byte[] { (byte) 0xff, 0, 0, 0, 0, 0, 0, 0 }, new QuantityType<>("-72057594037927936 Wh"));
648 helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
649 helper("29.011", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 VAh"));
650 helper("29.012", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 varh"));
655 // special DPT for metering, allows several units and different scaling
656 // -> Calimero uses scaling, but always encodes as dimensionless value
657 final int dimensionlessCounter = 0b10111010;
658 helper("229.001", new byte[] { 0, 0, 0, 0, (byte) dimensionlessCounter, 0 }, new DecimalType(0));
662 void testColorDpts() {
664 helper("232.600", new byte[] { 123, 45, 67 }, ColorUtil.rgbToHsb(new int[] { 123, 45, 67 }));
666 helper("232.60000", new byte[] { 123, 45, 67 }, new HSBType("173.6, 17.6, 26.3"));
669 int x = (int) (14.65 * 65535.0 / 100.0);
670 int y = (int) (11.56 * 65535.0 / 100.0);
671 // encoding is always xy and brightness (C+B, 0x03), do not test other combinations
672 helper("242.600", new byte[] { (byte) ((x >> 8) & 0xff), (byte) (x & 0xff), (byte) ((y >> 8) & 0xff),
673 (byte) (y & 0xff), (byte) 0x28, 0x3 }, new HSBType("220,90,50"), new byte[] { 0, 8, 0, 8, 0, 0 },
675 // TODO check brightness
677 // RGBW, only RGB part
678 helper("251.600", new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e }, new HSBType("207, 23, 19"),
679 new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
680 // RGBW, only RGB part
681 helper("251.600", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00, 0x0e },
682 new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
684 helper("251.600", new byte[] { 0x0, 0x0, 0x0, 0x1A, 0x00, 0x01 }, new PercentType("10.2"));
686 helper("251.60600", new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xff, 0x00, 0x0f },
687 new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 2, 0, 0 }, new byte[0]);
689 int[] rgbw = new int[] { 240, 0x0, 0x0, 0x0f };
690 HSBType hsb = ColorUtil.rgbToHsb(rgbw);
691 helper("251.60600", new byte[] { (byte) rgbw[0], (byte) rgbw[1], (byte) rgbw[2], (byte) rgbw[3], 0x00, 0x0f },
692 hsb, new byte[] { 2, 2, 2, 2, 0, 0 }, new byte[0]);
696 void testColorTransitionDpts() {
697 // DPT 243.600 DPT_Colour_Transition_xyY
698 // time(2) y(2) x(2), %brightness(1), flags(1)
699 helper("243.600", new byte[] { 0, 5, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
700 new StringType("(0.9922, 0.4961) 16.5 % 0.5 s"));
701 // DPT 249.600 DPT_Brightness_Colour_Temperature_Transition
702 // time(2) colortemp(2), brightness(1), flags(1)
703 helper("249.600", new byte[] { 0, 5, 0, 40, 127, 7 }, new StringType("49.8 % 40 K 0.5 s"));
704 // DPT 250.600 DPT_Brightness_Colour_Temperature_Control
705 // cct(1) cb(1) flags(1)
706 helper("250.600", new byte[] { 0x0f, 0x0e, 3 }, new StringType("CT increase 7 steps BRT increase 6 steps"));
707 // DPT 252.600 DPT_Relative_Control_RGBW
708 // r(1) g(1) b(1) w(1) flags(1)
709 helper("252.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x0c, 0x0f },
710 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps W increase 4 steps"));
711 // DPT 253.600 DPT_Relative_Control_xyY
712 // cs(1) ct(1) cb(1) flags(1)
713 helper("253.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x7 },
714 new StringType("x increase 7 steps y increase 6 steps Y increase 5 steps"));
715 // DPT 254.600 DPT_Relative_Control_RGB
717 helper("254.600", new byte[] { 0x0f, 0x0e, 0x0d },
718 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps"));
723 static void checkForMissingMainTypes() {
724 // checks if we have itests for all main DPT types supported by Calimero library,
725 // data is collected within method helper()
726 var wrapper = new Object() {
727 boolean testsMissing = false;
729 TranslatorTypes.getAllMainTypes().forEach((i, t) -> {
730 if (!dptTested.contains(i)) {
731 LOGGER.warn("missing tests for main DPT type " + i);
732 wrapper.testsMissing = true;
735 assertEquals(false, wrapper.testsMissing, "add tests for new DPT main types");