]> git.basschouten.com Git - openhab-addons.git/blob
5781d1192bf8bf19d84ba59ef3302b6ba629d571
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.knx.internal.client;
14
15 import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
16 import static org.openhab.binding.knx.internal.handler.DeviceConstants.*;
17
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.HexFormat;
21 import java.util.Map;
22 import java.util.Set;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
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;
36
37 /**
38  * Client dedicated to read device specific information using the {@link DeviceInfoClient}.
39  *
40  * @author Simon Kaufmann - initial contribution and API.
41  * @author Holger Friedrich - support additional device properties
42  *
43  */
44 @NonNullByDefault
45 public class DeviceInspector {
46
47     private static final long OPERATION_TIMEOUT = 5000;
48     private static final long OPERATION_INTERVAL = 2000;
49
50     private final Logger logger = LoggerFactory.getLogger(DeviceInspector.class);
51     private final DeviceInfoClient client;
52     private final IndividualAddress address;
53
54     public static class Result {
55         private final Map<String, String> properties;
56         private final Set<GroupAddress> groupAddresses;
57
58         public Result(Map<String, String> properties, Set<GroupAddress> groupAddresses) {
59             super();
60             this.properties = properties;
61             this.groupAddresses = groupAddresses;
62         }
63
64         public Map<String, String> getProperties() {
65             return properties;
66         }
67
68         public Set<GroupAddress> getGroupAddresses() {
69             return groupAddresses;
70         }
71     }
72
73     public DeviceInspector(DeviceInfoClient client, IndividualAddress address) {
74         this.client = client;
75         this.address = address;
76     }
77
78     private DeviceInfoClient getClient() {
79         return client;
80     }
81
82     /**
83      * {@link readDeviceInfo} tries to read information from the KNX device.
84      * This function catches {@link java.lang.InterruptedException}. It can safely be cancelled.
85      *
86      * The number of properties returned by this function depends on the data provided
87      * by the KNX device.
88      *
89      * @return List of device properties
90      */
91     @Nullable
92     public Result readDeviceInfo() {
93         if (!getClient().isConnected()) {
94             return null;
95         }
96
97         logger.debug("Fetching device information for address {}", address);
98         Map<String, String> properties = new HashMap<>();
99         try {
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 : "");
106         }
107         return new Result(properties, Collections.emptySet());
108     }
109
110     /**
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.
115      *
116      * @param address Individual address of KNX device
117      * @return List of device properties
118      * @throws InterruptedException
119      */
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,
125                 OPERATION_TIMEOUT);
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");
130
131             Thread.sleep(OPERATION_INTERVAL);
132             String serialNo = toHex(getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.SERIAL_NUMBER, 1, 1,
133                     false, OPERATION_TIMEOUT), "");
134
135             Thread.sleep(OPERATION_INTERVAL);
136             String hardwareType = toHex(getClient().readDeviceProperties(address, DEVICE_OBJECT, HARDWARE_TYPE, 1, 1,
137                     false, OPERATION_TIMEOUT), " ");
138
139             // PID_FIRMWARE_REVISION, optional, fallback PID_VERSION according to spec
140             Thread.sleep(OPERATION_INTERVAL);
141             String firmwareRevision = null;
142             try {
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));
147                 } else {
148                     // try fallback to PID_VERSION
149                     Thread.sleep(OPERATION_INTERVAL);
150                     result = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.VERSION, 1, 1, false,
151                             OPERATION_TIMEOUT);
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));
157                     }
158                 }
159             } catch (InterruptedException e) {
160                 throw e;
161             } catch (Exception ignore) {
162                 // allowed to fail, optional
163             }
164
165             // MAX_APDU_LENGTH, for *routing*, optional, fallback to MAX_APDU_LENGTH of device
166             Thread.sleep(OPERATION_INTERVAL);
167             String maxApdu = "";
168             try {
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));
173                 }
174             } catch (InterruptedException e) {
175                 throw e;
176             } catch (Exception ignore) {
177                 // allowed to fail, optional
178             }
179             if (!maxApdu.isEmpty()) {
180                 logger.trace("Max APDU of device {} is {} bytes (routing)", address, maxApdu);
181             } else {
182                 // fallback: MAX_APDU_LENGTH; if availble set the default is 14 according to spec
183                 Thread.sleep(OPERATION_INTERVAL);
184                 try {
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));
189                     }
190                 } catch (InterruptedException e) {
191                     throw e;
192                 } catch (Exception ignore) {
193                     // allowed to fail, optional
194                 }
195                 if (!maxApdu.isEmpty()) {
196                     logger.trace("Max APDU of device {} is {} bytes", address, maxApdu);
197                 } else {
198                     logger.trace("Max APDU of device {} not set, fallback to 14 bytes", address);
199                     maxApdu = "14"; // see spec
200                 }
201             }
202
203             Thread.sleep(OPERATION_INTERVAL);
204             byte[] orderInfo = getClient().readDeviceProperties(address, DEVICE_OBJECT, PID.ORDER_INFO, 1, 1, false,
205                     OPERATION_TIMEOUT);
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;
213                     } else {
214                         final String printable = result.replaceAll("[^\\x20-\\x7E]", ".");
215                         if (!printable.equals(result)) {
216                             result = printable + " (0x" + hexString + ")";
217                         }
218                     }
219                     logger.trace("Order code of device {} is \"{}\"", address, result);
220                     ret.put(MANUFACTURER_ORDER_INFO, result);
221                 }
222             }
223
224             // read FRIENDLY_NAME, optional
225             Thread.sleep(OPERATION_INTERVAL);
226             try {
227                 byte[] count = getClient().readDeviceProperties(address, ROUTER_OBJECT, PID.FRIENDLY_NAME, 0, 1, false,
228                         OPERATION_TIMEOUT);
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) {
238                             if (data != null) {
239                                 buf.append(new String(data));
240                             }
241                         } else {
242                             break;
243                         }
244                     }
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);
249                     } else {
250                         // this is due to devices which have a buggy implememtation (and show a broken string also
251                         // in ETS tool)
252                         logger.debug("Ignoring FRIENDLY_NAME of device {} as it contains non-printable characters",
253                                 address);
254                     }
255                 }
256             } catch (InterruptedException e) {
257                 throw e;
258             } catch (Exception e) {
259                 // allowed to fail, optional
260             }
261
262             ret.put(MANUFACTURER_NAME, manufacturerId);
263             if (serialNo != null) {
264                 ret.put(MANUFACTURER_SERIAL_NO, serialNo);
265             }
266             if (hardwareType != null) {
267                 ret.put(MANUFACTURER_HARDWARE_TYPE, hardwareType);
268             }
269             if (firmwareRevision != null) {
270                 ret.put(MANUFACTURER_FIRMWARE_REVISION, firmwareRevision);
271             }
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);
275         } else {
276             logger.debug("The KNX device with address {} does not expose a Device Object", address);
277         }
278         return ret;
279     }
280
281     private @Nullable String toHex(byte @Nullable [] input, String separator) {
282         return input == null ? null : HexFormat.ofDelimiter(separator).formatHex(input);
283     }
284
285     /**
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.
289      *
290      * @param address Individual address of KNX device
291      * @return List of device properties
292      * @throws InterruptedException
293      */
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);
297         if (data != null) {
298             try {
299                 final DD0 dd = DeviceDescriptor.DD0.from(data);
300
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());
308             }
309         } else {
310             logger.debug("The device with address {} does not expose a Device Descriptor type 0", address);
311         }
312         if (logger.isDebugEnabled()) {
313             Thread.sleep(OPERATION_INTERVAL);
314             data = getClient().readDeviceDescription(address, 2, false, OPERATION_TIMEOUT);
315             if (data != null) {
316                 try {
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,
321                             e.getMessage());
322                 }
323             }
324         }
325         return ret;
326     }
327
328     private int toUnsigned(final byte @Nullable [] data) {
329         if (data == null) {
330             return 0;
331         }
332         int value = data[0] & 0xff;
333         if (data.length == 1) {
334             return value;
335         }
336         value = value << 8 | data[1] & 0xff;
337         if (data.length == 2) {
338             return value;
339         }
340         value = value << 16 | data[2] & 0xff << 8 | data[3] & 0xff;
341         return value;
342     }
343
344     private static String getMediumType(int type) {
345         switch (type) {
346             case 0:
347                 return "TP";
348             case 1:
349                 return "PL";
350             case 2:
351                 return "RF";
352             case 3:
353                 return "TP0 (deprecated)";
354             case 4:
355                 return "PL123 (deprecated)";
356             case 5:
357                 return "IP";
358             default:
359                 return "unknown (" + type + ")";
360         }
361     }
362 }