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.heliosventilation.internal;
15 import org.eclipse.jdt.annotation.NonNullByDefault;
16 import org.eclipse.jdt.annotation.Nullable;
17 import org.openhab.core.library.types.DecimalType;
18 import org.openhab.core.library.types.OnOffType;
19 import org.openhab.core.library.types.QuantityType;
20 import org.openhab.core.library.unit.SIUnits;
21 import org.openhab.core.library.unit.Units;
22 import org.openhab.core.types.State;
23 import org.openhab.core.types.UnDefType;
26 * The {@link HeliosVentilationDataPoint} is a description of a datapoint in the Helios ventilation system.
28 * @author Raphael Mack - Initial contribution
31 public class HeliosVentilationDataPoint {
32 public enum DataType {
43 * mapping from temperature byte values to °C
45 private static final int[] TEMP_MAP = { -74, -70, -66, -62, -59, -56, -54, -52, -50, -48, -47, -46, -44, -43, -42,
46 -41, -40, -39, -38, -37, -36, -35, -34, -33, -33, -32, -31, -30, -30, -29, -28, -28, -27, -27, -26, -25,
47 -25, -24, -24, -23, -23, -22, -22, -21, -21, -20, -20, -19, -19, -19, -18, -18, -17, -17, -16, -16, -16,
48 -15, -15, -14, -14, -14, -13, -13, -12, -12, -12, -11, -11, -11, -10, -10, -9, -9, -9, -8, -8, -8, -7, -7,
49 -7, -6, -6, -6, -5, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 1, 1, 1, 2, 2, 2,
50 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13,
51 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 21, 21, 21, 22, 22, 22,
52 23, 23, 24, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, 34, 34, 35,
53 35, 36, 36, 37, 37, 38, 38, 39, 40, 40, 41, 41, 42, 43, 43, 44, 45, 45, 46, 47, 48, 48, 49, 50, 51, 52, 53,
54 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 65, 66, 68, 69, 71, 73, 75, 77, 79, 81, 82, 86, 90, 93, 97, 100,
55 100, 100, 100, 100, 100, 100, 100, 100 };
58 * mapping from human readable fanspeed to raw value
60 private static final int[] FANSPEED_MAP = { 0, 1, 3, 7, 15, 31, 63, 127, 255 };
62 private static final int BYTE_PERCENT_OFFSET = 52;
65 private boolean writable;
66 private DataType datatype;
69 private int bitLength;
71 private @Nullable HeliosVentilationDataPoint next;
74 * parse fullSpec in the properties format to declare a datapoint
76 * @param name the name of the datapoint
77 * @param fullSpec datapoint specification, see format in datapoints.properties
78 * @throws HeliosPropertiesFormatException in case fullSpec is not parsable
80 public HeliosVentilationDataPoint(String name, String fullSpec) throws HeliosPropertiesFormatException {
81 String specWithoutComment;
82 if (fullSpec.contains("#")) {
83 specWithoutComment = fullSpec.substring(0, fullSpec.indexOf("#"));
85 specWithoutComment = fullSpec;
87 String[] tokens = specWithoutComment.split(",");
89 if (tokens.length != 3) {
90 throw new HeliosPropertiesFormatException("invalid length", name, fullSpec);
93 String addr = tokens[0];
95 if (addr.contains(":")) {
96 addrTokens = addr.split(":");
98 addrTokens = new String[] { addr };
102 this.address = (byte) (int) Integer.decode(addrTokens[0]);
103 if (addrTokens.length > 1) {
104 bitStart = (byte) (int) Integer.decode(addrTokens[1]);
107 if (addrTokens.length > 2) {
108 bitLength = (byte) (int) Integer.decode(addrTokens[2]) - bitStart + 1;
110 if (addrTokens.length > 3) {
111 throw new HeliosPropertiesFormatException(
112 "invalid address spec: too many separators in bit specification", name, fullSpec);
114 } catch (NumberFormatException e) {
115 throw new HeliosPropertiesFormatException("invalid address spec", name, fullSpec);
118 this.writable = Boolean.parseBoolean(tokens[1]);
120 this.datatype = DataType.valueOf(tokens[2].replaceAll("\\s+", ""));
121 } catch (IllegalArgumentException e) {
122 throw new HeliosPropertiesFormatException("invalid type spec", name, fullSpec);
126 public HeliosVentilationDataPoint(String name, byte address, boolean writable, DataType datatype) {
127 this.datatype = datatype;
128 this.writable = writable;
130 this.address = address;
133 public boolean isWritable() {
138 public String toString() {
144 * @return the name of the variable, which is also the channel name
146 public String getName() {
152 * @return address of the variable
154 public byte address() {
159 * @return the bit mask of the data point. 0xFF in case the full byte is used.
161 public byte bitMask() {
162 byte mask = (byte) 0xff;
163 if (datatype == DataType.NUMBER || datatype == DataType.SWITCH) {
164 mask = (byte) (((1 << bitLength) - 1) << bitStart);
170 * interpret the given byte b and return the value as State.
173 * @return state representation of byte value b in current datatype
175 public State asState(byte b) {
179 return new QuantityType<>(TEMP_MAP[val], SIUnits.CELSIUS);
181 return new QuantityType<>((int) ((val - BYTE_PERCENT_OFFSET) * 100.0 / (255 - BYTE_PERCENT_OFFSET)),
184 if (bitLength != 1) {
185 return UnDefType.UNDEF;
186 } else if ((b & (1 << bitStart)) != 0) {
189 return OnOffType.OFF;
192 int value = (b & bitMask()) >> bitStart;
193 return new DecimalType(value);
195 return new QuantityType<>(val, Units.PERCENT);
198 while (i < FANSPEED_MAP.length && FANSPEED_MAP[i] < val) {
201 return new DecimalType(i);
203 return new QuantityType<>(val / 3, SIUnits.CELSIUS);
205 return UnDefType.UNDEF;
210 * interpret the given byte b and return the value as string.
213 * @return sting representation of byte value b in current datatype
215 public String asString(byte b) {
216 State ste = asState(b);
217 String str = ste.toString();
218 if (ste instanceof UnDefType) {
219 return String.format("<unknown type> %02X ", b);
226 * generate byte data to transmit
228 * @param val is the state of a channel
229 * @return byte value with RS485 representation. Bit level values are returned in the correct location, but other
230 * bits/datapoints in the same address are zero.
232 public byte getTransmitDataFor(State val) {
234 DecimalType value = val.as(DecimalType.class);
237 * if value is not convertible to a numeric type we cannot do anything reasonable with it, let's use the
238 * initial value for it
241 QuantityType<?> quantvalue;
244 quantvalue = ((QuantityType<?>) val);
245 quantvalue = quantvalue.toUnit(SIUnits.CELSIUS);
246 if (quantvalue != null) {
247 value = quantvalue.as(DecimalType.class);
249 int temp = (int) Math.round(value.doubleValue());
251 while (i < TEMP_MAP.length && TEMP_MAP[i] < temp) {
259 int i = value.intValue();
265 result = (byte) FANSPEED_MAP[i];
268 result = (byte) ((value.doubleValue() / 100.0) * (255 - BYTE_PERCENT_OFFSET) + BYTE_PERCENT_OFFSET);
271 double d = (Math.round(value.doubleValue()));
274 } else if (d > 100.0) {
280 quantvalue = ((QuantityType<?>) val).toUnit(SIUnits.CELSIUS);
281 if (quantvalue != null) {
282 result = (byte) (quantvalue.intValue() * 3);
287 // those are the types supporting bit level specification
288 // output only the relevant bits
289 result = (byte) (value.intValue() << bitStart);
298 * Get further datapoint linked to the same address.
300 * @return sister datapoint
302 public @Nullable HeliosVentilationDataPoint next() {
307 * Add a next to a datapoint on the same address.
308 * Caller has to ensure that identical datapoints are not added several times.
310 * @param next is the sister datapoint
312 @SuppressWarnings("PMD.CompareObjectsWithEquals")
313 public void append(HeliosVentilationDataPoint next) {
314 HeliosVentilationDataPoint existing = this.next;
316 // this datapoint is already there, so we do nothing and return
318 } else if (existing != null) {
319 existing.append(next);
326 * @return true if writing to this datapoint requires a read-modify-write on the address
328 public boolean requiresReadModifyWrite() {
330 * the address either has multiple datapoints linked to it or is a bit-level point
331 * this means we need to do read-modify-write on udpate and therefore we store the data in memory
333 return (bitMask() != (byte) 0xFF || next != null);