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 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;
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;
62 * Integration test to check conversion from raw KNX frame data to OH data types and back.
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.
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.
74 * @see DummyKNXNetworkLink
76 * @author Holger Friedrich - Initial contribution
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;
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[] { (byte) (subType % 4) },
305 new DecimalType(subType % 4));
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 });
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);
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));
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));
355 helper("5.010", new byte[] { 42 }, new DecimalType(42));
356 helper("5.010", new byte[] { (byte) 0xff }, new DecimalType(255));
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
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));
370 helper("6.020", new byte[] { 9 }, StringType.valueOf("0/0/0/0/1 0"));
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"));
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"));
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"));
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"));
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"));
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"));
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³"));
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
567 // note: local timezone is set when creating DateTimeType, for example "1970-01-01Thh:mm:ss.000+0100"
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
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
579 .toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x31, (byte) 0x1e, 0 }, DecimalType.class))
580 .startsWith("1970-01-05T17:30:00.000+"));
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 });
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"));
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"));
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³"));
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"));
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"));
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"));
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³"));
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"));
768 helper("14.1200", F32_MINUS_ONE, new QuantityType<>("-1 m³/h"));
769 helper("14.1201", F32_MINUS_ONE, new QuantityType<>("-1 l/s"));
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"));
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));
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));
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)
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"));
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"));
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));
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));
841 // test default String representation of bitfield (incomplete)
842 helper("21.001", new byte[] { 5 }, new StringType("overridden, out of service"));
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));
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));
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"));
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));
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"));
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"));
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));
895 void testColorDpts() {
897 helper("232.600", new byte[] { 123, 45, 67 }, ColorUtil.rgbToHsb(new int[] { 123, 45, 67 }));
899 helper("232.60000", new byte[] { 123, 45, 67 }, new HSBType("173.6, 17.6, 26.3"));
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 },
908 // TODO check brightness
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]);
917 helper("251.600", new byte[] { 0x0, 0x0, 0x0, 0x1A, 0x00, 0x01 }, new PercentType("10.2"));
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]);
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]);
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
959 helper("254.600", new byte[] { 0x0f, 0x0e, 0x0d },
960 new StringType("R increase 7 steps G increase 6 steps B increase 5 steps"));
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;
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;
977 assertEquals(false, wrapper.testsMissing, "add tests for new DPT main types");