[knx] Add integration tests (#15727)

* [knx] Add integration tests
* [knx] Adapt handling of DPTs

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2023-12-16 12:51:14 +01:00 committed by GitHub
parent e6982e71bb
commit 4ba325d0df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 930 additions and 25 deletions

View File

@ -46,8 +46,8 @@ MainType: 3
3.008: DPT_Control_Blinds values: 0 = up 1 = down
MainType: 4
4.001: DPT_Char_ASCII
4.002: DPT_Char_8859_1
unsupported 4.001: DPT_Char_ASCII
unsupported 4.002: DPT_Char_8859_1
MainType: 5
5.000: General byte

View File

@ -70,22 +70,28 @@ public class DPTUtil {
Map.entry("9", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("10", Set.of(DateTimeType.class)), //
Map.entry("11", Set.of(DateTimeType.class)), //
Map.entry("12", Set.of(DecimalType.class)), //
Map.entry("12", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("13", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("14", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("16", Set.of(StringType.class)), //
Map.entry("17", Set.of(DecimalType.class)), //
Map.entry("18", Set.of(DecimalType.class)), //
Map.entry("19", Set.of(DateTimeType.class)), //
Map.entry("20", Set.of(StringType.class)), //
Map.entry("21", Set.of(StringType.class)), //
Map.entry("22", Set.of(StringType.class)), //
Map.entry("20", Set.of(StringType.class, DecimalType.class)), //
Map.entry("21", Set.of(StringType.class, DecimalType.class)), //
Map.entry("22", Set.of(StringType.class, DecimalType.class)), //
Map.entry("28", Set.of(StringType.class)), //
Map.entry("29", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("229", Set.of(DecimalType.class)), //
Map.entry("232", Set.of(HSBType.class)), //
Map.entry("242", Set.of(HSBType.class)), //
Map.entry("251", Set.of(HSBType.class, PercentType.class)));
Map.entry("243", Set.of(StringType.class)), //
Map.entry("249", Set.of(StringType.class)), //
Map.entry("250", Set.of(StringType.class)), //
Map.entry("251", Set.of(HSBType.class, PercentType.class)), //
Map.entry("252", Set.of(StringType.class)), //
Map.entry("253", Set.of(StringType.class)), //
Map.entry("254", Set.of(StringType.class)));
// compatible types for full DPTs
private static final Map<String, Set<Class<? extends Type>>> DPT_TYPE_MAP = Map.ofEntries(

View File

@ -48,7 +48,10 @@ import tuwien.auto.calimero.KNXFormatException;
import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
import tuwien.auto.calimero.dptxlator.DPTXlatorSceneControl;
@ -134,13 +137,22 @@ public class ValueDecoder {
}
return new DecimalType(decimalValue);
case "19":
return handleDpt19(translator);
case "16":
return handleDpt19(translator, data);
case "20":
case "21":
return handleStringOrDecimal(data, value, preferredType, 8);
case "22":
return handleStringOrDecimal(data, value, preferredType, 16);
case "16":
case "28":
case "250": // Map all combined color transitions to String,
case "252": // as no native support is planned.
case "253": // Currently only one subtype 2xx.600
case "254": // is defined for those DPTs.
return StringType.valueOf(value);
case "243": // color translation, fix regional
case "249": // settings
return StringType.valueOf(value.replace(',', '.').replace(". ", ", "));
case "232":
return handleDpt232(value, subType);
case "242":
@ -149,6 +161,7 @@ public class ValueDecoder {
return handleDpt251(value, preferredType);
default:
return handleNumericDpt(id, translator, preferredType);
// TODO 6.001 is mapped to PercentType, which can only cover 0-100%, not -128..127%
}
} catch (NumberFormatException | KNXFormatException | KNXIllegalArgumentException | ParseException e) {
LOGGER.info("Translator couldn't parse data '{}' for datapoint type '{}' ({}).", data, dptId, e.getClass());
@ -198,19 +211,10 @@ public class ValueDecoder {
}
private static Type handleDpt10(String value) throws ParseException {
if (value.contains("no-day")) {
/*
* KNX "no-day" needs special treatment since openHAB's DateTimeType doesn't support "no-day".
* Workaround: remove the "no-day" String, parse the remaining time string, which will result in a
* date of "1970-01-01".
* Replace "no-day" with the current day name
*/
StringBuilder stb = new StringBuilder(value);
int start = stb.indexOf("no-day");
int end = start + "no-day".length();
stb.replace(start, end, String.format(Locale.US, "%1$ta", Calendar.getInstance()));
value = stb.toString();
}
// TODO check handling of DPT10: date is not set to current date, but 1970-01-01 + offset if day is given
// maybe we should change the semantics and use current date + offset if day is given
// Calimero will provide either TIME_DAY_FORMAT or TIME_FORMAT, no-day is not printed
Date date = null;
try {
date = new SimpleDateFormat(TIME_DAY_FORMAT, Locale.US).parse(value);
@ -220,7 +224,7 @@ public class ValueDecoder {
return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(date));
}
private static @Nullable Type handleDpt19(DPTXlator translator) throws KNXFormatException {
private static @Nullable Type handleDpt19(DPTXlator translator, byte[] data) throws KNXFormatException {
DPTXlatorDateTime translatorDateTime = (DPTXlatorDateTime) translator;
if (translatorDateTime.isFaultyClock()) {
// Not supported: faulty clock
@ -263,7 +267,18 @@ public class ValueDecoder {
} else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
// Date format and time information
cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
try {
cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
} catch (KNXFormatException ignore) {
// throws KNXFormatException in case DST (SUTI) flag does not match calendar
// As the spec regards the SUTI flag as purely informative, flip it and try again.
if (data.length < 8) {
return null;
}
data[6] = (byte) (data[6] ^ 0x01);
translator.setData(data, 0);
cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
}
String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
return DateTimeType.valueOf(value);
} else {
@ -272,6 +287,30 @@ public class ValueDecoder {
}
}
private static @Nullable Type handleStringOrDecimal(byte[] data, String value, Class<? extends Type> preferredType,
int bits) {
if (DecimalType.class.equals(preferredType)) {
try {
// need a new translator for 8 bit unsigned, as Calimero handles only the string type
if (bits == 8) {
DPTXlator8BitUnsigned translator = new DPTXlator8BitUnsigned("5.010");
translator.setData(data);
return new DecimalType(translator.getValueUnsigned());
} else if (bits == 16) {
DPTXlator2ByteUnsigned translator = new DPTXlator2ByteUnsigned("7.001");
translator.setData(data);
return new DecimalType(translator.getValueUnsigned());
} else {
return null;
}
} catch (KNXFormatException e) {
return null;
}
} else {
return StringType.valueOf(value);
}
}
private static @Nullable Type handleDpt232(String value, String subType) {
Matcher rgb = RGB_PATTERN.matcher(value);
if (rgb.matches()) {
@ -358,6 +397,10 @@ public class ValueDecoder {
if (allowedTypes.contains(QuantityType.class) && !disableUoM) {
String unit = DPTUnits.getUnitForDpt(id);
if (unit != null) {
if (translator instanceof DPTXlator64BitSigned translatorSigned) {
// prevent loss of precision, do not represent 64bit decimal using double
return new QuantityType<>(translatorSigned.getValueSigned() + " " + unit);
}
return new QuantityType<>(value + " " + unit);
} else {
LOGGER.trace("Could not determine unit for DPT '{}', fallback to plain decimal", id);
@ -365,6 +408,10 @@ public class ValueDecoder {
}
if (allowedTypes.contains(DecimalType.class)) {
if (translator instanceof DPTXlator64BitSigned translatorSigned) {
// prevent loss of precision, do not represent 64bit decimal using double
return new DecimalType(translatorSigned.getValueSigned());
}
return new DecimalType(value);
}

View File

@ -16,6 +16,7 @@ import static org.openhab.binding.knx.internal.dpt.DPTUtil.NORMALIZED_DPT;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.Locale;
import java.util.regex.Matcher;
@ -108,6 +109,10 @@ public class ValueEncoder {
} else if (value instanceof DecimalType || value instanceof QuantityType<?>) {
return handleNumericTypes(dptId, mainNumber, dpt, value);
} else if (value instanceof StringType) {
if ("243.600".equals(dptId) || "249.600".equals(dptId)) {
return value.toString().replace('.', ((DecimalFormat) DecimalFormat.getInstance())
.getDecimalFormatSymbols().getDecimalSeparator());
}
return value.toString();
} else if (value instanceof DateTimeType type) {
return handleDateTimeType(dptId, type);

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.client;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData;
import org.openhab.core.thing.ThingUID;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.link.KNXNetworkLink;
/**
* {@link AbstractKNXClient} implementation for test, using {@link DummyKNXNetworkLink}.
*
* @author Holger Friedrich - initial contribution and API.
*
*/
@NonNullByDefault
public class DummyClient extends AbstractKNXClient {
public DummyClient() {
super(0, new ThingUID("dummy connection"), 0, 0, 0, null, new CommandExtensionData(Collections.emptyMap()),
null);
}
@Override
protected KNXNetworkLink establishConnection() throws KNXException, InterruptedException {
return new DummyKNXNetworkLink();
}
}

View File

@ -0,0 +1,134 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.client;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXAddress;
import tuwien.auto.calimero.KNXTimeoutException;
import tuwien.auto.calimero.Priority;
import tuwien.auto.calimero.cemi.CEMILData;
import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.NetworkLinkListener;
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
/**
* This class provides a simulated KNXNetworkLink with test stubs for integration tests.
*
* See Calimero documentation, calimero-ng.pdf.
*
* Frames sent via {@link #sendRequest()} and {@link sendRequestWait()} will be looped back
* to all registered listeners. {@link #getLastFrame()} will return the binary data provided
* to the last send command.
*
* @author Holger Friedrich - Initial contribution
*/
@NonNullByDefault({})
public class DummyKNXNetworkLink implements KNXNetworkLink {
public static final Logger LOGGER = LoggerFactory.getLogger(DummyKNXNetworkLink.class);
public static final int GROUP_WRITE = 0x80;
private byte[] lastFrame = new byte[0];
private Set<NetworkLinkListener> listeners = new HashSet<>();
public void setKNXMedium(KNXMediumSettings settings) {
LOGGER.warn(settings.toString());
}
public KNXMediumSettings getKNXMedium() {
return KNXMediumSettings.create(KNXMediumSettings.MEDIUM_TP1, new IndividualAddress(1, 2, 3));
}
public void addLinkListener(NetworkLinkListener l) {
listeners.add(l);
}
public void removeLinkListener(NetworkLinkListener l) {
listeners.remove(l);
}
public void setHopCount(int count) {
}
public int getHopCount() {
return 0;
}
public void sendRequest(KNXAddress dst, Priority p, byte[] nsdu)
throws KNXTimeoutException, KNXLinkClosedException {
sendRequestWait(dst, p, nsdu);
}
public void sendRequestWait(KNXAddress dst, Priority p, byte[] nsdu)
throws KNXTimeoutException, KNXLinkClosedException {
LOGGER.info("sendRequestWait() {} {} {}", dst, p, HexUtils.bytesToHex(nsdu, " "));
lastFrame = nsdu.clone();
// not we want to mimic a received frame by looping it back to all listeners
/*
* relevant steps to create a CEMI frame needed for triggering a frame event:
*
* final CEMILData f = (CEMILData) e.getFrame();
* final var apdu = f.getPayload();
* final int svc = DataUnitBuilder.getAPDUService(apdu);
* svc == GROUP_WRITE
* fireGroupReadWrite(f, DataUnitBuilder.extractASDU(apdu), svc, apdu.length <= 2);
* send(CEMILData.MC_LDATA_IND, dst, p, nsdu, true);
*/
int service = GROUP_WRITE;
byte[] apdu = new byte[nsdu.length + 2];
apdu[0] = (byte) (service >> 8);
apdu[1] = (byte) service;
System.arraycopy(nsdu, 0, apdu, 2, nsdu.length);
final IndividualAddress src = new IndividualAddress(1, 1, 1);
final boolean repeat = false;
final int hopCount = 1;
FrameEvent f = new FrameEvent(this, new CEMILData(CEMILData.MC_LDATA_IND, src, dst, nsdu, p, repeat, hopCount));
listeners.forEach(listener -> {
listener.indication(f);
});
}
public void send(CEMILData msg, boolean waitForCon) throws KNXTimeoutException, KNXLinkClosedException {
LOGGER.warn("send() not implemented");
}
public String getName() {
return "dummy link";
}
public boolean isOpen() {
return true;
}
public void close() {
}
public byte[] getLastFrame() {
return lastFrame;
}
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.client;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
/**
* This implementation of {@link ProcessListener} caches a received frames.
*
* It can be registered to {@link DummyKNXNetworkLink} to receive raw frame data.
*
* @author Holger Friedrich - Initial contribution
*
*/
@NonNullByDefault
public class DummyProcessListener implements ProcessListener {
private byte[] lastFrame = new byte[0];
public static final Logger LOGGER = LoggerFactory.getLogger(DummyProcessListener.class);
public DummyProcessListener() {
}
@Override
public void detached(@Nullable DetachEvent e) {
LOGGER.info("The KNX network link was detached from the process communicator");
}
@Override
public void groupWrite(@Nullable ProcessEvent e) {
if (e == null) {
lastFrame = new byte[0];
LOGGER.warn("invalid ProcessEvent");
return;
}
LOGGER.info("groupWrite({})", e.toString());
lastFrame = e.getASDU(); // clones
}
@Override
public void groupReadRequest(@Nullable ProcessEvent e) {
if (e == null) {
lastFrame = new byte[0];
LOGGER.warn("invalid ProcessEvent");
return;
}
LOGGER.warn("groupReadRequest({})", e.toString());
lastFrame = e.getASDU(); // clones
}
@Override
public void groupReadResponse(@Nullable ProcessEvent e) {
if (e == null) {
lastFrame = new byte[0];
LOGGER.warn("invalid ProcessEvent");
return;
}
LOGGER.warn("groupReadResponse({})", e.toString());
lastFrame = e.getASDU(); // clones
}
public byte[] getLastFrame() {
return lastFrame;
}
}

View File

@ -31,6 +31,7 @@ import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.util.ColorUtil;
import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
@ -330,7 +331,29 @@ class DPTTest {
}
@Test
public void dpt252EncoderTest() {
public void dpt251White() {
// input data: color white
byte[] data = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00, 0x0e };
HSBType hsbType = (HSBType) ValueDecoder.decode("251.600", data, HSBType.class);
assertNotNull(hsbType);
assertEquals(0, hsbType.getHue().doubleValue(), 0.5);
assertEquals(0, hsbType.getSaturation().doubleValue(), 0.5);
assertEquals(100, hsbType.getBrightness().doubleValue(), 0.5);
String enc = ValueEncoder.encode(hsbType, "251.600");
// white should be "100 100 100 - %", but expect small deviation due to rounding
assertNotNull(enc);
String[] parts = enc.split(" ");
assertEquals(5, parts.length);
int[] rgb = ColorUtil.hsbToRgb(hsbType);
assertEquals(rgb[0] * 100d / 255, Double.valueOf(parts[0].replace(',', '.')), 1);
assertEquals(rgb[1] * 100d / 255, Double.valueOf(parts[1].replace(',', '.')), 1);
assertEquals(rgb[2] * 100d / 255, Double.valueOf(parts[2].replace(',', '.')), 1);
}
@Test
public void dpt251Value() {
// input data
byte[] data = new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e };
HSBType hsbType = (HSBType) ValueDecoder.decode("251.600", data, HSBType.class);
@ -339,6 +362,16 @@ class DPTTest {
assertEquals(207, hsbType.getHue().doubleValue(), 0.5);
assertEquals(23, hsbType.getSaturation().doubleValue(), 0.5);
assertEquals(19, hsbType.getBrightness().doubleValue(), 0.5);
String enc = ValueEncoder.encode(hsbType, "251.600");
// white should be "100 100 100 - %", but expect small deviation due to rounding
assertNotNull(enc);
String[] parts = enc.split(" ");
assertEquals(5, parts.length);
int[] rgb = ColorUtil.hsbToRgb(hsbType);
assertEquals(rgb[0] * 100d / 255, Double.valueOf(parts[0].replace(',', '.')), 1);
assertEquals(rgb[1] * 100d / 255, Double.valueOf(parts[1].replace(',', '.')), 1);
assertEquals(rgb[2] * 100d / 255, Double.valueOf(parts[2].replace(',', '.')), 1);
}
// This test checks all our overrides for units. It allows to detect unnecessary overrides when we

View File

@ -0,0 +1,557 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.itests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openhab.binding.knx.internal.client.DummyKNXNetworkLink;
import org.openhab.binding.knx.internal.client.DummyProcessListener;
import org.openhab.binding.knx.internal.dpt.DPTUtil;
import org.openhab.binding.knx.internal.dpt.ValueDecoder;
import org.openhab.binding.knx.internal.dpt.ValueEncoder;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Type;
import org.openhab.core.util.ColorUtil;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.datapoint.CommandDP;
import tuwien.auto.calimero.datapoint.Datapoint;
import tuwien.auto.calimero.dptxlator.TranslatorTypes;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
/**
* Integration test to check conversion from raw KNX frame data to OH data types and back.
*
* This test checks
* <ul>
* <li>if OH can properly decode raw data payload from KNX frames using {@link ValueDecoder#decode()},
* <li>if OH can properly encode the data for handover to Calimero using {@link ValueEncoder#encode()},
* <li>if Calimero supports and correctly handles the data conversion to raw bytes for sending.
* </ul>
*
* In addition, it checks if newly integrated releases of Calimero introduce new DPT types not yet
* handled by this test. However, new subtypes are not detected.
*
* @see DummyKNXNetworkLink
* @see DummyClient
* @author Holger Friedrich - Initial contribution
*
*/
@NonNullByDefault
public class Back2BackTest {
public static final Logger LOGGER = LoggerFactory.getLogger(Back2BackTest.class);
static Set<Integer> dptTested = new HashSet<>();
boolean testsMissing = false;
/**
* helper method for integration tests
*
* @param dpt DPT type, e.g. "251.600", see 03_07_02-Datapoint-Types-v02.02.01-AS.pdf
* @param rawData byte array containing raw data, known content
* @param ohReferenceData OpenHAB data type, initialized to known good value
* @param maxDistance byte array containing maximal deviations when comparing byte arrays (rawData against created
* KNX frame), may be empty if no deviation is considered
* @param bitmask to mask certain bits in the raw to raw comparison, required for multi-valued KNX frames
*/
void helper(String dpt, byte[] rawData, Type ohReferenceData, byte[] maxDistance, byte[] bitmask) {
try {
DummyKNXNetworkLink link = new DummyKNXNetworkLink();
ProcessCommunicator pc = new ProcessCommunicatorImpl(link);
DummyProcessListener processListener = new DummyProcessListener();
pc.addProcessListener(processListener);
GroupAddress groupAddress = new GroupAddress(2, 4, 6);
Datapoint datapoint = new CommandDP(groupAddress, "dummy GA", 0,
DPTUtil.NORMALIZED_DPT.getOrDefault(dpt, dpt));
// 0) check usage of helper()
assertEquals(true, rawData.length > 0);
if (maxDistance.length == 0) {
maxDistance = new byte[rawData.length];
}
assertEquals(rawData.length, maxDistance.length, "incorrect length of maxDistance array");
if (bitmask.length == 0) {
bitmask = new byte[rawData.length];
Arrays.fill(bitmask, (byte) 0xff);
}
assertEquals(rawData.length, bitmask.length, "incorrect length of bitmask array");
int mainType = Integer.parseUnsignedInt(dpt.substring(0, dpt.indexOf('.')));
dptTested.add(Integer.valueOf(mainType));
// check if OH would be able to send out a frame, given the type
Set<Integer> knownWorking = Set.of(1, 3, 5);
if (!knownWorking.contains(mainType)) {
Set<Class<? extends Type>> allowedTypes = DPTUtil.getAllowedTypes("" + mainType);
if (!allowedTypes.contains(ohReferenceData.getClass())) {
LOGGER.warn(
"test for DPT {} uses type {} which is not contained in DPT_TYPE_MAP, sending may not be allowed",
dpt, ohReferenceData.getClass());
}
}
// 1) check if the decoder works (rawData to known good type ohReferenceData)
//
// This test is based on known raw data. The mapping to openHAB type is known and confirmed.
// In this test, only ValueDecoder.decode() is involved.
// raw data of the DPT on application layer, without all headers from the layers below
// see 03_07_02-Datapoint-Types-v02.02.01-AS.pdf
Type ohData = (Type) ValueDecoder.decode(dpt, rawData, ohReferenceData.getClass());
assertNotNull(ohData, "could not decode frame data for DPT " + dpt);
if ((ohReferenceData instanceof HSBType hsbReferenceData) && (ohData instanceof HSBType hsbData)) {
assertTrue(hsbReferenceData.closeTo(hsbData, 0.001),
"comparing reference to decoded value for DPT " + dpt);
} else {
assertEquals(ohReferenceData, ohData, "comparing reference to decoded value: failed for DPT " + dpt
+ ", check ValueEncoder.decode()");
}
// 2) check the encoding (ohData to raw data)
//
// Test approach is to a) encode the value into String format using ValueEncoder.encode(),
// b) pass it to Calimero for conversion into a raw representation, and
// c) finally grab raw data bytes from a custom KNXNetworkLink implementation
String enc = ValueEncoder.encode(ohData, dpt);
pc.write(datapoint, enc);
byte[] frame = link.getLastFrame();
assertNotNull(frame);
// remove header; for compact frames extract data byte from header
frame = DataUnitBuilder.extractASDU(frame);
assertEquals(rawData.length, frame.length,
"unexpected length of KNX frame: " + HexUtils.bytesToHex(frame, " "));
for (int i = 0; i < rawData.length; i++) {
assertEquals(rawData[i] & bitmask[i] & 0xff, frame[i] & bitmask[i] & 0xff, maxDistance[i],
"unexpected content in encoded data, " + i);
}
// 3) Check date provided by Calimero library as input via loopback, it should match the initial data
//
// Deviations in some bytes of the frame may be possible due to data conversion, e.g. for HSBType.
// This is why maxDistance is used.
byte[] input = processListener.getLastFrame();
LOGGER.info("loopback {}", HexUtils.bytesToHex(input, " "));
assertNotNull(input);
assertEquals(rawData.length, input.length, "unexpected length of loopback frame");
for (int i = 0; i < rawData.length; i++) {
assertEquals(rawData[i] & bitmask[i] & 0xff, input[i] & bitmask[i] & 0xff, maxDistance[i],
"unexpected content in loopback data, " + i);
}
pc.close();
} catch (KNXException e) {
LOGGER.warn("exception occurred", e.toString());
assertEquals("", e.toString());
}
}
void helper(String dpt, byte[] rawData, Type ohReferenceData) {
helper(dpt, rawData, ohReferenceData, new byte[0], new byte[0]);
}
@Test
void testDpt1() {
// for now only the DPTs for general use, others omitted
// TODO add tests for more subtypes
helper("1.001", new byte[] { 0 }, OnOffType.OFF);
helper("1.001", new byte[] { 1 }, OnOffType.ON);
helper("1.002", new byte[] { 0 }, OnOffType.OFF);
helper("1.002", new byte[] { 1 }, OnOffType.ON);
helper("1.003", new byte[] { 0 }, OnOffType.OFF);
helper("1.003", new byte[] { 1 }, OnOffType.ON);
helper("1.008", new byte[] { 0 }, UpDownType.UP);
helper("1.008", new byte[] { 1 }, UpDownType.DOWN);
// NOTE: This is how DPT 1.009 is defined: 0: open, 1: closed
// For historical reasons it is defined the other way on OH
helper("1.009", new byte[] { 0 }, OpenClosedType.CLOSED);
helper("1.009", new byte[] { 1 }, OpenClosedType.OPEN);
helper("1.010", new byte[] { 0 }, StopMoveType.STOP);
helper("1.010", new byte[] { 1 }, StopMoveType.MOVE);
helper("1.015", new byte[] { 0 }, OnOffType.OFF);
helper("1.015", new byte[] { 1 }, OnOffType.ON);
helper("1.016", new byte[] { 0 }, OnOffType.OFF);
helper("1.016", new byte[] { 1 }, OnOffType.ON);
// DPT 1.017 is a special case, "trigger" has no "value", both 0 and 1 shall trigger
helper("1.017", new byte[] { 0 }, OnOffType.OFF);
// Calimero maps it always to 0
// helper("1.017", new byte[] { 1 }, OnOffType.ON);
helper("1.018", new byte[] { 0 }, OnOffType.OFF);
helper("1.018", new byte[] { 1 }, OnOffType.ON);
helper("1.019", new byte[] { 0 }, OpenClosedType.CLOSED);
helper("1.019", new byte[] { 1 }, OpenClosedType.OPEN);
helper("1.024", new byte[] { 0 }, OnOffType.OFF);
helper("1.024", new byte[] { 1 }, OnOffType.ON);
}
@Test
void testDpt2() {
for (int subType = 1; subType <= 12; subType++) {
helper("2." + String.format("%03d", subType), new byte[] { 3 }, new DecimalType(3));
}
}
@Test
void testDpt3() {
// DPT 3.007 and DPT 3.008 consist of a control bit (1 bit) and stepsize (3 bit)
// if stepsize is 0, OH will ignore the command
byte controlBit = 1 << 3;
// loop all other step sizes and check only the control bit
for (byte i = 1; i < 8; i++) {
helper("3.007", new byte[] { i }, IncreaseDecreaseType.DECREASE, new byte[0], new byte[] { controlBit });
helper("3.007", new byte[] { (byte) (i + controlBit) }, IncreaseDecreaseType.INCREASE, new byte[0],
new byte[] { controlBit });
helper("3.008", new byte[] { i }, UpDownType.UP, new byte[0], new byte[] { controlBit });
helper("3.008", new byte[] { (byte) (i + controlBit) }, UpDownType.DOWN, new byte[0],
new byte[] { controlBit });
}
// check if OH ignores incoming frames with mask 0 (mapped to UndefType)
Assertions.assertFalse(ValueDecoder.decode("3.007", new byte[] { 0 },
IncreaseDecreaseType.class) instanceof IncreaseDecreaseType);
Assertions.assertFalse(ValueDecoder.decode("3.007", new byte[] { controlBit },
IncreaseDecreaseType.class) instanceof IncreaseDecreaseType);
Assertions.assertFalse(ValueDecoder.decode("3.008", new byte[] { 0 }, UpDownType.class) instanceof UpDownType);
Assertions.assertFalse(
ValueDecoder.decode("3.008", new byte[] { controlBit }, UpDownType.class) instanceof UpDownType);
}
@Test
void testDpt5() {
// TODO add tests for more subtypes
helper("5.001", new byte[] { 0 }, new PercentType(0));
helper("5.001", new byte[] { (byte) 0x80 }, new PercentType(50));
helper("5.001", new byte[] { (byte) 0xff }, new PercentType(100));
helper("5.010", new byte[] { 42 }, new DecimalType(42));
helper("5.010", new byte[] { (byte) 0xff }, new DecimalType(255));
}
@Test
void testDpt6() {
helper("6.010", new byte[] { 0 }, new DecimalType(0));
helper("6.010", new byte[] { (byte) 0x7f }, new DecimalType(127));
helper("6.010", new byte[] { (byte) 0xff }, new DecimalType(-1));
// TODO 6.001 is mapped to PercentType, which can only cover 0-100%, not -128..127%
// helper("6.001", new byte[] { 0 }, new DecimalType(0));
}
@Test
void testDpt7() {
// TODO add tests for more subtypes
helper("7.001", new byte[] { 0, 42 }, new DecimalType(42));
helper("7.001", new byte[] { (byte) 0xff, (byte) 0xff }, new DecimalType(65535));
}
@Test
void testDpt8() {
// TODO add tests for more subtypes
helper("8.001", new byte[] { (byte) 0x7f, (byte) 0xff }, new DecimalType(32767));
helper("8.001", new byte[] { (byte) 0x80, (byte) 0x00 }, new DecimalType(-32768));
}
@Test
void testDpt9() {
// TODO add tests for more subtypes
helper("9.001", new byte[] { (byte) 0x00, (byte) 0x64 }, new QuantityType<Temperature>("1 °C"));
}
@Test
void testDpt10() {
// TODO check handling of DPT10: date is not set to current date, but 1970-01-01 + offset if day is given
// maybe we should change the semantics and use current date + offset if day is given
// note: local timezone is set when creating DateTimeType, for example "1970-01-01Thh:mm:ss.000+0100"
// no-day
assertTrue(Objects
.toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, 0 }, DecimalType.class))
.startsWith("1970-01-01T17:30:00.000+"));
// Thursday, this is correct for 1970-01-01
assertTrue(Objects
.toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, 0 }, DecimalType.class))
.startsWith("1970-01-01T17:30:00.000+"));
// Monday -> 1970-01-05
assertTrue(Objects
.toString(ValueDecoder.decode("10.001", new byte[] { (byte) 0x31, (byte) 0x1e, 0 }, DecimalType.class))
.startsWith("1970-01-05T17:30:00.000+"));
// Thursday, otherwise first byte of encoded data will not match
helper("10.001", new byte[] { (byte) 0x91, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"));
helper("10.001", new byte[] { (byte) 0x11, (byte) 0x1e, (byte) 0x0 }, new DateTimeType("17:30:00"), new byte[0],
new byte[] { (byte) 0x1f, (byte) 0xff, (byte) 0xff });
}
@Test
void testDpt11() {
// note: local timezone and dst is set when creating DateTimeType, for example "2019-06-12T00:00:00.000+0200"
helper("11.001", new byte[] { (byte) 12, 6, 19 }, new DateTimeType("2019-06-12"));
}
@Test
void testDpt12() {
helper("12.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
new DecimalType("4294967294"));
helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 s"));
helper("12.100", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 min"));
helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("60 min"));
helper("12.101", new byte[] { 0, 0, 0, 60 }, new QuantityType<>("1 h"));
helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 h"));
helper("12.102", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("60 min"));
helper("12.1200", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 l"));
helper("12.1200", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
new QuantityType<>("4294967294 l"));
helper("12.1201", new byte[] { 0, 0, 0, 1 }, new QuantityType<>("1 m³"));
helper("12.1201", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe },
new QuantityType<>("4294967294 m³"));
}
@Test
void testDpt13() {
// TODO add tests for more subtypes
helper("13.001", new byte[] { 0, 0, 0, 0 }, new DecimalType(0));
helper("13.001", new byte[] { 0, 0, 0, 42 }, new DecimalType(42));
helper("13.001", new byte[] { (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff },
new DecimalType(2147483647));
// KNX representation typically uses two's complement
helper("13.001", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }, new DecimalType(-1));
helper("13.001", new byte[] { (byte) 0x80, (byte) 0x0, (byte) 0x0, (byte) 0x0 }, new DecimalType(-2147483648));
}
@Test
void testDpt14() {
// TODO add tests for more subtypes
helper("14.068", new byte[] { (byte) 0x3f, (byte) 0x80, 0, 0 }, new QuantityType<Temperature>("1 °C"));
}
@Test
void testDpt16() {
helper("16.000", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
helper("16.001", new byte[] { (byte) 0x4B, (byte) 0x4E, 0x58, 0x20, 0x69, 0x73, 0x20, (byte) 0x4F, (byte) 0x4B,
0x0, 0x0, 0x0, 0x0, 0x0 }, new StringType("KNX is OK"));
}
@Test
void testDpt17() {
helper("17.001", new byte[] { 0 }, new DecimalType(0));
helper("17.001", new byte[] { 42 }, new DecimalType(42));
helper("17.001", new byte[] { 63 }, new DecimalType(63));
}
@Test
void testDpt18() {
// scene, activate 0..63
helper("18.001", new byte[] { 0 }, new DecimalType(0));
helper("18.001", new byte[] { 42 }, new DecimalType(42));
helper("18.001", new byte[] { 63 }, new DecimalType(63));
// scene, learn += 0x80
helper("18.001", new byte[] { (byte) (0x80 + 0) }, new DecimalType(0x80));
helper("18.001", new byte[] { (byte) (0x80 + 42) }, new DecimalType(0x80 + 42));
helper("18.001", new byte[] { (byte) (0x80 + 63) }, new DecimalType(0x80 + 63));
}
@Test
void testDpt19() {
// 2019-01-15 17:30:00
helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
new DateTimeType("2019-01-15T17:30:00"));
helper("19.001", new byte[] { (byte) (2019 - 1900), 1, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
new DateTimeType("2019-01-15T17:30:00"));
// 2019-07-15 17:30:00
helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x25, (byte) 0x00 },
new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
helper("19.001", new byte[] { (byte) (2019 - 1900), 7, 15, 17, 30, 0, (byte) 0x24, (byte) 0x00 },
new DateTimeType("2019-07-15T17:30:00"), new byte[0], new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 });
}
@Test
void testDpt20() {
// test default String representation of enum (incomplete)
helper("20.001", new byte[] { 0 }, new StringType("autonomous"));
helper("20.001", new byte[] { 1 }, new StringType("slave"));
helper("20.001", new byte[] { 2 }, new StringType("master"));
helper("20.002", new byte[] { 0 }, new StringType("building in use"));
helper("20.002", new byte[] { 1 }, new StringType("building not used"));
helper("20.002", new byte[] { 2 }, new StringType("building protection"));
// test DecimalType representation of enum
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,
106, 107, 108, 109, 110, 111, 112, 113, 114, 120, 121, 122, 600, 601, 602, 603, 604, 605, 606, 607, 608,
609, 610, 801, 802, 803, 804, 1000, 1001, 1002, 1003, 1004, 1005, 1200, 1202 };
for (int subType : subTypes) {
helper("20." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
}
// once these DPTs are available in Calimero, add to check above
int[] unsupportedSubTypes = new int[] { 22, 115, 611, 612, 613, 1203, 1204, 1205, 1206, 1207, 1208, 1209 };
for (int subType : unsupportedSubTypes) {
assertNull(ValueDecoder.decode("20." + String.format("%03d", subType), new byte[] { 0 }, StringType.class));
}
}
@Test
void testDpt21() {
// test default String representation of bitfield (incomplete)
helper("21.001", new byte[] { 5 }, new StringType("overridden, out of service"));
// test DecimalType representation of bitfield
int[] subTypes = new int[] { 1, 2, 100, 101, 102, 103, 104, 105, 106, 601, 1000, 1001, 1002, 1010 };
for (int subType : subTypes) {
helper("21." + String.format("%03d", subType), new byte[] { 1 }, new DecimalType(1));
}
// once these DPTs are available in Calimero, add to check above
assertNull(ValueDecoder.decode("21.1200", new byte[] { 0 }, StringType.class));
assertNull(ValueDecoder.decode("21.1201", new byte[] { 0 }, StringType.class));
}
@Test
void testDpt22() {
// test default String representation of bitfield (incomplete)
helper("22.101", new byte[] { 1, 0 }, new StringType("heating mode"));
helper("22.101", new byte[] { 1, 2 }, new StringType("heating mode, heating eco mode"));
// test DecimalType representation of bitfield
helper("22.101", new byte[] { 0, 2 }, new DecimalType(2));
helper("22.1000", new byte[] { 0, 2 }, new DecimalType(2));
// once these DPTs are available in Calimero, add to check above
assertNull(ValueDecoder.decode("22.100", new byte[] { 0, 2 }, StringType.class));
assertNull(ValueDecoder.decode("22.1010", new byte[] { 0, 2 }, StringType.class));
}
@Test
void testDpt28() {
// null terminated strings, UTF8
helper("28.001", new byte[] { 0x31, 0x32, 0x33, 0x34, 0x0 }, new StringType("1234"));
helper("28.001", new byte[] { (byte) 0xce, (byte) 0xb5, 0x34, 0x0 }, new StringType("\u03b54"));
}
@Test
void testDpt29() {
helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
helper("29.010", new byte[] { (byte) 0x80, 0, 0, 0, 0, 0, 0, 0 },
new QuantityType<>("-9223372036854775808 Wh"));
helper("29.010", new byte[] { (byte) 0xff, 0, 0, 0, 0, 0, 0, 0 }, new QuantityType<>("-72057594037927936 Wh"));
helper("29.010", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 Wh"));
helper("29.011", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 VAh"));
helper("29.012", new byte[] { 0, 0, 0, 0, 0, 0, 0, 42 }, new QuantityType<>("42 varh"));
}
@Test
void testDpt229() {
// special DPT for metering, allows several units and different scaling
// -> Calimero uses scaling, but always encodes as dimensionless value
final int dimensionlessCounter = 0b10111010;
helper("229.001", new byte[] { 0, 0, 0, 0, (byte) dimensionlessCounter, 0 }, new DecimalType(0));
}
@Test
void testColorDpts() {
// HSB
helper("232.600", new byte[] { 123, 45, 67 }, ColorUtil.rgbToHsb(new int[] { 123, 45, 67 }));
// RGB, MDT specific
helper("232.60000", new byte[] { 123, 45, 67 }, new HSBType("173.6, 17.6, 26.3"));
// xyY
int x = (int) (14.65 * 65535.0 / 100.0);
int y = (int) (11.56 * 65535.0 / 100.0);
// encoding is always xy and brightness (C+B, 0x03), do not test other combinations
helper("242.600", new byte[] { (byte) ((x >> 8) & 0xff), (byte) (x & 0xff), (byte) ((y >> 8) & 0xff),
(byte) (y & 0xff), (byte) 0x28, 0x3 }, new HSBType("220,90,50"), new byte[] { 0, 8, 0, 8, 0, 0 },
new byte[0]);
// TODO check brightness
// RGBW, only RGB part
helper("251.600", new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e }, new HSBType("207, 23, 19"),
new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
// RGBW, only RGB part
helper("251.600", new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00, 0x0e },
new HSBType("0, 0, 100"), new byte[] { 1, 1, 1, 0, 0, 0 }, new byte[0]);
}
@Test
void testColorTransitionDpts() {
// DPT 243.600 DPT_Colour_Transition_xyY
// time(2) y(2) x(2), %brightness(1), flags(1)
helper("243.600", new byte[] { 0, 5, 0x7F, 0, (byte) 0xfe, 0, 42, 3 },
new StringType("(0.9922, 0.4961) 16.5 % 0.5 s"));
// DPT 249.600 DPT_Brightness_Colour_Temperature_Transition
// time(2) colortemp(2), brightness(1), flags(1)
helper("249.600", new byte[] { 0, 5, 0, 40, 127, 7 }, new StringType("49.8 % 40 K 0.5 s"));
// DPT 250.600 DPT_Brightness_Colour_Temperature_Control
// cct(1) cb(1) flags(1)
helper("250.600", new byte[] { 0x0f, 0x0e, 3 }, new StringType("CT increase 7 steps BRT increase 6 steps"));
// DPT 252.600 DPT_Relative_Control_RGBW
// r(1) g(1) b(1) w(1) flags(1)
helper("252.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x0c, 0x0f },
new StringType("R increase 7 steps G increase 6 steps B increase 5 steps W increase 4 steps"));
// DPT 253.600 DPT_Relative_Control_xyY
// cs(1) ct(1) cb(1) flags(1)
helper("253.600", new byte[] { 0x0f, 0x0e, 0x0d, 0x7 },
new StringType("x increase 7 steps y increase 6 steps Y increase 5 steps"));
// DPT 254.600 DPT_Relative_Control_RGB
// cr(1) cg(1) cb(1)
helper("254.600", new byte[] { 0x0f, 0x0e, 0x0d },
new StringType("R increase 7 steps G increase 6 steps B increase 5 steps"));
}
@Test
@AfterAll
static void checkForMissingMainTypes() {
// checks if we have itests for all main DPT types supported by Calimero library,
// data is collected within method helper()
var wrapper = new Object() {
boolean testsMissing = false;
};
TranslatorTypes.getAllMainTypes().forEach((i, t) -> {
if (!dptTested.contains(i)) {
LOGGER.warn("missing tests for main DPT type " + i);
wrapper.testsMissing = true;
}
});
assertEquals(false, wrapper.testsMissing, "add tests for new DPT main types");
}
}