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.client;
15 import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
16 import static org.openhab.binding.knx.internal.handler.DeviceConstants.*;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.HexFormat;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
29 import tuwien.auto.calimero.DeviceDescriptor;
30 import tuwien.auto.calimero.DeviceDescriptor.DD0;
31 import tuwien.auto.calimero.DeviceDescriptor.DD2;
32 import tuwien.auto.calimero.GroupAddress;
33 import tuwien.auto.calimero.IndividualAddress;
34 import tuwien.auto.calimero.KNXIllegalArgumentException;
35 import tuwien.auto.calimero.mgmt.PropertyAccess.PID;
38 * Client dedicated to read device specific information using the {@link DeviceInfoClient}.
40 * @author Simon Kaufmann - initial contribution and API.
41 * @author Holger Friedrich - support additional device properties
45 public class DeviceInspector {
47 private static final long OPERATION_TIMEOUT = 5000;
48 private static final long OPERATION_INTERVAL = 2000;
50 private final Logger logger = LoggerFactory.getLogger(DeviceInspector.class);
51 private final DeviceInfoClient client;
52 private final IndividualAddress address;
54 public static class Result {
55 private final Map<String, String> properties;
56 private final Set<GroupAddress> groupAddresses;
58 public Result(Map<String, String> properties, Set<GroupAddress> groupAddresses) {
60 this.properties = properties;
61 this.groupAddresses = groupAddresses;
64 public Map<String, String> getProperties() {
68 public Set<GroupAddress> getGroupAddresses() {
69 return groupAddresses;
73 public DeviceInspector(DeviceInfoClient client, IndividualAddress address) {
75 this.address = address;
78 private DeviceInfoClient getClient() {
83 * {@link readDeviceInfo} tries to read information from the KNX device.
84 * This function catches {@link java.lang.InterruptedException}. It can safely be cancelled.
86 * The number of properties returned by this function depends on the data provided
89 * @return List of device properties
92 public Result readDeviceInfo() {
93 if (!getClient().isConnected()) {
97 logger.debug("Fetching device information for address {}", address);
98 Map<String, String> properties = new HashMap<>();
100 properties.putAll(readDeviceDescription(address));
101 properties.putAll(readDeviceProperties(address));
102 } catch (InterruptedException e) {
103 final String msg = e.getMessage();
104 logger.debug("Interrupted while fetching the device description for a device '{}' {}", address,
105 msg != null ? ": " + msg : "");
107 return new Result(properties, Collections.emptySet());
111 * @implNote {@link readDeviceProperties(address)} tries to read several properties from the KNX device.
112 * Errors reading single properties are ignored, the respective item is skipped and readout continues
113 * with next property. {@link java.lang.InterruptedException} is thrown to allow for stopping the readout
114 * task immediately on connection loss or thing deconstruction.
116 * @param address Individual address of KNX device
117 * @return List of device properties
118 * @throws InterruptedException
120 private Map<String, String> readDeviceProperties(IndividualAddress address) throws InterruptedException {
121 Map<String, String> ret = new HashMap<>();
122 Thread.sleep(OPERATION_INTERVAL);
123 // check if there is a Device Object in the KNX device
124 byte[] elements = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.OBJECT_TYPE, 0, 1, false,
126 if ((elements == null ? 0 : toUnsigned(elements)) == 1) {
127 Thread.sleep(OPERATION_INTERVAL);
128 String manufacturerId = MANUFACTURER_MAP.getOrDefault(toUnsigned(getClient().readDeviceProperties(address,
129 DEVICE_OBJECT, PID.MANUFACTURER_ID, 1, 1, false, OPERATION_TIMEOUT)), "Unknown");
131 Thread.sleep(OPERATION_INTERVAL);
132 String serialNo = toHex(getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.SERIAL_NUMBER, 1, 1,
133 false, OPERATION_TIMEOUT), "");
135 Thread.sleep(OPERATION_INTERVAL);
136 String hardwareType = toHex(getClient().readDeviceProperties(address, DEVICE_OBJECT, HARDWARE_TYPE, 1, 1,
137 false, OPERATION_TIMEOUT), " ");
139 // PID_FIRMWARE_REVISION, optional, fallback PID_VERSION according to spec
140 Thread.sleep(OPERATION_INTERVAL);
141 String firmwareRevision = null;
143 byte[] result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.FIRMWARE_REVISION, 1, 1,
144 false, OPERATION_TIMEOUT);
145 if (result != null) {
146 firmwareRevision = Integer.toString(toUnsigned(result));
148 // try fallback to PID_VERSION
149 Thread.sleep(OPERATION_INTERVAL);
150 result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.VERSION, 1, 1, false,
152 if (result != null) {
153 // data format is DPT217.001
154 int i = toUnsigned(result);
155 firmwareRevision = Integer.toString((i & 0xF800) >> 11) + "."
156 + Integer.toString((i & 0x07C0) >> 6) + "." + Integer.toString((i & 0x003F));
159 } catch (InterruptedException e) {
161 } catch (Exception ignore) {
162 // allowed to fail, optional
165 // MAX_APDU_LENGTH, for *routing*, optional, fallback to MAX_APDU_LENGTH of device
166 Thread.sleep(OPERATION_INTERVAL);
169 byte[] result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.MAX_APDULENGTH, 1, 1,
170 false, OPERATION_TIMEOUT);
171 if (result != null) {
172 maxApdu = Integer.toString(toUnsigned(result));
174 } catch (InterruptedException e) {
176 } catch (Exception ignore) {
177 // allowed to fail, optional
179 if (!maxApdu.isEmpty()) {
180 logger.trace("Max APDU of device {} is {} bytes (routing)", address, maxApdu);
182 // fallback: MAX_APDU_LENGTH; if availble set the default is 14 according to spec
183 Thread.sleep(OPERATION_INTERVAL);
185 byte[] result = getClient().readDeviceProperties(address, ADDRESS_TABLE_OBJECT,
186 MAX_ROUTED_APDU_LENGTH, 1, 1, false, OPERATION_TIMEOUT);
187 if (result != null) {
188 maxApdu = Integer.toString(toUnsigned(result));
190 } catch (InterruptedException e) {
192 } catch (Exception ignore) {
193 // allowed to fail, optional
195 if (!maxApdu.isEmpty()) {
196 logger.trace("Max APDU of device {} is {} bytes", address, maxApdu);
198 logger.trace("Max APDU of device {} not set, fallback to 14 bytes", address);
199 maxApdu = "14"; // see spec
203 Thread.sleep(OPERATION_INTERVAL);
204 byte[] orderInfo = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.ORDER_INFO, 1, 1, false,
206 if (orderInfo != null) {
207 final String hexString = toHex(orderInfo, "");
208 if (!"ffffffffffffffffffff".equals(hexString) && !"00000000000000000000".equals(hexString)) {
209 String result = new String(orderInfo);
210 result = result.trim();
211 if (result.isEmpty()) {
212 result = "0x" + hexString;
214 final String printable = result.replaceAll("[^\\x20-\\x7E]", ".");
215 if (!printable.equals(result)) {
216 result = printable + " (0x" + hexString + ")";
219 logger.trace("Order code of device {} is \"{}\"", address, result);
220 ret.put(MANUFACTURER_ORDER_INFO, result);
224 // read FRIENDLY_NAME, optional
225 Thread.sleep(OPERATION_INTERVAL);
227 byte[] count = getClient().readDeviceProperties(address, ROUTER_OBJECT, PID.FRIENDLY_NAME, 0, 1, false,
229 if ((count != null) && (toUnsigned(count) == 30)) {
230 StringBuilder buf = new StringBuilder(30);
231 for (int i = 1; i <= 30; i++) {
232 Thread.sleep(OPERATION_INTERVAL);
233 // for some reason, reading more than one character per message fails
234 // reading only one character is inefficient, but works
235 byte[] data = getClient().readDeviceProperties(address, ROUTER_OBJECT, PID.FRIENDLY_NAME, i, 1,
236 false, OPERATION_TIMEOUT);
237 if (toUnsigned(data) != 0) {
239 buf.append(new String(data));
245 final String result = buf.toString();
246 if (result.matches("^[\\x20-\\x7E]+$")) {
247 logger.debug("Identified device {} as \"{}\"", address, result);
248 ret.put(FRIENDLY_NAME, result);
250 // this is due to devices which have a buggy implememtation (and show a broken string also
252 logger.debug("Ignoring FRIENDLY_NAME of device {} as it contains non-printable characters",
256 } catch (InterruptedException e) {
258 } catch (Exception e) {
259 // allowed to fail, optional
262 ret.put(MANUFACTURER_NAME, manufacturerId);
263 if (serialNo != null) {
264 ret.put(MANUFACTURER_SERIAL_NO, serialNo);
266 if (hardwareType != null) {
267 ret.put(MANUFACTURER_HARDWARE_TYPE, hardwareType);
269 if (firmwareRevision != null) {
270 ret.put(MANUFACTURER_FIRMWARE_REVISION, firmwareRevision);
272 ret.put(MAX_APDU_LENGTH, maxApdu);
273 logger.debug("Identified device {} as {}, type {}, revision {}, serial number {}, max APDU {}", address,
274 manufacturerId, hardwareType, firmwareRevision, serialNo, maxApdu);
276 logger.debug("The KNX device with address {} does not expose a Device Object", address);
281 private @Nullable String toHex(byte @Nullable [] input, String separator) {
282 return input == null ? null : HexFormat.ofDelimiter(separator).formatHex(input);
286 * @implNote {@link readDeviceDescription(address)} tries to read device description from the KNX device.
287 * According to KNX specification, either device descriptor DD0 or DD2 must be implemented.
288 * Currently only data from DD0 is returned; DD2 is just logged in debug mode.
290 * @param address Individual address of KNX device
291 * @return List of device properties
292 * @throws InterruptedException
294 private Map<String, String> readDeviceDescription(IndividualAddress address) throws InterruptedException {
295 Map<String, String> ret = new HashMap<>();
296 byte[] data = getClient().readDeviceDescription(address, 0, false, OPERATION_TIMEOUT);
299 final DD0 dd = DeviceDescriptor.DD0.from(data);
301 ret.put(DEVICE_MASK_VERSION, String.format("%04X", dd.maskVersion()));
302 ret.put(DEVICE_PROFILE, dd.deviceProfile());
303 ret.put(DEVICE_MEDIUM_TYPE, getMediumType(dd.mediumType()));
304 logger.debug("The device with address {} has mask {} ({}, medium {})", address,
305 ret.get(DEVICE_MASK_VERSION), ret.get(DEVICE_PROFILE), ret.get(DEVICE_MEDIUM_TYPE));
306 } catch (KNXIllegalArgumentException e) {
307 logger.warn("Can not parse Device Descriptor 0 of device with address {}: {}", address, e.getMessage());
310 logger.debug("The device with address {} does not expose a Device Descriptor type 0", address);
312 if (logger.isDebugEnabled()) {
313 Thread.sleep(OPERATION_INTERVAL);
314 data = getClient().readDeviceDescription(address, 2, false, OPERATION_TIMEOUT);
317 final DD2 dd = DeviceDescriptor.DD2.from(data);
318 logger.debug("The device with address {} is has DD2 {}", address, dd.toString());
319 } catch (KNXIllegalArgumentException e) {
320 logger.warn("Can not parse device descriptor 2 of device with address {}: {}", address,
328 private int toUnsigned(final byte @Nullable [] data) {
332 int value = data[0] & 0xff;
333 if (data.length == 1) {
336 value = value << 8 | data[1] & 0xff;
337 if (data.length == 2) {
340 value = value << 16 | data[2] & 0xff << 8 | data[3] & 0xff;
344 private static String getMediumType(int type) {
353 return "TP0 (deprecated)";
355 return "PL123 (deprecated)";
359 return "unknown (" + type + ")";