]> git.basschouten.com Git - openhab-addons.git/blob
232d4b8524247a42e2d516050a6aec65b303bcc0
[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.dpt.DPTUtil.NORMALIZED_DPT;
16
17 import java.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.text.DecimalFormat;
20 import java.util.Locale;
21 import java.util.regex.Matcher;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.library.types.DateTimeType;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.HSBType;
28 import org.openhab.core.library.types.IncreaseDecreaseType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.OpenClosedType;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.library.types.QuantityType;
33 import org.openhab.core.library.types.StopMoveType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.library.types.UpDownType;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.library.unit.Units;
38 import org.openhab.core.types.Type;
39 import org.openhab.core.util.ColorUtil;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import tuwien.auto.calimero.KNXException;
44 import tuwien.auto.calimero.dptxlator.DPT;
45 import tuwien.auto.calimero.dptxlator.DPTXlator;
46 import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled;
47 import tuwien.auto.calimero.dptxlator.DPTXlator2ByteFloat;
48 import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned;
49 import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
50 import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
51 import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
52 import tuwien.auto.calimero.dptxlator.DPTXlatorDate;
53 import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
54 import tuwien.auto.calimero.dptxlator.DPTXlatorTime;
55 import tuwien.auto.calimero.dptxlator.TranslatorTypes;
56
57 /**
58  * This class encodes openHAB data types to strings for sending via Calimero
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 ValueEncoder {
66     private static final Logger LOGGER = LoggerFactory.getLogger(ValueEncoder.class);
67
68     private ValueEncoder() {
69         // prevent instantiation
70     }
71
72     /**
73      * Formats the given value as String for outputting via Calimero.
74      *
75      * @param value the value
76      * @param dptId the DPT id to use for formatting the string (e.g. 9.001)
77      * @return the value formatted as String
78      */
79     public static @Nullable String encode(Type value, String dptId) {
80         Matcher m = DPTUtil.DPT_PATTERN.matcher(dptId);
81         if (!m.matches() || m.groupCount() != 2) {
82             LOGGER.warn("Couldn't identify main/sub number in dptId '{}'", dptId);
83             return null;
84         }
85
86         String mainNumber = m.group("main");
87
88         try {
89             DPTXlator translator = TranslatorTypes.createTranslator(Integer.parseInt(mainNumber),
90                     NORMALIZED_DPT.getOrDefault(dptId, dptId));
91             DPT dpt = translator.getType();
92
93             // check for HSBType first, because it extends PercentType as well
94             if (value instanceof HSBType type) {
95                 return handleHSBType(dptId, type);
96             } else if (value instanceof OnOffType) {
97                 return OnOffType.OFF == value ? dpt.getLowerValue() : dpt.getUpperValue();
98             } else if (value instanceof UpDownType) {
99                 return UpDownType.UP == value ? dpt.getLowerValue() : dpt.getUpperValue();
100             } else if (value instanceof IncreaseDecreaseType) {
101                 DPT valueDPT = ((DPTXlator3BitControlled.DPT3BitControlled) dpt).getControlDPT();
102                 return IncreaseDecreaseType.DECREASE == value ? valueDPT.getLowerValue() + " 5"
103                         : valueDPT.getUpperValue() + " 5";
104             } else if (value instanceof OpenClosedType) {
105                 return OpenClosedType.CLOSED == value ? dpt.getLowerValue() : dpt.getUpperValue();
106             } else if (value instanceof StopMoveType) {
107                 return StopMoveType.STOP == value ? dpt.getLowerValue() : dpt.getUpperValue();
108             } else if (value instanceof PercentType type) {
109                 int intValue = type.intValue();
110                 return "251.600".equals(dptId) ? String.format("- - - %d %%", intValue) : String.valueOf(intValue);
111             } else if (value instanceof DecimalType || value instanceof QuantityType<?>) {
112                 return handleNumericTypes(dptId, mainNumber, dpt, value);
113             } else if (value instanceof StringType) {
114                 if ("243.600".equals(dptId) || "249.600".equals(dptId)) {
115                     return value.toString().replace('.', ((DecimalFormat) DecimalFormat.getInstance())
116                             .getDecimalFormatSymbols().getDecimalSeparator());
117                 }
118                 return value.toString();
119             } else if (value instanceof DateTimeType type) {
120                 return handleDateTimeType(dptId, type);
121             }
122         } catch (KNXException e) {
123             return null;
124         } catch (Exception e) {
125             LOGGER.warn("An exception occurred converting value {} to dpt id {}: error message={}", value, dptId,
126                     e.getMessage());
127             return null;
128         }
129
130         LOGGER.debug("formatAsDPTString: Couldn't convert value {} to dpt id {} (no mapping).", value, dptId);
131         return null;
132     }
133
134     /**
135      * Formats the given internal <code>dateType</code> to a knx readable String
136      * according to the target datapoint type <code>dpt</code>.
137      *
138      * @param value the input value
139      * @param dptId the target datapoint type
140      *
141      * @return a String which contains either an ISO8601 formatted date (yyyy-mm-dd),
142      *         a formatted 24-hour clock with the day of week prepended (Mon, 12:00:00) or
143      *         a formatted 24-hour clock (12:00:00)
144      */
145     private static @Nullable String handleDateTimeType(String dptId, DateTimeType value) {
146         if (DPTXlatorDate.DPT_DATE.getID().equals(dptId)) {
147             return value.format("%tF");
148         } else if (DPTXlatorTime.DPT_TIMEOFDAY.getID().equals(dptId)) {
149             return value.format(Locale.US, "%1$ta, %1$tT");
150         } else if (DPTXlatorDateTime.DPT_DATE_TIME.getID().equals(dptId)) {
151             return value.format(Locale.US, "%tF %1$tT");
152         }
153         LOGGER.warn("Could not format DateTimeType for datapoint type '{}'", dptId);
154         return null;
155     }
156
157     private static String handleHSBType(String dptId, HSBType hsb) {
158         switch (dptId) {
159             case "232.600":
160                 int[] rgb = ColorUtil.hsbToRgb(hsb);
161                 return String.format("r:%d g:%d b:%d", rgb[0], rgb[1], rgb[2]);
162             case "232.60000":
163                 // MDT specific: mis-use 232.600 for hsv instead of rgb
164                 int hue = hsb.getHue().toBigDecimal().multiply(BigDecimal.valueOf(255))
165                         .divide(BigDecimal.valueOf(360), 0, RoundingMode.HALF_UP).intValue();
166                 return "r:" + hue + " g:" + convertPercentToByte(hsb.getSaturation()) + " b:"
167                         + convertPercentToByte(hsb.getBrightness());
168             case "242.600":
169                 double[] xyY = ColorUtil.hsbToXY(hsb);
170                 return String.format("(%,.4f %,.4f) %,.1f %%", xyY[0], xyY[1], xyY[2] * 100.0);
171             case "251.600":
172                 PercentType[] rgbw = ColorUtil.hsbToRgbPercent(hsb);
173                 return String.format("%,.1f %,.1f %,.1f - %%", rgbw[0].doubleValue(), rgbw[1].doubleValue(),
174                         rgbw[2].doubleValue());
175             case "251.60600":
176                 PercentType[] rgbw2 = ColorUtil.hsbToRgbwPercent(hsb);
177                 return String.format("%,.1f %,.1f %,.1f %,.1f %%", rgbw2[0].doubleValue(), rgbw2[1].doubleValue(),
178                         rgbw2[2].doubleValue(), rgbw2[3].doubleValue());
179             case "5.003":
180                 return hsb.getHue().toString();
181             default:
182                 return hsb.getBrightness().toString();
183         }
184     }
185
186     private static String handleNumericTypes(String dptId, String mainNumber, DPT dpt, Type value) {
187         BigDecimal bigDecimal;
188         if (value instanceof DecimalType decimalType) {
189             bigDecimal = decimalType.toBigDecimal();
190         } else {
191             String unit = DPTUnits.getUnitForDpt(dptId);
192
193             // exception for DPT using temperature differences
194             // - conversion °C or °F to K is wrong for differences,
195             // - stick to the unit given, fix the scaling for °F
196             // 9.002 DPT_Value_Tempd
197             // 9.003 DPT_Value_Tempa
198             // 9.023 DPT_KelvinPerPercent
199             if (DPTXlator2ByteFloat.DPT_TEMPERATURE_DIFFERENCE.getID().equals(dptId)
200                     || DPTXlator2ByteFloat.DPT_TEMPERATURE_GRADIENT.getID().equals(dptId)
201                     || DPTXlator2ByteFloat.DPT_KELVIN_PER_PERCENT.getID().equals(dptId)) {
202                 // match unicode character or °C
203                 if (value.toString().contains(SIUnits.CELSIUS.getSymbol()) || value.toString().contains("°C")) {
204                     if (unit != null) {
205                         unit = unit.replace("K", "°C");
206                     }
207                 } else if (value.toString().contains("°F")) {
208                     // an new approach to handle temperature differences was introduced to core
209                     // after 4.0, stripping the unit and and creating a new QuantityType works
210                     // both with core release 4.0 and current snapshot
211                     boolean perPercent = value.toString().contains("/%");
212                     value = new QuantityType<>(((QuantityType<?>) value).doubleValue() * 5.0 / 9.0, Units.KELVIN);
213                     // PercentType needs to be adapted
214                     if (perPercent) {
215                         value = ((QuantityType<?>) value).multiply(BigDecimal.valueOf(100));
216                     }
217                 }
218             } else if (DPTXlator4ByteFloat.DPT_LIGHT_QUANTITY.getID().equals(dptId)) {
219                 if (!value.toString().contains("J")) {
220                     if (unit != null) {
221                         unit = unit.replace("J", "lm*s");
222                     }
223                 }
224             } else if (DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getID().equals(dptId)) {
225                 // use alternate definition of flux
226                 if (value.toString().contains("C")) {
227                     unit = "C";
228                 }
229             }
230
231             if (unit != null) {
232                 QuantityType<?> converted = ((QuantityType<?>) value).toUnit(unit);
233                 if (converted == null) {
234                     LOGGER.warn("Could not convert {} to unit {}, stripping unit only. Check your configuration.",
235                             value, unit);
236                     bigDecimal = ((QuantityType<?>) value).toBigDecimal();
237                 } else {
238                     bigDecimal = converted.toBigDecimal();
239                 }
240             } else {
241                 bigDecimal = ((QuantityType<?>) value).toBigDecimal();
242             }
243         }
244         switch (mainNumber) {
245             case "1":
246                 if (DPTXlatorBoolean.DPT_SCENE_AB.getID().equals(dptId)) {
247                     return (bigDecimal.intValue() == 0) ? dpt.getLowerValue() : dpt.getUpperValue();
248                 }
249                 return bigDecimal.stripTrailingZeros().toPlainString();
250             case "2":
251                 DPT valueDPT = ((DPTXlator1BitControlled.DPT1BitControlled) dpt).getValueDPT();
252                 switch (bigDecimal.intValue()) {
253                     case 0:
254                         return "0 " + valueDPT.getLowerValue();
255                     case 1:
256                         return "0 " + valueDPT.getUpperValue();
257                     case 2:
258                         return "1 " + valueDPT.getLowerValue();
259                     default:
260                         return "1 " + valueDPT.getUpperValue();
261                 }
262             case "7":
263                 if (DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_10.getID().equals(dptId)
264                         || DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_100.getID().equals(dptId)) {
265                     return bigDecimal.divide(BigDecimal.valueOf(1000)).stripTrailingZeros().toPlainString().replace('.',
266                             ((DecimalFormat) DecimalFormat.getInstance()).getDecimalFormatSymbols()
267                                     .getDecimalSeparator());
268                 }
269                 return bigDecimal.stripTrailingZeros().toPlainString();
270             case "18":
271                 int intVal = bigDecimal.intValue();
272                 if (intVal > 63) {
273                     return "learn " + (intVal - 0x80);
274                 } else {
275                     return "activate " + intVal;
276                 }
277             case "8":
278                 if ("8.010".equals(dptId)) {
279                     // 8.010 has a resolution of 0.01 and will be scaled. Calimero expects locale-specific separator.
280                     return bigDecimal.stripTrailingZeros().toPlainString().replace('.',
281                             ((DecimalFormat) DecimalFormat.getInstance()).getDecimalFormatSymbols()
282                                     .getDecimalSeparator());
283                 }
284                 // fallthrough
285             default:
286                 return bigDecimal.stripTrailingZeros().toPlainString();
287         }
288     }
289
290     /**
291      * convert 0...100% to 1 byte 0..255
292      *
293      * @param percent
294      * @return int 0..255
295      */
296     private static int convertPercentToByte(PercentType percent) {
297         return percent.toBigDecimal().multiply(BigDecimal.valueOf(255))
298                 .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP).intValue();
299     }
300 }