ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
hon_parser.h
Go to the documentation of this file.
1#pragma once
2
3#include <array>
4#include <cstddef>
5#include <cstdint>
6
8
10
11// A logical frame, after the FF FF separator and the FF 55 escapes are removed:
12// [length][flags][5 reserved bytes][type][payload...][checksum][crc high][crc low]
13// The length counts every byte before the checksum; the CRC is only present when its flag is set.
14static constexpr uint8_t FRAME_FLAGS_OFFSET = 1;
15static constexpr uint8_t FRAME_TYPE_OFFSET = 7;
16static constexpr uint8_t FRAME_PAYLOAD_OFFSET = 8;
17static constexpr uint8_t FRAME_FLAG_CRC = 0x40;
18static constexpr uint8_t FRAME_FLAG_ENCRYPTED = 0x80;
19static constexpr uint8_t MIN_FRAME_LENGTH = 8;
20static constexpr uint8_t MAX_FRAME_LENGTH = 0xF1;
21// The longest frame that is decoded is the 61-byte status report. Longer frames are still checked
22// in full, but only their head is kept.
23static constexpr size_t MAX_STORED_FRAME = 64;
24static constexpr size_t MAX_PAYLOAD = 4;
25static constexpr size_t MAX_WIRE_FRAME = 28; // 2 separators, up to 12 escaped bytes, an escaped checksum
26
27enum class FrameType : uint8_t {
28 FRAME_TYPE_CONTROL = 0x01,
29 FRAME_TYPE_STATUS = 0x02,
30 FRAME_TYPE_INVALID = 0x03, // the appliance rejected the last request
31 FRAME_TYPE_CONFIRM = 0x05,
32 FRAME_TYPE_REPORT = 0x06, // an unsolicited status, which expects a CONFIRM back
34 FRAME_TYPE_VERSION = 0x62,
39};
40
41// Bounded receive parser. The checksum and CRC are kept running while the bytes arrive, so a frame
42// longer than the stored head is validated without being stored.
44 public:
45 void reset() {
46 this->size_ = 0;
47 this->separators_ = 0;
48 this->escape_ = false;
49 this->complete_ = false;
50 this->checksum_ = 0;
51 this->crc_ = 0;
52 }
53 bool complete() const { return this->complete_; }
55 const uint8_t *data() const { return this->buffer_.data(); }
57 size_t size() const { return this->size_; }
58 size_t stored_size() const { return std::min<size_t>(this->size_, MAX_STORED_FRAME); }
59
61 bool feed(uint8_t value) {
62 if (this->complete_) {
63 this->reset();
64 }
65 if (this->separators_ < 2) {
66 this->separators_ = value == 0xFF ? this->separators_ + 1 : 0;
67 return false;
68 }
69 if (this->escape_) {
70 this->escape_ = false;
71 if (value == 0xFF) {
72 // A new unescaped separator pair: start over without losing synchronisation.
73 this->size_ = 0;
74 this->checksum_ = 0;
75 this->crc_ = 0;
76 return false;
77 }
78 if (value != 0x55) {
79 this->reset();
80 return false;
81 }
82 value = 0xFF;
83 } else if (value == 0xFF) {
84 this->escape_ = true;
85 return false;
86 }
87 if (this->size_ == 0 && (value < MIN_FRAME_LENGTH || value > MAX_FRAME_LENGTH)) {
88 this->reset();
89 return false;
90 }
91 const uint8_t index = this->size_++;
92 if (index < MAX_STORED_FRAME) {
93 this->buffer_[index] = value;
94 }
95 const uint8_t length = this->buffer_[0];
96 if (index < length) {
97 this->checksum_ += value;
98 if (value == 0xFF) {
99 this->checksum_ += 0x55; // The escape byte counts towards the checksum.
100 }
101 this->crc_ = crc16(&value, 1, this->crc_, 0xA001, false, false);
102 } else if (index == length) {
103 this->received_checksum_ = value;
104 } else if (index == length + 1) {
105 this->received_crc_ = static_cast<uint16_t>(value) << 8;
106 } else {
107 this->received_crc_ |= value;
108 }
109 if (this->size_ < 2) {
110 return false;
111 }
112 const bool crc = (this->buffer_[FRAME_FLAGS_OFFSET] & FRAME_FLAG_CRC) != 0;
113 if (this->size_ != length + 1 + (crc ? 2 : 0)) {
114 return false;
115 }
116 this->complete_ = true;
117 if ((this->buffer_[FRAME_FLAGS_OFFSET] & ~FRAME_FLAG_CRC) != 0 || this->checksum_ != this->received_checksum_) {
118 return false;
119 }
120 return !crc || this->crc_ == this->received_crc_;
121 }
122
123 protected:
124 std::array<uint8_t, MAX_STORED_FRAME> buffer_{};
125 uint16_t crc_{0};
126 uint16_t received_crc_{0};
127 uint8_t size_{0};
128 uint8_t separators_{0};
129 uint8_t checksum_{0};
131 bool escape_{false};
132 bool complete_{false};
133};
134
137inline size_t encode_frame(FrameType type, const uint8_t *payload, size_t size, uint8_t *wire) {
138 if (size > MAX_PAYLOAD) {
139 return 0;
140 }
141 uint8_t core[FRAME_PAYLOAD_OFFSET + MAX_PAYLOAD] = {static_cast<uint8_t>(FRAME_PAYLOAD_OFFSET + size)};
142 core[FRAME_TYPE_OFFSET] = static_cast<uint8_t>(type);
143 for (size_t i = 0; i < size; i++) {
144 core[FRAME_PAYLOAD_OFFSET + i] = payload[i];
145 }
146 size_t count = 0;
147 wire[count++] = 0xFF;
148 wire[count++] = 0xFF;
149 uint8_t checksum = 0;
150 for (size_t i = 0; i < FRAME_PAYLOAD_OFFSET + size; i++) {
151 wire[count++] = core[i];
152 checksum += core[i];
153 if (core[i] == 0xFF) {
154 wire[count++] = 0x55;
155 checksum += 0x55;
156 }
157 }
158 wire[count++] = checksum;
159 if (checksum == 0xFF) {
160 wire[count++] = 0x55;
161 }
162 return count;
163}
164
166
167// The appliance reports and takes target temperatures with this offset.
168static constexpr uint8_t TARGET_TEMPERATURE_OFFSET = 30;
169static constexpr uint8_t MAX_REPORTED_TEMPERATURE = 100;
170
181
182// The mode is spread over three flag bytes; exactly one of the three indications is set in a known mode.
183inline HonMode decode_mode(const uint8_t *payload) {
184 const bool normal = (payload[38] & 0x10) && (payload[39] & 0x10);
185 const bool eco = (payload[41] & 0x04) != 0;
186 const bool bps = (payload[40] & 0x04) != 0;
187 if (unsigned(normal) + unsigned(eco) + unsigned(bps) != 1) {
189 }
190 if (normal) {
192 }
194}
195
196static constexpr uint8_t STATUS_FRAME_LENGTH = 60;
197
202inline bool decode_status(const uint8_t *frame, size_t size, Status &status) {
203 if (size <= STATUS_FRAME_LENGTH || frame[0] != STATUS_FRAME_LENGTH) {
204 return false;
205 }
206 const auto type = static_cast<FrameType>(frame[FRAME_TYPE_OFFSET]);
208 return false;
209 }
210 const uint8_t *payload = frame + FRAME_PAYLOAD_OFFSET;
211 if (payload[0] != 0x6D || payload[1] != 0x01) {
212 return false;
213 }
214 status.current_temperature = payload[2];
215 status.target_temperature = payload[3] + TARGET_TEMPERATURE_OFFSET;
216 status.estimated_heating_time = payload[7];
217 status.secondary_temperature = payload[15];
218 status.powered = (payload[39] & 0x01) != 0;
219 status.heating = (payload[41] & 0x01) != 0;
220 status.mode = decode_mode(payload);
221 status.available_water = payload[45];
222 return payload[2] <= MAX_REPORTED_TEMPERATURE && payload[3] <= MAX_REPORTED_TEMPERATURE - TARGET_TEMPERATURE_OFFSET;
223}
224
225// A control write: one parameter of the 0x5D group and its value.
226enum class Parameter : uint8_t {
227 PARAMETER_POWER = 0x01,
228 PARAMETER_TARGET_TEMPERATURE = 0x02, // value is the temperature minus TARGET_TEMPERATURE_OFFSET
229 PARAMETER_ECO = 0x05, // 1 selects ECO, 0 selects Normal
230 PARAMETER_BPS = 0x17,
231};
232static constexpr uint8_t CONTROL_GROUP = 0x5D;
233
234struct Command {
236 uint8_t value;
237};
238
240inline void encode_control(const Command &command, uint8_t *payload) {
241 payload[0] = CONTROL_GROUP;
242 payload[1] = static_cast<uint8_t>(command.parameter);
243 payload[2] = 0x00;
244 payload[3] = command.value;
245}
246
248inline bool command_confirmed(const Command &command, const Status &status) {
249 switch (command.parameter) {
251 return status.powered == (command.value != 0);
253 return status.target_temperature == command.value + TARGET_TEMPERATURE_OFFSET;
255 return status.mode == (command.value != 0 ? HonMode::HON_MODE_ECO : HonMode::HON_MODE_NORMAL);
257 return status.mode == HonMode::HON_MODE_BPS;
258 }
259 return false;
260}
261
262} // namespace esphome::haier_water_heater::water_heater_protocol
uint8_t checksum
Definition bl0906.h:3
uint8_t status
Definition bl0942.h:8
bool feed(uint8_t value)
Returns true once a valid frame is complete. A complete but invalid frame leaves complete() set.
Definition hon_parser.h:61
size_t size() const
Logical bytes received, including the checksum and CRC; may exceed MAX_STORED_FRAME.
Definition hon_parser.h:57
const uint8_t * data() const
The stored head of the frame, at most MAX_STORED_FRAME bytes; valid until the next feed or reset.
Definition hon_parser.h:55
uint8_t type
unsigned count
Definition gif.cpp:51
bool decode_status(const uint8_t *frame, size_t size, Status &status)
Decodes a status report or a polled status; false for any other frame or an implausible one.
Definition hon_parser.h:202
bool command_confirmed(const Command &command, const Status &status)
True when a status shows that the command took effect.
Definition hon_parser.h:248
HonMode decode_mode(const uint8_t *payload)
Definition hon_parser.h:183
size_t encode_frame(FrameType type, const uint8_t *payload, size_t size, uint8_t *wire)
Writes a clear-text frame with up to MAX_PAYLOAD bytes of payload into wire, which must hold MAX_WIRE...
Definition hon_parser.h:137
void encode_control(const Command &command, uint8_t *payload)
Fills the four-byte control payload for a command.
Definition hon_parser.h:240
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
Definition helpers.cpp:95
uint16_t size
Definition helpers.cpp:34
uint16_t length
Definition tt21100.cpp:0