2 * Copyright (c) 2010-2020 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.openthermgateway.internal;
15 import java.util.regex.Pattern;
17 import org.eclipse.jdt.annotation.NonNullByDefault;
18 import org.eclipse.jdt.annotation.Nullable;
21 * The {@link Message} represent a single message received from the OpenTherm Gateway.
23 * @author Arjen Korevaar - Initial contribution
26 public class Message {
28 private static final Pattern messagePattern = Pattern.compile("[TBRA]{1}[A-F0-9]{8}");
31 * The code field is not part of OpenTherm specification, but added by OpenTherm Gateway.
32 * It can be any of the following:
34 * T: Message received from the thermostat
35 * B: Message received from the boiler
36 * R: Request sent to the boiler
37 * A: Response returned to the thermostat
38 * E: Parity or stop bit error
41 private MessageType messageType;
45 public String getCode() {
49 public MessageType getMessageType() {
57 public @Nullable String getData(ByteType byteType) {
58 if (this.data.length() == 4) {
61 return this.data.substring(0, 2);
63 return this.data.substring(2, 4);
72 public boolean getBit(ByteType byteType, int pos) {
74 String data = getData(byteType);
77 // First parse the hex value to an integer
78 int parsed = Integer.parseInt(data, 16);
80 // Then right shift it pos positions so that the required bit is at the front
81 // and then apply a bitmask of 00000001 (1)
82 return ((parsed >> pos) & 1) == 1;
88 public int getUInt(ByteType byteType) {
90 String data = getData(byteType);
93 return Integer.parseInt(data, 16);
99 public int getInt(ByteType byteType) {
101 String data = getData(byteType);
104 return parseSignedInteger(data);
110 public float getFloat() {
111 // f8.8, two's complement
113 String data = getData(ByteType.BOTH);
116 long value = Long.parseLong(data, 16);
118 // left padded with zeros
119 String binary = String.format("%16s", Long.toBinaryString(value)).replace(' ', '0');
121 if (binary.charAt(0) == '1') {
124 String inverted = invertBinary(binary);
126 value = Long.parseLong(inverted, 2);
131 // divide by 2^8 = 256
132 return (float) value / 256;
138 public boolean overrides(@Nullable Message other) {
139 // If the message is a Request sent to the boiler or an Answer returned to the
140 // thermostat, and it's ID is equal to the previous message, then this is an
141 // override sent by the OpenTherm Gateway
142 return other != null && this.getID() == other.getID()
143 && ("R".equals(this.getCode()) || "A".equals(this.getCode()));
147 public String toString() {
148 return String.format("%s - %s - %s", this.code, this.id, this.data);
151 public Message(String code, MessageType messageType, int id, String data) {
153 this.messageType = messageType;
158 public static @Nullable Message parse(String message) {
159 if (messagePattern.matcher(message).matches()) {
160 // For now, only parse TBRA codes
161 String code = message.substring(0, 1);
162 MessageType messageType = getMessageType(message.substring(1, 3));
163 int id = Integer.valueOf(message.substring(3, 5), 16);
164 String data = message.substring(5);
166 return new Message(code, messageType, id, data);
172 private static MessageType getMessageType(String value) {
173 // First parse the hex value to an integer
174 int integer = Integer.parseInt(value, 16);
176 // Then right shift it 4 bits so that the message type bits are at the front
177 int shifted = integer >> 4;
179 // Then mask it with 00000111 (7), so that we only get the first 3 bits,
180 // effectively cutting off the parity bit.
181 int cutoff = shifted & 7;
185 return MessageType.READDATA;
187 return MessageType.WRITEDATA;
189 return MessageType.INVALIDDATA;
191 return MessageType.RESERVED;
193 return MessageType.READACK;
195 return MessageType.WRITEACK;
197 return MessageType.DATAINVALID;
200 return MessageType.UNKNOWNDATAID;
204 private int parseSignedInteger(String data) {
205 // First parse the hex value to an unsigned integer value
206 int result = Integer.parseInt(data, 16);
208 if (data.length() == 4) {
209 // This is a two byte value, apply a bitmask of 01111111 11111111 (32767) to cut
211 result = result & 32767;
213 // Then apply a bitmask of 10000000 00000000 (32768) to check the sign bit
214 if ((result & 32768) == 32768) {
215 // If the sign is 1000000 00000000 (32768) then it's a negative
216 result = -32768 + result;
219 // This is a one byte value, apply a bitmask of 01111111 (127), to cut off the
221 result = result & 127;
223 // Then apply a bitmask of 10000000 (128) to check the sign bit
224 if ((result & 128) == 128) {
225 // If the sign is 1000000 (128) then it's a negative
226 result = -128 + result;
233 private String invertBinary(String value) {
234 // There is probably a better solution, but for now this works
235 String result = value;
237 result = result.replace('1', 'X');
238 result = result.replace('0', '1');
239 result = result.replace('X', '0');