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