]> git.basschouten.com Git - openhab-addons.git/blob
1fc19b0ce423738d4729627d03552fc70eaf6a63
[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.dpt;
14
15 import static org.openhab.binding.knx.internal.KNXBindingConstants.disableUoM;
16
17 import java.math.BigDecimal;
18 import java.text.ParseException;
19 import java.text.SimpleDateFormat;
20 import java.util.Calendar;
21 import java.util.Date;
22 import java.util.Locale;
23 import java.util.Set;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.core.library.types.DateTimeType;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.HSBType;
32 import org.openhab.core.library.types.IncreaseDecreaseType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.OpenClosedType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.StopMoveType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.types.UpDownType;
40 import org.openhab.core.types.Type;
41 import org.openhab.core.types.UnDefType;
42 import org.openhab.core.util.ColorUtil;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import tuwien.auto.calimero.KNXException;
47 import tuwien.auto.calimero.KNXFormatException;
48 import tuwien.auto.calimero.KNXIllegalArgumentException;
49 import tuwien.auto.calimero.dptxlator.DPTXlator;
50 import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled;
51 import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned;
52 import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
53 import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned;
54 import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
55 import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
56 import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
57 import tuwien.auto.calimero.dptxlator.DPTXlatorSceneControl;
58 import tuwien.auto.calimero.dptxlator.TranslatorTypes;
59
60 /**
61  * This class decodes raw data received from the KNX bus to an openHAB datatype
62  *
63  * Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al.
64  *
65  * @author Jan N. Klug - Initial contribution
66  */
67 @NonNullByDefault
68 public class ValueDecoder {
69     private static final Logger LOGGER = LoggerFactory.getLogger(ValueDecoder.class);
70
71     private static final String TIME_DAY_FORMAT = "EEE, HH:mm:ss";
72     private static final String TIME_FORMAT = "HH:mm:ss";
73     private static final String DATE_FORMAT = "yyyy-MM-dd";
74     // RGB: "r:123 g:123 b:123" value-range: 0-255
75     private static final Pattern RGB_PATTERN = Pattern.compile("r:(?<r>\\d+) g:(?<g>\\d+) b:(?<b>\\d+)");
76     // RGBW: "100 27 25 12 %", value range: 0-100, invalid values: "-"
77     private static final Pattern RGBW_PATTERN = Pattern
78             .compile("(?:(?<r>[\\d,.]+)|-)\\s(?:(?<g>[\\d,.]+)|-)\\s(?:(?<b>[\\d,.]+)|-)\\s(?:(?<w>[\\d,.]+)|-)\\s%");
79     // xyY: "(0,123 0,123) 56 %", value range 0-1 for xy (comma or point as decimal point), 0-100 for Y, invalid values
80     // omitted
81     public static final Pattern XYY_PATTERN = Pattern
82             .compile("(?:\\((?<x>\\d+(?:[,.]\\d+)?) (?<y>\\d+(?:[,.]\\d+)?)\\))?\\s*(?:(?<Y>\\d+(?:[,.]\\d+)?)\\s%)?");
83
84     /**
85      * convert the raw value received to the corresponding openHAB value
86      *
87      * @param dptId the DPT of the given data
88      * @param data a byte array containing the value
89      * @param preferredType the preferred datatype for this conversion
90      * @return the data converted to an openHAB Type (or null if conversion failed)
91      */
92     public static @Nullable Type decode(String dptId, byte[] data, Class<? extends Type> preferredType) {
93         try {
94             DPTXlator translator = TranslatorTypes.createTranslator(0,
95                     DPTUtil.NORMALIZED_DPT.getOrDefault(dptId, dptId));
96             translator.setData(data);
97             String value = translator.getValue();
98
99             String id = dptId; // prefer using the user-supplied DPT
100
101             Matcher m = DPTUtil.DPT_PATTERN.matcher(id);
102             if (!m.matches() || m.groupCount() != 2) {
103                 LOGGER.trace("User-Supplied DPT '{}' did not match for sub-type, using DPT returned from Translator",
104                         id);
105                 id = translator.getType().getID();
106                 m = DPTUtil.DPT_PATTERN.matcher(id);
107                 if (!m.matches() || m.groupCount() != 2) {
108                     LOGGER.warn("Couldn't identify main/sub number in dptID '{}'", id);
109                     return null;
110                 }
111             }
112             LOGGER.trace("Finally using datapoint DPT = {}", id);
113
114             String mainType = m.group("main");
115             String subType = m.group("sub");
116
117             switch (mainType) {
118                 case "1":
119                     return handleDpt1(subType, translator, preferredType);
120                 case "2":
121                     DPTXlator1BitControlled translator1BitControlled = (DPTXlator1BitControlled) translator;
122                     int decValue = (translator1BitControlled.getControlBit() ? 2 : 0)
123                             + (translator1BitControlled.getValueBit() ? 1 : 0);
124                     return new DecimalType(decValue);
125                 case "3":
126                     return handleDpt3(subType, translator);
127                 case "10":
128                     return handleDpt10(value);
129                 case "11":
130                     return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN)
131                             .format(new SimpleDateFormat(DATE_FORMAT).parse(value)));
132                 case "18":
133                     DPTXlatorSceneControl translatorSceneControl = (DPTXlatorSceneControl) translator;
134                     int decimalValue = translatorSceneControl.getSceneNumber();
135                     if (value.startsWith("learn")) {
136                         decimalValue += 0x80;
137                     }
138                     return new DecimalType(decimalValue);
139                 case "19":
140                     return handleDpt19(translator, data);
141                 case "20":
142                 case "21":
143                     return handleStringOrDecimal(data, value, preferredType, 8);
144                 case "22":
145                     return handleStringOrDecimal(data, value, preferredType, 16);
146                 case "16":
147                 case "28":
148                 case "250": // Map all combined color transitions to String,
149                 case "252": // as no native support is planned.
150                 case "253": // Currently only one subtype 2xx.600
151                 case "254": // is defined for those DPTs.
152                     return StringType.valueOf(value);
153                 case "243": // color translation, fix regional
154                 case "249": // settings
155                     return StringType.valueOf(value.replace(',', '.').replace(". ", ", "));
156                 case "232":
157                     return handleDpt232(value, subType);
158                 case "242":
159                     return handleDpt242(value);
160                 case "251":
161                     return handleDpt251(value, preferredType);
162                 default:
163                     return handleNumericDpt(id, translator, preferredType);
164                 // TODO 6.001 is mapped to PercentType, which can only cover 0-100%, not -128..127%
165             }
166         } catch (NumberFormatException | KNXFormatException | KNXIllegalArgumentException | ParseException e) {
167             LOGGER.info("Translator couldn't parse data '{}' for datapoint type '{}' ({}).", data, dptId, e.getClass());
168         } catch (KNXException e) {
169             LOGGER.warn("Failed creating a translator for datapoint type '{}'.", dptId, e);
170         }
171
172         return null;
173     }
174
175     private static Type handleDpt1(String subType, DPTXlator translator, Class<? extends Type> preferredType) {
176         DPTXlatorBoolean translatorBoolean = (DPTXlatorBoolean) translator;
177         switch (subType) {
178             case "008":
179                 return translatorBoolean.getValueBoolean() ? UpDownType.DOWN : UpDownType.UP;
180             case "009":
181             case "019":
182                 // default is OpenClosedType (Contact), but it may be mapped to OnOffType as well
183                 if (OnOffType.class.equals(preferredType)) {
184                     return OnOffType.from(translatorBoolean.getValueBoolean());
185                 }
186
187                 // This is wrong for DPT 1.009. It should be true -> CLOSE, false -> OPEN, but unfortunately
188                 // can't be fixed without breaking a lot of working installations.
189                 // The documentation has been updated to reflect that. / @J-N-K
190                 return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
191             case "010":
192                 return translatorBoolean.getValueBoolean() ? StopMoveType.MOVE : StopMoveType.STOP;
193             case "022":
194                 return DecimalType.valueOf(translatorBoolean.getValueBoolean() ? "1" : "0");
195             default:
196                 // default is OnOffType (Switch), but it may be mapped to OpenClosedType as well
197                 if (OpenClosedType.class.equals(preferredType)) {
198                     return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
199                 }
200
201                 return OnOffType.from(translatorBoolean.getValueBoolean());
202         }
203     }
204
205     private static @Nullable Type handleDpt3(String subType, DPTXlator translator) {
206         DPTXlator3BitControlled translator3BitControlled = (DPTXlator3BitControlled) translator;
207         if (translator3BitControlled.getStepCode() == 0) {
208             LOGGER.debug("convertRawDataToType: KNX DPT_Control_Dimming: break received.");
209             return UnDefType.NULL;
210         }
211         switch (subType) {
212             case "007":
213                 return translator3BitControlled.getControlBit() ? IncreaseDecreaseType.INCREASE
214                         : IncreaseDecreaseType.DECREASE;
215             case "008":
216                 return translator3BitControlled.getControlBit() ? UpDownType.DOWN : UpDownType.UP;
217             default:
218                 LOGGER.warn("DPT3, subtype '{}' is unknown.", subType);
219                 return null;
220         }
221     }
222
223     private static Type handleDpt10(String value) throws ParseException {
224         // TODO check handling of DPT10: date is not set to current date, but 1970-01-01 + offset if day is given
225         // maybe we should change the semantics and use current date + offset if day is given
226
227         // Calimero will provide either TIME_DAY_FORMAT or TIME_FORMAT, no-day is not printed
228         Date date = null;
229         try {
230             date = new SimpleDateFormat(TIME_DAY_FORMAT, Locale.US).parse(value);
231         } catch (ParseException pe) {
232             date = new SimpleDateFormat(TIME_FORMAT, Locale.US).parse(value);
233         }
234         return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(date));
235     }
236
237     private static @Nullable Type handleDpt19(DPTXlator translator, byte[] data) throws KNXFormatException {
238         DPTXlatorDateTime translatorDateTime = (DPTXlatorDateTime) translator;
239         if (translatorDateTime.isFaultyClock()) {
240             // Not supported: faulty clock
241             LOGGER.debug("KNX clock msg ignored: clock faulty bit set, which is not supported");
242             return null;
243         } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
244                 && translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) {
245             // Not supported: "/1/1" (month and day without year)
246             LOGGER.debug("KNX clock msg ignored: no year, but day and month, which is not supported");
247             return null;
248         } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
249                 && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) {
250             // Not supported: "1900" (year without month and day)
251             LOGGER.debug("KNX clock msg ignored: no day and month, but year, which is not supported");
252             return null;
253         } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
254                 && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)
255                 && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
256             // Not supported: No year, no date and no time
257             LOGGER.debug("KNX clock msg ignored: no day and month or year, which is not supported");
258             return null;
259         }
260
261         Calendar cal = Calendar.getInstance();
262         if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
263                 && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
264             // Pure date format, no time information
265             cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
266             String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
267             return DateTimeType.valueOf(value);
268         } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
269                 && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
270             // Pure time format, no date information
271             cal.clear();
272             cal.set(Calendar.HOUR_OF_DAY, translatorDateTime.getHour());
273             cal.set(Calendar.MINUTE, translatorDateTime.getMinute());
274             cal.set(Calendar.SECOND, translatorDateTime.getSecond());
275             String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
276             return DateTimeType.valueOf(value);
277         } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
278                 && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
279             // Date format and time information
280             try {
281                 cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
282             } catch (KNXFormatException ignore) {
283                 // throws KNXFormatException in case DST (SUTI) flag does not match calendar
284                 // As the spec regards the SUTI flag as purely informative, flip it and try again.
285                 if (data.length < 8) {
286                     return null;
287                 }
288                 data[6] = (byte) (data[6] ^ 0x01);
289                 translator.setData(data, 0);
290                 cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
291             }
292             String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
293             return DateTimeType.valueOf(value);
294         } else {
295             LOGGER.warn("Failed to convert '{}'", translator.getValue());
296             return null;
297         }
298     }
299
300     private static @Nullable Type handleStringOrDecimal(byte[] data, String value, Class<? extends Type> preferredType,
301             int bits) {
302         if (DecimalType.class.equals(preferredType)) {
303             try {
304                 // need a new translator for 8 bit unsigned, as Calimero handles only the string type
305                 if (bits == 8) {
306                     DPTXlator8BitUnsigned translator = new DPTXlator8BitUnsigned("5.010");
307                     translator.setData(data);
308                     return new DecimalType(translator.getValueUnsigned());
309                 } else if (bits == 16) {
310                     DPTXlator2ByteUnsigned translator = new DPTXlator2ByteUnsigned("7.001");
311                     translator.setData(data);
312                     return new DecimalType(translator.getValueUnsigned());
313                 } else {
314                     return null;
315                 }
316             } catch (KNXFormatException e) {
317                 return null;
318             }
319         } else {
320             return StringType.valueOf(value);
321         }
322     }
323
324     private static @Nullable Type handleDpt232(String value, String subType) {
325         Matcher rgb = RGB_PATTERN.matcher(value);
326         if (rgb.matches()) {
327             int r = Integer.parseInt(rgb.group("r"));
328             int g = Integer.parseInt(rgb.group("g"));
329             int b = Integer.parseInt(rgb.group("b"));
330
331             switch (subType) {
332                 case "600":
333                     return HSBType.fromRGB(r, g, b);
334                 case "60000":
335                     // MDT specific: mis-use 232.600 for hsv instead of rgb
336                     DecimalType hue = new DecimalType(coerceToRange(r * 360.0 / 255.0, 0.0, 359.9999));
337                     PercentType sat = new PercentType(BigDecimal.valueOf(coerceToRange(g / 2.55, 0.0, 100.0)));
338                     PercentType bright = new PercentType(BigDecimal.valueOf(coerceToRange(b / 2.55, 0.0, 100.0)));
339                     return new HSBType(hue, sat, bright);
340                 default:
341                     LOGGER.warn("Unknown subtype '232.{}', no conversion possible.", subType);
342                     return null;
343             }
344         }
345         LOGGER.warn("Failed to convert '{}' (DPT 232): Pattern does not match", value);
346         return null;
347     }
348
349     private static @Nullable Type handleDpt242(String value) {
350         Matcher xyY = XYY_PATTERN.matcher(value);
351         if (xyY.matches()) {
352             String stringx = xyY.group("x");
353             String stringy = xyY.group("y");
354             String stringY = xyY.group("Y");
355
356             if (stringx != null && stringy != null) {
357                 double x = Double.parseDouble(stringx.replace(",", "."));
358                 double y = Double.parseDouble(stringy.replace(",", "."));
359                 if (stringY == null) {
360                     return ColorUtil.xyToHsb(new double[] { x, y });
361                 } else {
362                     double pY = Double.parseDouble(stringY.replace(",", "."));
363                     return ColorUtil.xyToHsb(new double[] { x, y, pY / 100.0 });
364                 }
365             }
366         }
367         LOGGER.warn("Failed to convert '{}' (DPT 242): Pattern does not match", value);
368         return null;
369     }
370
371     private static @Nullable Type handleDpt251(String value, Class<? extends Type> preferredType) {
372         Matcher rgbw = RGBW_PATTERN.matcher(value);
373         if (rgbw.matches()) {
374             String rString = rgbw.group("r");
375             String gString = rgbw.group("g");
376             String bString = rgbw.group("b");
377             String wString = rgbw.group("w");
378
379             if (rString != null && gString != null && bString != null && HSBType.class.equals(preferredType)) {
380                 // does not support PercentType and r,g,b valid -> HSBType
381                 int r = coerceToRange((int) (Double.parseDouble(rString.replace(",", ".")) * 2.55), 0, 255);
382                 int g = coerceToRange((int) (Double.parseDouble(gString.replace(",", ".")) * 2.55), 0, 255);
383                 int b = coerceToRange((int) (Double.parseDouble(bString.replace(",", ".")) * 2.55), 0, 255);
384
385                 return HSBType.fromRGB(r, g, b);
386             } else if (wString != null && PercentType.class.equals(preferredType)) {
387                 // does support PercentType and w valid -> PercentType
388                 BigDecimal w = new BigDecimal(wString.replace(",", "."));
389
390                 return new PercentType(w);
391             }
392         }
393         LOGGER.warn("Failed to convert '{}' (DPT 251): Pattern does not match or invalid content", value);
394         return null;
395     }
396
397     private static @Nullable Type handleNumericDpt(String id, DPTXlator translator, Class<? extends Type> preferredType)
398             throws KNXFormatException {
399         Set<Class<? extends Type>> allowedTypes = DPTUtil.getAllowedTypes(id);
400
401         double value = translator.getNumericValue();
402         if (allowedTypes.contains(PercentType.class)
403                 && (HSBType.class.equals(preferredType) || PercentType.class.equals(preferredType))) {
404             return new PercentType(BigDecimal.valueOf(Math.round(value)));
405         }
406
407         if (allowedTypes.contains(QuantityType.class) && !disableUoM) {
408             String unit = DPTUnits.getUnitForDpt(id);
409             if (unit != null) {
410                 if (translator instanceof DPTXlator64BitSigned translatorSigned) {
411                     // prevent loss of precision, do not represent 64bit decimal using double
412                     return new QuantityType<>(translatorSigned.getValueSigned() + " " + unit);
413                 }
414                 return new QuantityType<>(value + " " + unit);
415             } else {
416                 LOGGER.trace("Could not determine unit for DPT '{}', fallback to plain decimal", id);
417             }
418         }
419
420         if (allowedTypes.contains(DecimalType.class)) {
421             if (translator instanceof DPTXlator64BitSigned translatorSigned) {
422                 // prevent loss of precision, do not represent 64bit decimal using double
423                 return new DecimalType(translatorSigned.getValueSigned());
424             }
425             return new DecimalType(value);
426         }
427
428         LOGGER.warn("Failed to convert '{}' (DPT '{}'): no matching type found", value, id);
429         return null;
430     }
431
432     private static double coerceToRange(double value, double min, double max) {
433         return Math.min(Math.max(value, min), max);
434     }
435
436     private static int coerceToRange(int value, int min, int max) {
437         return Math.min(Math.max(value, min), max);
438     }
439 }