2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.knx.internal.dpt;
15 import static org.openhab.binding.knx.internal.dpt.DPTUtil.NORMALIZED_DPT;
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;
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;
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.DPTXlator3BitControlled;
49 import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
50 import tuwien.auto.calimero.dptxlator.DPTXlatorDate;
51 import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
52 import tuwien.auto.calimero.dptxlator.DPTXlatorTime;
53 import tuwien.auto.calimero.dptxlator.TranslatorTypes;
56 * This class encodes openHAB data types to strings for sending via Calimero
58 * Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al.
60 * @author Jan N. Klug - Initial contribution
63 public class ValueEncoder {
64 private static final Logger LOGGER = LoggerFactory.getLogger(ValueEncoder.class);
66 private ValueEncoder() {
67 // prevent instantiation
71 * Formats the given value as String for outputting via Calimero.
73 * @param value the value
74 * @param dptId the DPT id to use for formatting the string (e.g. 9.001)
75 * @return the value formatted as String
77 public static @Nullable String encode(Type value, String dptId) {
78 Matcher m = DPTUtil.DPT_PATTERN.matcher(dptId);
79 if (!m.matches() || m.groupCount() != 2) {
80 LOGGER.warn("Couldn't identify main/sub number in dptId '{}'", dptId);
84 String mainNumber = m.group("main");
87 DPTXlator translator = TranslatorTypes.createTranslator(Integer.parseInt(mainNumber),
88 NORMALIZED_DPT.getOrDefault(dptId, dptId));
89 DPT dpt = translator.getType();
91 // check for HSBType first, because it extends PercentType as well
92 if (value instanceof HSBType type) {
93 return handleHSBType(dptId, type);
94 } else if (value instanceof OnOffType) {
95 return OnOffType.OFF == value ? dpt.getLowerValue() : dpt.getUpperValue();
96 } else if (value instanceof UpDownType) {
97 return UpDownType.UP == value ? dpt.getLowerValue() : dpt.getUpperValue();
98 } else if (value instanceof IncreaseDecreaseType) {
99 DPT valueDPT = ((DPTXlator3BitControlled.DPT3BitControlled) dpt).getControlDPT();
100 return IncreaseDecreaseType.DECREASE == value ? valueDPT.getLowerValue() + " 5"
101 : valueDPT.getUpperValue() + " 5";
102 } else if (value instanceof OpenClosedType) {
103 return OpenClosedType.CLOSED == value ? dpt.getLowerValue() : dpt.getUpperValue();
104 } else if (value instanceof StopMoveType) {
105 return StopMoveType.STOP == value ? dpt.getLowerValue() : dpt.getUpperValue();
106 } else if (value instanceof PercentType type) {
107 int intValue = type.intValue();
108 return "251.600".equals(dptId) ? String.format("- - - %d %%", intValue) : String.valueOf(intValue);
109 } else if (value instanceof DecimalType || value instanceof QuantityType<?>) {
110 return handleNumericTypes(dptId, mainNumber, dpt, value);
111 } else if (value instanceof StringType) {
112 if ("243.600".equals(dptId) || "249.600".equals(dptId)) {
113 return value.toString().replace('.', ((DecimalFormat) DecimalFormat.getInstance())
114 .getDecimalFormatSymbols().getDecimalSeparator());
116 return value.toString();
117 } else if (value instanceof DateTimeType type) {
118 return handleDateTimeType(dptId, type);
120 } catch (KNXException e) {
122 } catch (Exception e) {
123 LOGGER.warn("An exception occurred converting value {} to dpt id {}: error message={}", value, dptId,
128 LOGGER.debug("formatAsDPTString: Couldn't convert value {} to dpt id {} (no mapping).", value, dptId);
133 * Formats the given internal <code>dateType</code> to a knx readable String
134 * according to the target datapoint type <code>dpt</code>.
136 * @param value the input value
137 * @param dptId the target datapoint type
139 * @return a String which contains either an ISO8601 formatted date (yyyy-mm-dd),
140 * a formatted 24-hour clock with the day of week prepended (Mon, 12:00:00) or
141 * a formatted 24-hour clock (12:00:00)
143 private static @Nullable String handleDateTimeType(String dptId, DateTimeType value) {
144 if (DPTXlatorDate.DPT_DATE.getID().equals(dptId)) {
145 return value.format("%tF");
146 } else if (DPTXlatorTime.DPT_TIMEOFDAY.getID().equals(dptId)) {
147 return value.format(Locale.US, "%1$ta, %1$tT");
148 } else if (DPTXlatorDateTime.DPT_DATE_TIME.getID().equals(dptId)) {
149 return value.format(Locale.US, "%tF %1$tT");
151 LOGGER.warn("Could not format DateTimeType for datapoint type '{}'", dptId);
155 private static String handleHSBType(String dptId, HSBType hsb) {
158 int[] rgb = ColorUtil.hsbToRgb(hsb);
159 return String.format("r:%d g:%d b:%d", rgb[0], rgb[1], rgb[2]);
161 // MDT specific: mis-use 232.600 for hsv instead of rgb
162 int hue = hsb.getHue().toBigDecimal().multiply(BigDecimal.valueOf(255))
163 .divide(BigDecimal.valueOf(360), 0, RoundingMode.HALF_UP).intValue();
164 return "r:" + hue + " g:" + convertPercentToByte(hsb.getSaturation()) + " b:"
165 + convertPercentToByte(hsb.getBrightness());
167 double[] xyY = ColorUtil.hsbToXY(hsb);
168 return String.format("(%,.4f %,.4f) %,.1f %%", xyY[0], xyY[1], xyY[2] * 100.0);
170 PercentType[] rgbw = ColorUtil.hsbToRgbPercent(hsb);
171 return String.format("%,.1f %,.1f %,.1f - %%", rgbw[0].doubleValue(), rgbw[1].doubleValue(),
172 rgbw[2].doubleValue());
174 return hsb.getHue().toString();
176 return hsb.getBrightness().toString();
180 private static String handleNumericTypes(String dptId, String mainNumber, DPT dpt, Type value) {
181 BigDecimal bigDecimal;
182 if (value instanceof DecimalType decimalType) {
183 bigDecimal = decimalType.toBigDecimal();
185 String unit = DPTUnits.getUnitForDpt(dptId);
187 // exception for DPT using temperature differences
188 // - conversion °C or °F to K is wrong for differences,
189 // - stick to the unit given, fix the scaling for °F
190 // 9.002 DPT_Value_Tempd
191 // 9.003 DPT_Value_Tempa
192 // 9.023 DPT_KelvinPerPercent
193 if (DPTXlator2ByteFloat.DPT_TEMPERATURE_DIFFERENCE.getID().equals(dptId)
194 || DPTXlator2ByteFloat.DPT_TEMPERATURE_GRADIENT.getID().equals(dptId)
195 || DPTXlator2ByteFloat.DPT_KELVIN_PER_PERCENT.getID().equals(dptId)) {
196 // match unicode character or °C
197 if (value.toString().contains(SIUnits.CELSIUS.getSymbol()) || value.toString().contains("°C")) {
199 unit = unit.replace("K", "°C");
201 } else if (value.toString().contains("°F")) {
202 // an new approach to handle temperature differences was introduced to core
203 // after 4.0, stripping the unit and and creating a new QuantityType works
204 // both with core release 4.0 and current snapshot
205 boolean perPercent = value.toString().contains("/%");
206 value = new QuantityType<>(((QuantityType<?>) value).doubleValue() * 5.0 / 9.0, Units.KELVIN);
207 // PercentType needs to be adapted
209 value = ((QuantityType<?>) value).multiply(BigDecimal.valueOf(100));
212 } else if (DPTXlator4ByteFloat.DPT_LIGHT_QUANTITY.getID().equals(dptId)) {
213 if (!value.toString().contains("J")) {
215 unit = unit.replace("J", "lm*s");
218 } else if (DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getID().equals(dptId)) {
219 // use alternate definition of flux
220 if (value.toString().contains("C")) {
226 QuantityType<?> converted = ((QuantityType<?>) value).toUnit(unit);
227 if (converted == null) {
228 LOGGER.warn("Could not convert {} to unit {}, stripping unit only. Check your configuration.",
230 bigDecimal = ((QuantityType<?>) value).toBigDecimal();
232 bigDecimal = converted.toBigDecimal();
235 bigDecimal = ((QuantityType<?>) value).toBigDecimal();
238 switch (mainNumber) {
240 DPT valueDPT = ((DPTXlator1BitControlled.DPT1BitControlled) dpt).getValueDPT();
241 switch (bigDecimal.intValue()) {
243 return "0 " + valueDPT.getLowerValue();
245 return "0 " + valueDPT.getUpperValue();
247 return "1 " + valueDPT.getLowerValue();
249 return "1 " + valueDPT.getUpperValue();
252 int intVal = bigDecimal.intValue();
254 return "learn " + (intVal - 0x80);
256 return "activate " + intVal;
259 return bigDecimal.stripTrailingZeros().toPlainString();
264 * convert 0...100% to 1 byte 0..255
269 private static int convertPercentToByte(PercentType percent) {
270 return percent.toBigDecimal().multiply(BigDecimal.valueOf(255))
271 .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP).intValue();