]> git.basschouten.com Git - openhab-addons.git/blob
b94f0ac166487448dc0787d56895191599d4221f
[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.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.DPTXlator3BitControlled;
52 import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
53 import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
54 import tuwien.auto.calimero.dptxlator.DPTXlatorSceneControl;
55 import tuwien.auto.calimero.dptxlator.TranslatorTypes;
56
57 /**
58  * This class decodes raw data received from the KNX bus to an openHAB datatype
59  *
60  * Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al.
61  *
62  * @author Jan N. Klug - Initial contribution
63  */
64 @NonNullByDefault
65 public class ValueDecoder {
66     private static final Logger LOGGER = LoggerFactory.getLogger(ValueDecoder.class);
67
68     private static final String TIME_DAY_FORMAT = "EEE, HH:mm:ss";
69     private static final String TIME_FORMAT = "HH:mm:ss";
70     private static final String DATE_FORMAT = "yyyy-MM-dd";
71     // RGB: "r:123 g:123 b:123" value-range: 0-255
72     private static final Pattern RGB_PATTERN = Pattern.compile("r:(?<r>\\d+) g:(?<g>\\d+) b:(?<b>\\d+)");
73     // RGBW: "100 27 25 12 %", value range: 0-100, invalid values: "-"
74     private static final Pattern RGBW_PATTERN = Pattern
75             .compile("(?:(?<r>[\\d,.]+)|-)\\s(?:(?<g>[\\d,.]+)|-)\\s(?:(?<b>[\\d,.]+)|-)\\s(?:(?<w>[\\d,.]+)|-)\\s%");
76     // xyY: "(0,123 0,123) 56 %", value range 0-1 for xy (comma or point as decimal point), 0-100 for Y, invalid values
77     // omitted
78     public static final Pattern XYY_PATTERN = Pattern
79             .compile("(?:\\((?<x>\\d+(?:[,.]\\d+)?) (?<y>\\d+(?:[,.]\\d+)?)\\))?\\s*(?:(?<Y>\\d+(?:[,.]\\d+)?)\\s%)?");
80
81     /**
82      * convert the raw value received to the corresponding openHAB value
83      *
84      * @param dptId the DPT of the given data
85      * @param data a byte array containing the value
86      * @param preferredType the preferred datatype for this conversion
87      * @return the data converted to an openHAB Type (or null if conversion failed)
88      */
89     public static @Nullable Type decode(String dptId, byte[] data, Class<? extends Type> preferredType) {
90         try {
91             DPTXlator translator = TranslatorTypes.createTranslator(0,
92                     DPTUtil.NORMALIZED_DPT.getOrDefault(dptId, dptId));
93             translator.setData(data);
94             String value = translator.getValue();
95
96             String id = dptId; // prefer using the user-supplied DPT
97
98             Matcher m = DPTUtil.DPT_PATTERN.matcher(id);
99             if (!m.matches() || m.groupCount() != 2) {
100                 LOGGER.trace("User-Supplied DPT '{}' did not match for sub-type, using DPT returned from Translator",
101                         id);
102                 id = translator.getType().getID();
103                 m = DPTUtil.DPT_PATTERN.matcher(id);
104                 if (!m.matches() || m.groupCount() != 2) {
105                     LOGGER.warn("Couldn't identify main/sub number in dptID '{}'", id);
106                     return null;
107                 }
108             }
109             LOGGER.trace("Finally using datapoint DPT = {}", id);
110
111             String mainType = m.group("main");
112             String subType = m.group("sub");
113
114             switch (mainType) {
115                 case "1":
116                     return handleDpt1(subType, translator);
117                 case "2":
118                     DPTXlator1BitControlled translator1BitControlled = (DPTXlator1BitControlled) translator;
119                     int decValue = (translator1BitControlled.getControlBit() ? 2 : 0)
120                             + (translator1BitControlled.getValueBit() ? 1 : 0);
121                     return new DecimalType(decValue);
122                 case "3":
123                     return handleDpt3(subType, translator);
124                 case "10":
125                     return handleDpt10(value);
126                 case "11":
127                     return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN)
128                             .format(new SimpleDateFormat(DATE_FORMAT).parse(value)));
129                 case "18":
130                     DPTXlatorSceneControl translatorSceneControl = (DPTXlatorSceneControl) translator;
131                     int decimalValue = translatorSceneControl.getSceneNumber();
132                     if (value.startsWith("learn")) {
133                         decimalValue += 0x80;
134                     }
135                     return new DecimalType(decimalValue);
136                 case "19":
137                     return handleDpt19(translator);
138                 case "16":
139                 case "20":
140                 case "21":
141                 case "22":
142                 case "28":
143                     return StringType.valueOf(value);
144                 case "232":
145                     return handleDpt232(value, subType);
146                 case "242":
147                     return handleDpt242(value);
148                 case "251":
149                     return handleDpt251(value, preferredType);
150                 default:
151                     return handleNumericDpt(id, translator, preferredType);
152             }
153         } catch (NumberFormatException | KNXFormatException | KNXIllegalArgumentException | ParseException e) {
154             LOGGER.info("Translator couldn't parse data '{}' for datapoint type '{}' ({}).", data, dptId, e.getClass());
155         } catch (KNXException e) {
156             LOGGER.warn("Failed creating a translator for datapoint type '{}'.", dptId, e);
157         }
158
159         return null;
160     }
161
162     private static Type handleDpt1(String subType, DPTXlator translator) {
163         DPTXlatorBoolean translatorBoolean = (DPTXlatorBoolean) translator;
164         switch (subType) {
165             case "008":
166                 return translatorBoolean.getValueBoolean() ? UpDownType.DOWN : UpDownType.UP;
167             case "009":
168             case "019":
169                 // This is wrong for DPT 1.009. It should be true -> CLOSE, false -> OPEN, but unfortunately
170                 // can't be fixed without breaking a lot of working installations.
171                 // The documentation has been updated to reflect that. / @J-N-K
172                 return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
173             case "010":
174                 return translatorBoolean.getValueBoolean() ? StopMoveType.MOVE : StopMoveType.STOP;
175             case "022":
176                 return DecimalType.valueOf(translatorBoolean.getValueBoolean() ? "1" : "0");
177             default:
178                 return OnOffType.from(translatorBoolean.getValueBoolean());
179         }
180     }
181
182     private static @Nullable Type handleDpt3(String subType, DPTXlator translator) {
183         DPTXlator3BitControlled translator3BitControlled = (DPTXlator3BitControlled) translator;
184         if (translator3BitControlled.getStepCode() == 0) {
185             LOGGER.debug("convertRawDataToType: KNX DPT_Control_Dimming: break received.");
186             return UnDefType.NULL;
187         }
188         switch (subType) {
189             case "007":
190                 return translator3BitControlled.getControlBit() ? IncreaseDecreaseType.INCREASE
191                         : IncreaseDecreaseType.DECREASE;
192             case "008":
193                 return translator3BitControlled.getControlBit() ? UpDownType.DOWN : UpDownType.UP;
194             default:
195                 LOGGER.warn("DPT3, subtype '{}' is unknown.", subType);
196                 return null;
197         }
198     }
199
200     private static Type handleDpt10(String value) throws ParseException {
201         if (value.contains("no-day")) {
202             /*
203              * KNX "no-day" needs special treatment since openHAB's DateTimeType doesn't support "no-day".
204              * Workaround: remove the "no-day" String, parse the remaining time string, which will result in a
205              * date of "1970-01-01".
206              * Replace "no-day" with the current day name
207              */
208             StringBuilder stb = new StringBuilder(value);
209             int start = stb.indexOf("no-day");
210             int end = start + "no-day".length();
211             stb.replace(start, end, String.format(Locale.US, "%1$ta", Calendar.getInstance()));
212             value = stb.toString();
213         }
214         Date date = null;
215         try {
216             date = new SimpleDateFormat(TIME_DAY_FORMAT, Locale.US).parse(value);
217         } catch (ParseException pe) {
218             date = new SimpleDateFormat(TIME_FORMAT, Locale.US).parse(value);
219             throw pe;
220         }
221         return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(date));
222     }
223
224     private static @Nullable Type handleDpt19(DPTXlator translator) throws KNXFormatException {
225         DPTXlatorDateTime translatorDateTime = (DPTXlatorDateTime) translator;
226         if (translatorDateTime.isFaultyClock()) {
227             // Not supported: faulty clock
228             LOGGER.debug("KNX clock msg ignored: clock faulty bit set, which is not supported");
229             return null;
230         } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
231                 && translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) {
232             // Not supported: "/1/1" (month and day without year)
233             LOGGER.debug("KNX clock msg ignored: no year, but day and month, which is not supported");
234             return null;
235         } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
236                 && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) {
237             // Not supported: "1900" (year without month and day)
238             LOGGER.debug("KNX clock msg ignored: no day and month, but year, which is not supported");
239             return null;
240         } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
241                 && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)
242                 && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
243             // Not supported: No year, no date and no time
244             LOGGER.debug("KNX clock msg ignored: no day and month or year, which is not supported");
245             return null;
246         }
247
248         Calendar cal = Calendar.getInstance();
249         if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
250                 && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
251             // Pure date format, no time information
252             cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
253             String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
254             return DateTimeType.valueOf(value);
255         } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
256                 && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
257             // Pure time format, no date information
258             cal.clear();
259             cal.set(Calendar.HOUR_OF_DAY, translatorDateTime.getHour());
260             cal.set(Calendar.MINUTE, translatorDateTime.getMinute());
261             cal.set(Calendar.SECOND, translatorDateTime.getSecond());
262             String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
263             return DateTimeType.valueOf(value);
264         } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
265                 && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
266             // Date format and time information
267             cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
268             String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
269             return DateTimeType.valueOf(value);
270         } else {
271             LOGGER.warn("Failed to convert '{}'", translator.getValue());
272             return null;
273         }
274     }
275
276     private static @Nullable Type handleDpt232(String value, String subType) {
277         Matcher rgb = RGB_PATTERN.matcher(value);
278         if (rgb.matches()) {
279             int r = Integer.parseInt(rgb.group("r"));
280             int g = Integer.parseInt(rgb.group("g"));
281             int b = Integer.parseInt(rgb.group("b"));
282
283             switch (subType) {
284                 case "600":
285                     return HSBType.fromRGB(r, g, b);
286                 case "60000":
287                     // MDT specific: mis-use 232.600 for hsv instead of rgb
288                     DecimalType hue = new DecimalType(coerceToRange(r * 360.0 / 255.0, 0.0, 359.9999));
289                     PercentType sat = new PercentType(BigDecimal.valueOf(coerceToRange(g / 2.55, 0.0, 100.0)));
290                     PercentType bright = new PercentType(BigDecimal.valueOf(coerceToRange(b / 2.55, 0.0, 100.0)));
291                     return new HSBType(hue, sat, bright);
292                 default:
293                     LOGGER.warn("Unknown subtype '232.{}', no conversion possible.", subType);
294                     return null;
295             }
296         }
297         LOGGER.warn("Failed to convert '{}' (DPT 232): Pattern does not match", value);
298         return null;
299     }
300
301     private static @Nullable Type handleDpt242(String value) {
302         Matcher xyY = XYY_PATTERN.matcher(value);
303         if (xyY.matches()) {
304             String stringx = xyY.group("x");
305             String stringy = xyY.group("y");
306             String stringY = xyY.group("Y");
307
308             if (stringx != null && stringy != null) {
309                 double x = Double.parseDouble(stringx.replace(",", "."));
310                 double y = Double.parseDouble(stringy.replace(",", "."));
311                 if (stringY == null) {
312                     return ColorUtil.xyToHsb(new double[] { x, y });
313                 } else {
314                     double pY = Double.parseDouble(stringY.replace(",", "."));
315                     return ColorUtil.xyToHsb(new double[] { x, y, pY / 100.0 });
316                 }
317             }
318         }
319         LOGGER.warn("Failed to convert '{}' (DPT 242): Pattern does not match", value);
320         return null;
321     }
322
323     private static @Nullable Type handleDpt251(String value, Class<? extends Type> preferredType) {
324         Matcher rgbw = RGBW_PATTERN.matcher(value);
325         if (rgbw.matches()) {
326             String rString = rgbw.group("r");
327             String gString = rgbw.group("g");
328             String bString = rgbw.group("b");
329             String wString = rgbw.group("w");
330
331             if (rString != null && gString != null && bString != null && HSBType.class.equals(preferredType)) {
332                 // does not support PercentType and r,g,b valid -> HSBType
333                 int r = coerceToRange((int) (Double.parseDouble(rString.replace(",", ".")) * 2.55), 0, 255);
334                 int g = coerceToRange((int) (Double.parseDouble(gString.replace(",", ".")) * 2.55), 0, 255);
335                 int b = coerceToRange((int) (Double.parseDouble(bString.replace(",", ".")) * 2.55), 0, 255);
336
337                 return HSBType.fromRGB(r, g, b);
338             } else if (wString != null && PercentType.class.equals(preferredType)) {
339                 // does support PercentType and w valid -> PercentType
340                 BigDecimal w = new BigDecimal(wString.replace(",", "."));
341
342                 return new PercentType(w);
343             }
344         }
345         LOGGER.warn("Failed to convert '{}' (DPT 251): Pattern does not match or invalid content", value);
346         return null;
347     }
348
349     private static @Nullable Type handleNumericDpt(String id, DPTXlator translator, Class<? extends Type> preferredType)
350             throws KNXFormatException {
351         Set<Class<? extends Type>> allowedTypes = DPTUtil.getAllowedTypes(id);
352
353         double value = translator.getNumericValue();
354         if (allowedTypes.contains(PercentType.class)
355                 && (HSBType.class.equals(preferredType) || PercentType.class.equals(preferredType))) {
356             return new PercentType(BigDecimal.valueOf(Math.round(value)));
357         }
358
359         if (allowedTypes.contains(QuantityType.class) && !disableUoM) {
360             String unit = DPTUnits.getUnitForDpt(id);
361             if (unit != null) {
362                 return new QuantityType<>(value + " " + unit);
363             } else {
364                 LOGGER.trace("Could not determine unit for DPT '{}', fallback to plain decimal", id);
365             }
366         }
367
368         if (allowedTypes.contains(DecimalType.class)) {
369             return new DecimalType(value);
370         }
371
372         LOGGER.warn("Failed to convert '{}' (DPT '{}'): no matching type found", value, id);
373         return null;
374     }
375
376     private static double coerceToRange(double value, double min, double max) {
377         return Math.min(Math.max(value, min), max);
378     }
379
380     private static int coerceToRange(int value, int min, int max) {
381         return Math.min(Math.max(value, min), max);
382     }
383 }