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.max.internal.message;
15 import static org.openhab.binding.max.internal.MaxBindingConstants.*;
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;
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;
36 * The {@link CMessage} contains configuration about a MAX! device.
38 * @author Andreas Heil - Initial contribution
39 * @author Marcel Verpaalen - Detailed parsing, OH2 Update
42 public final class CMessage extends Message {
44 private final Logger logger = LoggerFactory.getLogger(CMessage.class);
46 private String rfAddress;
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 = "";
65 private Map<String, Object> properties = new HashMap<>();
67 public CMessage(String raw) {
69 String[] tokens = this.getPayload().split(Message.DELIMETER);
71 rfAddress = tokens[0];
73 byte[] bytes = Base64.getDecoder().decode(tokens[1].getBytes(StandardCharsets.UTF_8));
75 int[] data = new int[bytes.length];
77 for (int i = 0; i < bytes.length; i++) {
78 data[i] = bytes[i] & 0xFF;
82 if (length != data.length - 1) {
83 logger.debug("C Message malformed: wrong data length. Expected bytes {}, actual bytes {}", length,
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());
93 deviceType = DeviceType.create(data[4]);
94 roomId = data[5] & 0xFF;
96 serialNumber = getSerialNumber(bytes);
97 if (deviceType == DeviceType.HeatingThermostatPlus || deviceType == DeviceType.HeatingThermostat
98 || deviceType == DeviceType.WallMountedThermostat) {
99 parseHeatingThermostatData(bytes);
102 if (deviceType == DeviceType.Cube) {
103 parseCubeData(bytes);
105 if (deviceType == DeviceType.EcoSwitch || deviceType == DeviceType.ShutterContact) {
106 logger.trace("Device {} type {} Data: '{}'", rfAddress, deviceType, parseData(bytes));
110 private String getSerialNumber(byte[] bytes) {
111 byte[] sn = new byte[10];
113 for (int i = 0; i < 10; i++) {
114 sn[i] = bytes[i + 8];
117 return new String(sn, StandardCharsets.UTF_8);
120 private String parseData(byte[] bytes) {
121 if (bytes.length <= 18) {
126 byte[] sn = new byte[bytes.length - dataStart];
128 for (int i = 0; i < sn.length; i++) {
129 sn[i] = bytes[i + dataStart];
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);
140 private void parseCubeData(byte[] bytes) {
141 if (bytes.length != 238) {
142 logger.debug("Unexpected lenght for Cube message {}, expected: 238", bytes.length);
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());
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
160 properties.put("Summer Time", parseTimeInfo(Arrays.copyOfRange(bytes, 0xE7, 0xEF), "Summer").toString()); // Date
163 } catch (Exception e) {
164 logger.debug("Exception occurred during parsing: {}", e.getMessage(), e);
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();
181 private void parseHeatingThermostatData(byte[] bytes) {
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));
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));
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++) {
228 programData += "\r\n Day " + Integer.toString((ln / 13) % 7) + ": ";
229 startTime = "00:00h";
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 ";
240 } catch (Exception e) {
241 logger.debug("Exception occurred during heater data: {}", e.getMessage(), e);
246 public String getSerialNumber() {
251 public MessageType getType() {
252 return MessageType.C;
255 public String getRFAddress() {
259 public DeviceType getDeviceType() {
263 public Map<String, Object> getProperties() {
267 public int getRoomID() {
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));
284 logger.debug("{}: {}", key, properties.get(key));
287 if (programData != null) {
288 logger.trace("ProgramData: {}", programData);