2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.knx.internal.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;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.knx.internal.handler.Manufacturer;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
29 import tuwien.auto.calimero.DataUnitBuilder;
30 import tuwien.auto.calimero.DeviceDescriptor;
31 import tuwien.auto.calimero.DeviceDescriptor.DD0;
32 import tuwien.auto.calimero.DeviceDescriptor.DD2;
33 import tuwien.auto.calimero.GroupAddress;
34 import tuwien.auto.calimero.IndividualAddress;
35 import tuwien.auto.calimero.KNXIllegalArgumentException;
36 import tuwien.auto.calimero.mgmt.PropertyAccess.PID;
39 * Client dedicated to read device specific information using the {@link DeviceInfoClient}.
41 * @author Simon Kaufmann - initial contribution and API.
42 * @author Holger Friedrich - support additional device properties
46 public class DeviceInspector {
48 private static final long OPERATION_TIMEOUT = 5000;
49 private static final long OPERATION_INTERVAL = 2000;
51 private final Logger logger = LoggerFactory.getLogger(DeviceInspector.class);
52 private final DeviceInfoClient client;
53 private final IndividualAddress address;
55 public static class Result {
56 private final Map<String, String> properties;
57 private final Set<GroupAddress> groupAddresses;
59 public Result(Map<String, String> properties, Set<GroupAddress> groupAddresses) {
61 this.properties = properties;
62 this.groupAddresses = groupAddresses;
65 public Map<String, String> getProperties() {
69 public Set<GroupAddress> getGroupAddresses() {
70 return groupAddresses;
74 public DeviceInspector(DeviceInfoClient client, IndividualAddress address) {
76 this.address = address;
79 private DeviceInfoClient getClient() {
84 * {@link readDeviceInfo} tries to read information from the KNX device.
85 * This function catches {@link java.lang.InterruptedException}. It can safely be cancelled.
87 * The number of properties returned by this function depends on the data provided
90 * @return List of device properties
93 public Result readDeviceInfo() {
94 if (!getClient().isConnected()) {
98 logger.debug("Fetching device information for address {}", address);
99 Map<String, String> properties = new HashMap<>();
101 properties.putAll(readDeviceDescription(address));
102 properties.putAll(readDeviceProperties(address));
103 } catch (InterruptedException e) {
104 final String msg = e.getMessage();
105 logger.debug("Interrupted while fetching the device description for a device '{}' {}", address,
106 msg != null ? ": " + msg : "");
108 return new Result(properties, Collections.emptySet());
112 * @implNote {@link readDeviceProperties(address)} tries to read several properties from the KNX device.
113 * Errors reading single properties are ignored, the respective item is skipped and readout continues
114 * with next property. {@link java.lang.InterruptedException} is thrown to allow for stopping the readout
115 * task immediately on connection loss or thing deconstruction.
117 * @param address Individual address of KNX device
118 * @return List of device properties
119 * @throws InterruptedException
121 private Map<String, String> readDeviceProperties(IndividualAddress address) throws InterruptedException {
122 Map<String, String> ret = new HashMap<>();
123 Thread.sleep(OPERATION_INTERVAL);
124 // check if there is a Device Object in the KNX device
125 byte[] elements = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.OBJECT_TYPE, 0, 1, false,
127 if ((elements == null ? 0 : toUnsigned(elements)) == 1) {
128 Thread.sleep(OPERATION_INTERVAL);
129 String manufacturerID = Manufacturer.getName(toUnsigned(getClient().readDeviceProperties(address,
130 DEVICE_OBJECT, PID.MANUFACTURER_ID, 1, 1, false, OPERATION_TIMEOUT)));
132 Thread.sleep(OPERATION_INTERVAL);
133 String serialNo = toHex(getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.SERIAL_NUMBER, 1, 1,
134 false, OPERATION_TIMEOUT), "");
136 Thread.sleep(OPERATION_INTERVAL);
137 String hardwareType = toHex(getClient().readDeviceProperties(address, DEVICE_OBJECT, HARDWARE_TYPE, 1, 1,
138 false, OPERATION_TIMEOUT), " ");
140 // PID_FIRMWARE_REVISION, optional, fallback PID_VERSION according to spec
141 Thread.sleep(OPERATION_INTERVAL);
142 String firmwareRevision = null;
144 byte[] result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.FIRMWARE_REVISION, 1, 1,
145 false, OPERATION_TIMEOUT);
146 if (result != null) {
147 firmwareRevision = Integer.toString(toUnsigned(result));
149 // try fallback to PID_VERSION
150 Thread.sleep(OPERATION_INTERVAL);
151 result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.VERSION, 1, 1, false,
153 if (result != null) {
154 // data format is DPT217.001
155 int i = toUnsigned(result);
156 firmwareRevision = Integer.toString((i & 0xF800) >> 11) + "."
157 + Integer.toString((i & 0x07C0) >> 6) + "." + Integer.toString((i & 0x003F));
160 } catch (InterruptedException e) {
162 } catch (Exception ignore) {
163 // allowed to fail, optional
166 // MAX_APDU_LENGTH, for *routing*, optional, fallback to MAX_APDU_LENGTH of device
167 Thread.sleep(OPERATION_INTERVAL);
170 byte[] result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.MAX_APDULENGTH, 1, 1,
171 false, OPERATION_TIMEOUT);
172 if (result != null) {
173 maxApdu = Integer.toString(toUnsigned(result));
175 } catch (InterruptedException e) {
177 } catch (Exception ignore) {
178 // allowed to fail, optional
180 if (!maxApdu.isEmpty()) {
181 logger.trace("Max APDU of device {} is {} bytes (routing)", address, maxApdu);
183 // fallback: MAX_APDU_LENGTH; if availble set the default is 14 according to spec
184 Thread.sleep(OPERATION_INTERVAL);
186 byte[] result = getClient().readDeviceProperties(address, ADDRESS_TABLE_OBJECT,
187 MAX_ROUTED_APDU_LENGTH, 1, 1, false, OPERATION_TIMEOUT);
188 if (result != null) {
189 maxApdu = Integer.toString(toUnsigned(result));
191 } catch (InterruptedException e) {
193 } catch (Exception ignore) {
194 // allowed to fail, optional
196 if (!maxApdu.isEmpty()) {
197 logger.trace("Max APDU of device {} is {} bytes", address, maxApdu);
199 logger.trace("Max APDU of device {} not set, fallback to 14 bytes", address);
200 maxApdu = "14"; // see spec
204 Thread.sleep(OPERATION_INTERVAL);
205 byte[] orderInfo = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.ORDER_INFO, 1, 1, false,
207 if (orderInfo != null) {
208 final String hexString = toHex(orderInfo, "");
209 if (!"ffffffffffffffffffff".equals(hexString) && !"00000000000000000000".equals(hexString)) {
210 String result = new String(orderInfo);
211 result = result.trim();
212 if (result.isEmpty()) {
213 result = "0x" + hexString;
215 final String printable = result.replaceAll("[^\\x20-\\x7E]", ".");
216 if (!printable.equals(result)) {
217 result = printable + " (0x" + hexString + ")";
220 logger.trace("Order code of device {} is \"{}\"", address, result);
221 ret.put(MANUFACTURER_ORDER_INFO, result);
225 // read FRIENDLY_NAME, optional
226 Thread.sleep(OPERATION_INTERVAL);
228 byte[] count = getClient().readDeviceProperties(address, ROUTER_OBJECT, PID.FRIENDLY_NAME, 0, 1, false,
230 if ((count != null) && (toUnsigned(count) == 30)) {
231 StringBuffer buf = new StringBuffer(30);
232 for (int i = 1; i <= 30; i++) {
233 Thread.sleep(OPERATION_INTERVAL);
234 // for some reason, reading more than one character per message fails
235 // reading only one character is inefficient, but works
236 byte[] data = getClient().readDeviceProperties(address, ROUTER_OBJECT, PID.FRIENDLY_NAME, i, 1,
237 false, OPERATION_TIMEOUT);
238 if (toUnsigned(data) != 0) {
240 buf.append(new String(data));
246 final String result = buf.toString();
247 if (result.matches("^[\\x20-\\x7E]+$")) {
248 logger.debug("Identified device {} as \"{}\"", address, result);
249 ret.put(FRIENDLY_NAME, result);
251 // this is due to devices which have a buggy implememtation (and show a broken string also
253 logger.debug("Ignoring FRIENDLY_NAME of device {} as it contains non-printable characters",
257 } catch (InterruptedException e) {
259 } catch (Exception e) {
260 // allowed to fail, optional
263 ret.put(MANUFACTURER_NAME, manufacturerID);
264 if (serialNo != null) {
265 ret.put(MANUFACTURER_SERIAL_NO, serialNo);
267 if (hardwareType != null) {
268 ret.put(MANUFACTURER_HARDWARE_TYPE, hardwareType);
270 if (firmwareRevision != null) {
271 ret.put(MANUFACTURER_FIRMWARE_REVISION, firmwareRevision);
273 ret.put(MAX_APDU_LENGTH, maxApdu);
274 logger.debug("Identified device {} as {}, type {}, revision {}, serial number {}, max APDU {}", address,
275 manufacturerID, hardwareType, firmwareRevision, serialNo, maxApdu);
277 logger.debug("The KNX device with address {} does not expose a Device Object", address);
282 private @Nullable String toHex(byte @Nullable [] input, String separator) {
283 return input == null ? null : DataUnitBuilder.toHex(input, separator);
287 * @implNote {@link readDeviceDescription(address)} tries to read device description from the KNX device.
288 * According to KNX specification, eihter device descriptor DD0 or DD2 must be implemented.
289 * Currently only data from DD0 is returned; DD2 is just logged in debug mode.
291 * @param address Individual address of KNX device
292 * @return List of device properties
293 * @throws InterruptedException
295 private Map<String, String> readDeviceDescription(IndividualAddress address) throws InterruptedException {
296 Map<String, String> ret = new HashMap<>();
297 byte[] data = getClient().readDeviceDescription(address, 0, false, OPERATION_TIMEOUT);
300 final DD0 dd = DeviceDescriptor.DD0.from(data);
302 ret.put(DEVICE_MASK_VERSION, String.format("%04X", dd.maskVersion()));
303 ret.put(DEVICE_PROFILE, dd.deviceProfile());
304 ret.put(DEVICE_MEDIUM_TYPE, getMediumType(dd.mediumType()));
305 logger.debug("The device with address {} has mask {} ({}, medium {})", address,
306 ret.get(DEVICE_MASK_VERSION), ret.get(DEVICE_PROFILE), ret.get(DEVICE_MEDIUM_TYPE));
307 } catch (KNXIllegalArgumentException e) {
308 logger.warn("Can not parse Device Descriptor 0 of device with address {}: {}", address, e.getMessage());
311 logger.debug("The device with address {} does not expose a Device Descriptor type 0", address);
313 if (logger.isDebugEnabled()) {
314 Thread.sleep(OPERATION_INTERVAL);
315 data = getClient().readDeviceDescription(address, 2, false, OPERATION_TIMEOUT);
318 final DD2 dd = DeviceDescriptor.DD2.from(data);
319 logger.debug("The device with address {} is has DD2 {}", address, dd.toString());
320 } catch (KNXIllegalArgumentException e) {
321 logger.warn("Can not parse device descriptor 2 of device with address {}: {}", address,
329 private int toUnsigned(final byte @Nullable [] data) {
333 int value = data[0] & 0xff;
334 if (data.length == 1) {
337 value = value << 8 | data[1] & 0xff;
338 if (data.length == 2) {
341 value = value << 16 | data[2] & 0xff << 8 | data[3] & 0xff;
345 private static String getMediumType(int type) {
354 return "TP0 (deprecated)";
356 return "PL123 (deprecated)";
360 return "unknown (" + type + ")";