]> git.basschouten.com Git - openhab-addons.git/blob
98fa645a7e6eba8e5cef4047c16cb500d5ab91f3
[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.max.internal.message;
14
15 import static org.openhab.binding.max.internal.MaxBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.math.BigInteger;
19 import java.math.RoundingMode;
20 import java.nio.charset.StandardCharsets;
21 import java.util.Arrays;
22 import java.util.Base64;
23 import java.util.Calendar;
24 import java.util.Date;
25 import java.util.HashMap;
26 import java.util.Map;
27
28 import org.apache.commons.lang3.StringUtils;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.openhab.binding.max.internal.Utils;
31 import org.openhab.binding.max.internal.device.DeviceType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * The {@link CMessage} contains configuration about a MAX! device.
37  *
38  * @author Andreas Heil - Initial contribution
39  * @author Marcel Verpaalen - Detailed parsing, OH2 Update
40  */
41 @NonNullByDefault
42 public final class CMessage extends Message {
43
44     private final Logger logger = LoggerFactory.getLogger(CMessage.class);
45
46     private String rfAddress;
47     private int length;
48     private DeviceType deviceType;
49     private int roomId = -1;
50     private String serialNumber;
51     private BigDecimal tempComfort = BigDecimal.ZERO;
52     private BigDecimal tempEco = BigDecimal.ZERO;
53     private BigDecimal tempSetpointMax = BigDecimal.ZERO;
54     private BigDecimal tempSetpointMin = BigDecimal.ZERO;
55     private BigDecimal tempOffset = BigDecimal.ZERO;
56     private BigDecimal tempOpenWindow = BigDecimal.ZERO;
57     private BigDecimal durationOpenWindow = BigDecimal.ZERO;
58     private BigDecimal decalcification = BigDecimal.ZERO;
59     private BigDecimal valveMaximum = BigDecimal.ZERO;
60     private BigDecimal valveOffset = BigDecimal.ZERO;
61     private BigDecimal boostDuration = BigDecimal.ZERO;
62     private BigDecimal boostValve = BigDecimal.ZERO;
63     private String programData = "";
64
65     private Map<String, Object> properties = new HashMap<>();
66
67     public CMessage(String raw) {
68         super(raw);
69         String[] tokens = this.getPayload().split(Message.DELIMETER);
70
71         rfAddress = tokens[0];
72
73         byte[] bytes = Base64.getDecoder().decode(tokens[1].getBytes(StandardCharsets.UTF_8));
74
75         int[] data = new int[bytes.length];
76
77         for (int i = 0; i < bytes.length; i++) {
78             data[i] = bytes[i] & 0xFF;
79         }
80
81         length = data[0];
82         if (length != data.length - 1) {
83             logger.debug("C Message malformed: wrong data length. Expected bytes {}, actual bytes {}", length,
84                     data.length - 1);
85         }
86
87         String rfAddress2 = Utils.toHex(data[1], data[2], data[3]);
88         if (!rfAddress.toUpperCase().equals(rfAddress2.toUpperCase())) {
89             logger.debug("C Message malformed: wrong RF address. Expected address {}, actual address {}",
90                     rfAddress.toUpperCase(), rfAddress2.toUpperCase());
91         }
92
93         deviceType = DeviceType.create(data[4]);
94         roomId = data[5] & 0xFF;
95
96         serialNumber = getSerialNumber(bytes);
97         if (deviceType == DeviceType.HeatingThermostatPlus || deviceType == DeviceType.HeatingThermostat
98                 || deviceType == DeviceType.WallMountedThermostat) {
99             parseHeatingThermostatData(bytes);
100         }
101
102         if (deviceType == DeviceType.Cube) {
103             parseCubeData(bytes);
104         }
105         if (deviceType == DeviceType.EcoSwitch || deviceType == DeviceType.ShutterContact) {
106             logger.trace("Device {} type {} Data: '{}'", rfAddress, deviceType, parseData(bytes));
107         }
108     }
109
110     private String getSerialNumber(byte[] bytes) {
111         byte[] sn = new byte[10];
112
113         for (int i = 0; i < 10; i++) {
114             sn[i] = bytes[i + 8];
115         }
116
117         return new String(sn, StandardCharsets.UTF_8);
118     }
119
120     private String parseData(byte[] bytes) {
121         if (bytes.length <= 18) {
122             return "";
123         }
124         try {
125             int dataStart = 18;
126             byte[] sn = new byte[bytes.length - dataStart];
127
128             for (int i = 0; i < sn.length; i++) {
129                 sn[i] = bytes[i + dataStart];
130             }
131             logger.trace("DataBytes: {}", Utils.getHex(sn));
132             return new String(sn, StandardCharsets.UTF_8);
133         } catch (Exception e) {
134             logger.debug("Exception occurred during parsing: {}", e.getMessage(), e);
135         }
136
137         return "";
138     }
139
140     private void parseCubeData(byte[] bytes) {
141         if (bytes.length != 238) {
142             logger.debug("Unexpected lenght for Cube message {}, expected: 238", bytes.length);
143         }
144         try {
145             properties.put("Portal Enabled", Integer.toString(bytes[0x18] & 0xFF));
146             properties.put("Portal URL",
147                     new String(Arrays.copyOfRange(bytes, 0x55, 0xD5), StandardCharsets.UTF_8).trim());
148             properties.put("TimeZone (Winter)",
149                     new String(Arrays.copyOfRange(bytes, 0xD6, 0xDA), StandardCharsets.UTF_8).trim());
150             properties.put("TimeZone (Daylight)",
151                     new String(Arrays.copyOfRange(bytes, 0x00E2, 0x00E6), StandardCharsets.UTF_8).trim());
152
153             properties.put("Unknown1", Utils.getHex(Arrays.copyOfRange(bytes, 0x13, 0x33))); // Pushbutton Up config
154                                                                                              // 0=auto, 1=eco, 2=comfort
155             properties.put("Unknown2", Utils.getHex(Arrays.copyOfRange(bytes, 0x34, 0x54))); // Pushbutton down config
156                                                                                              // 0=auto, 1=eco, 2=comfort
157             properties.put("Winter Time", parseTimeInfo(Arrays.copyOfRange(bytes, 0xDB, 0xE2), "Winter").toString()); // Date
158                                                                                                                       // of
159                                                                                                                       // wintertime
160             properties.put("Summer Time", parseTimeInfo(Arrays.copyOfRange(bytes, 0xE7, 0xEF), "Summer").toString()); // Date
161                                                                                                                       // of
162                                                                                                                       // summertime
163         } catch (Exception e) {
164             logger.debug("Exception occurred during parsing: {}", e.getMessage(), e);
165         }
166     }
167
168     private Date parseTimeInfo(byte[] bytes, String suffix) {
169         int month = bytes[0] & 0xFF;
170         int weekDay = bytes[1] & 0xFF;
171         int hour = bytes[2] & 0xFF;
172         int utcOffset = new BigInteger(Arrays.copyOfRange(bytes, 0x03, 0x07)).intValue();
173         properties.put("Utc Offset" + " (" + suffix + ")", utcOffset);
174         Calendar pCal = Calendar.getInstance();
175         pCal.set(Calendar.getInstance().get(Calendar.YEAR), month - 1, 15, hour, 0, 0);
176         pCal.set(Calendar.DAY_OF_WEEK, weekDay + 1);
177         pCal.set(Calendar.DAY_OF_WEEK_IN_MONTH, -1);
178         return pCal.getTime();
179     }
180
181     private void parseHeatingThermostatData(byte[] bytes) {
182         try {
183             int plusDataStart = 18;
184             int programDataStart = 11;
185             tempComfort = new BigDecimal((bytes[plusDataStart] & 0xFF) / 2D);
186             tempEco = new BigDecimal((bytes[plusDataStart + 1] & 0xFF) / 2D);
187             tempSetpointMax = new BigDecimal((bytes[plusDataStart + 2] & 0xFF) / 2D);
188             tempSetpointMin = new BigDecimal((bytes[plusDataStart + 3] & 0xFF) / 2D);
189             properties.put(PROPERTY_THERMO_COMFORT_TEMP, tempComfort.setScale(1, RoundingMode.HALF_DOWN));
190             properties.put(PROPERTY_THERMO_ECO_TEMP, tempEco.setScale(1, RoundingMode.HALF_DOWN));
191             properties.put(PROPERTY_THERMO_MAX_TEMP_SETPOINT, tempSetpointMax.setScale(1, RoundingMode.HALF_DOWN));
192             properties.put(PROPERTY_THERMO_MIN_TEMP_SETPOINT, tempSetpointMin.setScale(1, RoundingMode.HALF_DOWN));
193             if (bytes.length < 211) {
194                 // Device is a WallMountedThermostat
195                 programDataStart = 4;
196                 logger.trace("WallThermostat byte {}: {}", bytes.length - 3,
197                         Float.toString(bytes[bytes.length - 3] & 0xFF));
198                 logger.trace("WallThermostat byte {}: {}", bytes.length - 2,
199                         Float.toString(bytes[bytes.length - 2] & 0xFF));
200                 logger.trace("WallThermostat byte {}: {}", bytes.length - 1,
201                         Float.toString(bytes[bytes.length - 1] & 0xFF));
202             } else {
203                 // Device is a HeatingThermostat(+)
204                 tempOffset = new BigDecimal((bytes[plusDataStart + 4] & 0xFF) / 2D - 3.5);
205                 tempOpenWindow = new BigDecimal((bytes[plusDataStart + 5] & 0xFF) / 2D);
206                 durationOpenWindow = new BigDecimal((bytes[plusDataStart + 6] & 0xFF) * 5);
207                 boostDuration = new BigDecimal(bytes[plusDataStart + 7] & 0xFF >> 5);
208                 boostValve = new BigDecimal((bytes[plusDataStart + 7] & 0x1F) * 5);
209                 decalcification = new BigDecimal(bytes[plusDataStart + 8]);
210                 valveMaximum = new BigDecimal((bytes[plusDataStart + 9] & 0xFF) * 100 / 255);
211                 valveOffset = new BigDecimal((bytes[plusDataStart + 10] & 0xFF) * 100 / 255);
212                 properties.put(PROPERTY_THERMO_OFFSET_TEMP, tempOffset.setScale(1, RoundingMode.HALF_DOWN));
213                 properties.put(PROPERTY_THERMO_WINDOW_OPEN_TEMP, tempOpenWindow.setScale(1, RoundingMode.HALF_DOWN));
214                 properties.put(PROPERTY_THERMO_WINDOW_OPEN_DURATION,
215                         durationOpenWindow.setScale(0, RoundingMode.HALF_DOWN));
216                 properties.put(PROPERTY_THERMO_BOOST_DURATION, boostDuration.setScale(0, RoundingMode.HALF_DOWN));
217                 properties.put(PROPERTY_THERMO_BOOST_VALVEPOS, boostValve.setScale(0, RoundingMode.HALF_DOWN));
218                 properties.put(PROPERTY_THERMO_DECALCIFICATION, decalcification.setScale(0, RoundingMode.HALF_DOWN));
219                 properties.put(PROPERTY_THERMO_VALVE_MAX, valveMaximum.setScale(0, RoundingMode.HALF_DOWN));
220                 properties.put(PROPERTY_THERMO_VALVE_OFFSET, valveOffset.setScale(0, RoundingMode.HALF_DOWN));
221             }
222             programData = "";
223             int ln = 13 * 6; // first day = Sat
224             String startTime = "00:00h";
225             for (int charIdx = plusDataStart + programDataStart; charIdx < (plusDataStart + programDataStart
226                     + 26 * 7); charIdx++) {
227                 if (ln % 13 == 0) {
228                     programData += "\r\n Day " + Integer.toString((ln / 13) % 7) + ": ";
229                     startTime = "00:00h";
230                 }
231                 int progTime = (bytes[charIdx + 1] & 0xFF) * 5 + (bytes[charIdx] & 0x01) * 1280;
232                 int progMinutes = progTime % 60;
233                 int progHours = (progTime - progMinutes) / 60;
234                 String endTime = Integer.toString(progHours) + ":" + String.format("%02d", progMinutes) + "h";
235                 programData += startTime + "-" + endTime + " " + Double.toString(bytes[charIdx] / 4) + "C  ";
236                 startTime = endTime;
237                 charIdx++;
238                 ln++;
239             }
240         } catch (Exception e) {
241             logger.debug("Exception occurred during heater data: {}", e.getMessage(), e);
242         }
243         return;
244     }
245
246     public String getSerialNumber() {
247         return serialNumber;
248     }
249
250     @Override
251     public MessageType getType() {
252         return MessageType.C;
253     }
254
255     public String getRFAddress() {
256         return rfAddress;
257     }
258
259     public DeviceType getDeviceType() {
260         return deviceType;
261     }
262
263     public Map<String, Object> getProperties() {
264         return properties;
265     }
266
267     public int getRoomID() {
268         return roomId;
269     }
270
271     @Override
272     public void debug(Logger logger) {
273         logger.debug("=== C Message === ");
274         logger.trace("\tRAW:                    {}", this.getPayload());
275         logger.debug("DeviceType:               {}", deviceType);
276         logger.debug("SerialNumber:             {}", serialNumber);
277         logger.debug("RFAddress:                {}", rfAddress);
278         logger.debug("RoomID:                   {}", roomId);
279         for (String key : properties.keySet()) {
280             if (!key.startsWith("Unknown")) {
281                 String propertyName = String.join(" ", StringUtils.splitByCharacterTypeCamelCase(key));
282                 logger.debug("{}: {}", propertyName, properties.get(key));
283             } else {
284                 logger.debug("{}: {}", key, properties.get(key));
285             }
286         }
287         if (programData != null) {
288             logger.trace("ProgramData:          {}", programData);
289         }
290     }
291 }