]> git.basschouten.com Git - openhab-addons.git/blob
7a2bc6e654d2c70b84941248b579dbc5ba042e8a
[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.heliosventilation.internal;
14
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;
24
25 /**
26  * The {@link HeliosVentilationDataPoint} is a description of a datapoint in the Helios ventilation system.
27  *
28  * @author Raphael Mack - Initial contribution
29  */
30 @NonNullByDefault
31 public class HeliosVentilationDataPoint {
32     public enum DataType {
33         TEMPERATURE,
34         HYSTERESIS,
35         FANSPEED,
36         SWITCH,
37         BYTE_PERCENT,
38         PERCENT,
39         NUMBER
40     }
41
42     /**
43      * mapping from temperature byte values to °C
44      */
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 };
56
57     /**
58      * mapping from human readable fanspeed to raw value
59      */
60     private static final int[] FANSPEED_MAP = { 0, 1, 3, 7, 15, 31, 63, 127, 255 };
61
62     private static final int BYTE_PERCENT_OFFSET = 52;
63
64     private String name;
65     private boolean writable;
66     private DataType datatype;
67     private byte address;
68     private int bitStart;
69     private int bitLength;
70
71     private @Nullable HeliosVentilationDataPoint next;
72
73     /**
74      * parse fullSpec in the properties format to declare a datapoint
75      *
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
79      */
80     public HeliosVentilationDataPoint(String name, String fullSpec) throws HeliosPropertiesFormatException {
81         String specWithoutComment;
82         if (fullSpec.contains("#")) {
83             specWithoutComment = fullSpec.substring(0, fullSpec.indexOf("#"));
84         } else {
85             specWithoutComment = fullSpec;
86         }
87         String[] tokens = specWithoutComment.split(",");
88         this.name = name;
89         if (tokens.length != 3) {
90             throw new HeliosPropertiesFormatException("invalid length", name, fullSpec);
91         }
92         try {
93             String addr = tokens[0];
94             String[] addrTokens;
95             if (addr.contains(":")) {
96                 addrTokens = addr.split(":");
97             } else {
98                 addrTokens = new String[] { addr };
99             }
100             bitLength = 8;
101             bitStart = 0;
102             this.address = (byte) (int) Integer.decode(addrTokens[0]);
103             if (addrTokens.length > 1) {
104                 bitStart = (byte) (int) Integer.decode(addrTokens[1]);
105                 bitLength = 1;
106             }
107             if (addrTokens.length > 2) {
108                 bitLength = (byte) (int) Integer.decode(addrTokens[2]) - bitStart + 1;
109             }
110             if (addrTokens.length > 3) {
111                 throw new HeliosPropertiesFormatException(
112                         "invalid address spec: too many separators in bit specification", name, fullSpec);
113             }
114         } catch (NumberFormatException e) {
115             throw new HeliosPropertiesFormatException("invalid address spec", name, fullSpec);
116         }
117
118         this.writable = Boolean.parseBoolean(tokens[1]);
119         try {
120             this.datatype = DataType.valueOf(tokens[2].replaceAll("\\s+", ""));
121         } catch (IllegalArgumentException e) {
122             throw new HeliosPropertiesFormatException("invalid type spec", name, fullSpec);
123         }
124     }
125
126     public HeliosVentilationDataPoint(String name, byte address, boolean writable, DataType datatype) {
127         this.datatype = datatype;
128         this.writable = writable;
129         this.name = name;
130         this.address = address;
131     }
132
133     public boolean isWritable() {
134         return writable;
135     }
136
137     @Override
138     public String toString() {
139         return name;
140     }
141
142     /**
143      *
144      * @return the name of the variable, which is also the channel name
145      */
146     public String getName() {
147         return name;
148     }
149
150     /**
151      *
152      * @return address of the variable
153      */
154     public byte address() {
155         return address;
156     }
157
158     /**
159      * @return the bit mask of the data point. 0xFF in case the full byte is used.
160      */
161     public byte bitMask() {
162         byte mask = (byte) 0xff;
163         if (datatype == DataType.NUMBER || datatype == DataType.SWITCH) {
164             mask = (byte) (((1 << bitLength) - 1) << bitStart);
165         }
166         return mask;
167     }
168
169     /**
170      * interpret the given byte b and return the value as State.
171      *
172      * @param b
173      * @return state representation of byte value b in current datatype
174      */
175     public State asState(byte b) {
176         int val = b & 0xff;
177         switch (datatype) {
178             case TEMPERATURE:
179                 return new QuantityType<>(TEMP_MAP[val], SIUnits.CELSIUS);
180             case BYTE_PERCENT:
181                 return new QuantityType<>((int) ((val - BYTE_PERCENT_OFFSET) * 100.0 / (255 - BYTE_PERCENT_OFFSET)),
182                         Units.PERCENT);
183             case SWITCH:
184                 if (bitLength != 1) {
185                     return UnDefType.UNDEF;
186                 } else if ((b & (1 << bitStart)) != 0) {
187                     return OnOffType.ON;
188                 } else {
189                     return OnOffType.OFF;
190                 }
191             case NUMBER:
192                 int value = (b & bitMask()) >> bitStart;
193                 return new DecimalType(value);
194             case PERCENT:
195                 return new QuantityType<>(val, Units.PERCENT);
196             case FANSPEED:
197                 int i = 1;
198                 while (i < FANSPEED_MAP.length && FANSPEED_MAP[i] < val) {
199                     i++;
200                 }
201                 return new DecimalType(i);
202             case HYSTERESIS:
203                 return new QuantityType<>(val / 3, SIUnits.CELSIUS);
204             default:
205                 return UnDefType.UNDEF;
206         }
207     }
208
209     /**
210      * interpret the given byte b and return the value as string.
211      *
212      * @param b
213      * @return sting representation of byte value b in current datatype
214      */
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);
220         } else {
221             return str;
222         }
223     }
224
225     /**
226      * generate byte data to transmit
227      *
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.
231      */
232     public byte getTransmitDataFor(State val) {
233         byte result = 0;
234         DecimalType value = val.as(DecimalType.class);
235         if (value == null) {
236             /*
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
239              */
240         } else {
241             QuantityType<?> quantvalue;
242             switch (datatype) {
243                 case TEMPERATURE:
244                     quantvalue = ((QuantityType<?>) val);
245                     quantvalue = quantvalue.toUnit(SIUnits.CELSIUS);
246                     if (quantvalue != null) {
247                         value = quantvalue.as(DecimalType.class);
248                         if (value != null) {
249                             int temp = (int) Math.round(value.doubleValue());
250                             int i = 0;
251                             while (i < TEMP_MAP.length && TEMP_MAP[i] < temp) {
252                                 i++;
253                             }
254                             result = (byte) i;
255                         }
256                     }
257                     break;
258                 case FANSPEED:
259                     int i = value.intValue();
260                     if (i < 0) {
261                         i = 0;
262                     } else if (i > 8) {
263                         i = 8;
264                     }
265                     result = (byte) FANSPEED_MAP[i];
266                     break;
267                 case BYTE_PERCENT:
268                     result = (byte) ((value.doubleValue() / 100.0) * (255 - BYTE_PERCENT_OFFSET) + BYTE_PERCENT_OFFSET);
269                     break;
270                 case PERCENT:
271                     double d = (Math.round(value.doubleValue()));
272                     if (d < 0.0) {
273                         d = 0.0;
274                     } else if (d > 100.0) {
275                         d = 100.0;
276                     }
277                     result = (byte) d;
278                     break;
279                 case HYSTERESIS:
280                     quantvalue = ((QuantityType<?>) val).toUnit(SIUnits.CELSIUS);
281                     if (quantvalue != null) {
282                         result = (byte) (quantvalue.intValue() * 3);
283                     }
284                     break;
285                 case SWITCH:
286                 case NUMBER:
287                     // those are the types supporting bit level specification
288                     // output only the relevant bits
289                     result = (byte) (value.intValue() << bitStart);
290                     break;
291             }
292         }
293
294         return result;
295     }
296
297     /**
298      * Get further datapoint linked to the same address.
299      *
300      * @return sister datapoint
301      */
302     public @Nullable HeliosVentilationDataPoint next() {
303         return next;
304     }
305
306     /**
307      * Add a next to a datapoint on the same address.
308      * Caller has to ensure that identical datapoints are not added several times.
309      *
310      * @param next is the sister datapoint
311      */
312     @SuppressWarnings("PMD.CompareObjectsWithEquals")
313     public void append(HeliosVentilationDataPoint next) {
314         HeliosVentilationDataPoint existing = this.next;
315         if (this == next) {
316             // this datapoint is already there, so we do nothing and return
317             return;
318         } else if (existing != null) {
319             existing.append(next);
320         } else {
321             this.next = next;
322         }
323     }
324
325     /**
326      * @return true if writing to this datapoint requires a read-modify-write on the address
327      */
328     public boolean requiresReadModifyWrite() {
329         /*
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
332          */
333         return (bitMask() != (byte) 0xFF || next != null);
334     }
335 }