ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ufm01.cpp
Go to the documentation of this file.
1#include "ufm01.h"
4#include "esphome/core/log.h"
5
6#include <algorithm>
7#include <array>
8#include <cinttypes>
9#include <cstring>
10
11namespace esphome::ufm01 {
12
13ESPHOME_LOG_TAG(TAG, "ufm01");
14
15static constexpr uint8_t COMMAND_ACK = 0xE5;
16static constexpr uint32_t COMMAND_ACK_TIMEOUT_MS = 500;
17static constexpr uint32_t CLEAR_QUEUE_TIMEOUT_MS = 15000;
18static constexpr uint32_t STARTUP_DELAY_MS = 2000;
19static constexpr uint32_t POST_RESET_DELAY_MS = 2000;
20static constexpr uint32_t RESET_RETRY_DELAY_MS = 800;
21static constexpr uint32_t STARTUP_RETRY_MS = 3000;
22static constexpr uint32_t PASSIVE_POLL_INTERVAL_MS = 1000;
23static constexpr uint32_t ACTIVE_STALE_MS = 5000;
24static constexpr uint32_t PASSIVE_READ_TIMEOUT_MS = 1000;
25static constexpr uint32_t SOFTWARE_VERSION_READ_TIMEOUT_MS = 2000;
26static constexpr uint32_t SOFTWARE_VERSION_RETRY_MS = 30000;
27static constexpr uint8_t SOFTWARE_VERSION_MAX_ATTEMPTS = 3;
28static constexpr uint32_t ACTIVE_FRAME_TIMEOUT_MS = 3000;
29// After this many consecutive passive poll failures, re-run the reset/startup sequence
30static constexpr uint8_t PASSIVE_FAIL_ESCALATE_COUNT = 8;
31
32static constexpr float L_PER_M3 = 1000.0f;
33static constexpr float M3_PER_L = 1.0f / L_PER_M3;
34
35static constexpr std::array<uint8_t, 7> ACTIVE_MODE = {0xFE, 0xFE, 0x11, 0x5C, 0x00, 0x5C, 0x16};
36static constexpr std::array<uint8_t, 7> PASSIVE_MODE = {0xFE, 0xFE, 0x11, 0x5C, 0x01, 0x5D, 0x16};
37static constexpr std::array<uint8_t, 7> CLEAR_ACCUMULATED_FLOW = {0xFE, 0xFE, 0x11, 0x5A, 0xFD, 0x57, 0x16};
38static constexpr std::array<uint8_t, 7> RESET_DEVICE = {0xFE, 0xFE, 0x11, 0x5D, 0xCB, 0x28, 0x16};
39static constexpr std::array<uint8_t, 7> READ_SENSOR_DATA_NO_ID = {0xFE, 0xFE, 0x11, 0x5B, 0x0F, 0x6A, 0x16};
40static constexpr std::array<uint8_t, 7> READ_SENSOR_DATA_WITH_ID = {0xFE, 0xFE, 0x11, 0x5B, 0xCB, 0x26, 0x16};
41static constexpr std::array<uint8_t, 7> GET_SOFTWARE_VERSION = {0xFE, 0xFE, 0x11, 0x5E, 0x62, 0xC0, 0x16};
42
43// Active-mode frame layout (datasheet Table 7)
44static constexpr size_t FRAME_DEVICE_ID_INDEX = 2;
45static constexpr size_t FRAME_CHECKSUM_INDEX = 30;
46static constexpr size_t FRAME_STOP_INDEX = 31;
47static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
48static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
49static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
50static constexpr uint8_t PASSIVE_START_BYTE_2_WITH_ID = 0x96;
51static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
52static constexpr size_t PASSIVE_WITH_ID_DEVICE_ID_INDEX = 2;
53static constexpr uint8_t PASSIVE_WITH_ID_ACC_FLOW_FLAG_INDEX = 8;
54static constexpr uint8_t PASSIVE_WITH_ID_INSTANT_FLOW_FLAG_INDEX = 22;
55static constexpr uint8_t PASSIVE_WITH_ID_TEMP_FLAG_INDEX = 31;
56static constexpr uint8_t PASSIVE_WITH_ID_ST1_INDEX = 35;
57static constexpr size_t PASSIVE_WITH_ID_CHECKSUM_INDEX = 37;
58static constexpr size_t PASSIVE_WITH_ID_STOP_INDEX = 38;
59static constexpr uint8_t FRAME_INDEX_INSTANT_FLOW_FLAG = 15;
60static constexpr uint8_t FRAME_INDEX_RESERVED_SECTION = 21;
61static constexpr uint8_t FRAME_INDEX_TEMP_FLAG = 24;
62static constexpr uint8_t FRAME_FLAG_INSTANT_FLOW = 0x0B;
63static constexpr uint8_t FRAME_FLAG_RESERVED_SECTION = 0x0C;
64static constexpr uint8_t FRAME_FLAG_TEMP = 0x0D;
65
66// Measurement decoding
67static constexpr uint8_t FRAME_ACC_FLOW_FLAG_INDEX = 8;
68static constexpr uint8_t ACC_FLOW_M3_FLAG = 0x1A;
69static constexpr uint8_t FRAME_FLOW_SIGN_INDEX = 20;
70static constexpr uint8_t FLOW_NEGATIVE_SIGN = 0x80;
71
72// Status bytes (datasheet ST1 / ST2)
73static constexpr uint8_t FRAME_ST1_INDEX = 28;
74static constexpr uint8_t FRAME_ST2_INDEX = 29;
75static constexpr uint8_t ST1_EMPTY_TUBE_MASK = 0x20;
76static constexpr uint8_t ST2_UFC_ERROR_MASK = 0x20;
77static constexpr uint8_t ST2_FLOW_DIRECTION_WRONG_MASK = 0x08;
78static constexpr uint8_t ST2_FLOW_RATE_OUT_OF_RANGE_MASK = 0x04;
79
80static_assert(SOFTWARE_VERSION_RESPONSE_SIZE <= PASSIVE_FRAME_MAX_SIZE, "software version read reuses passive_frame_");
81static constexpr size_t SOFTWARE_VERSION_CHECKSUM_INDEX = 5;
82static constexpr size_t SOFTWARE_VERSION_STOP_INDEX = 6;
83
84static float to_float(uint8_t data) { return (data >> 4) * 10 + (data & 0x0F); }
85
86static bool bcd_to_digits(const uint8_t *data, size_t len, char *out) {
87 bool all_zero = true;
88 for (size_t i = 0; i < len; ++i) {
89 const uint8_t lo = data[i] & 0x0F;
90 const uint8_t hi = data[i] >> 4;
91 if (lo > 9 || hi > 9)
92 return false;
93 const size_t pos = (len - 1 - i) * 2;
94 if (hi != 0)
95 all_zero = false;
96 out[pos] = static_cast<char>('0' + hi);
97 if (lo != 0)
98 all_zero = false;
99 out[pos + 1] = static_cast<char>('0' + lo);
100 }
101 out[len * 2] = '\0';
102 return !all_zero;
103}
104
105static bool validate_software_version_response(const uint8_t data[SOFTWARE_VERSION_RESPONSE_SIZE]) {
106 if (data[0] != COMMAND_ACK || data[SOFTWARE_VERSION_STOP_INDEX] != FRAME_STOP_BYTE)
107 return false;
108 uint8_t sum = 0;
109 for (size_t i = 1; i < SOFTWARE_VERSION_CHECKSUM_INDEX; ++i)
110 sum += data[i];
111 return data[SOFTWARE_VERSION_CHECKSUM_INDEX] == sum;
112}
113
114static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t expected, const char *name) {
115 if (data[index] == expected)
116 return true;
117 ESP_LOGW(TAG, "%s (byte %zu) - expected 0x%02X, but was 0x%02X", name, index, expected, data[index]);
118 return false;
119}
120
121static bool validate_active_frame(const uint8_t data[FRAME_SIZE]) {
122 const uint8_t sum = sum_bytes(data, FRAME_CHECKSUM_INDEX);
123 return check_byte(data, 0, FRAME_START_BYTE_1, "start byte 1") &&
124 check_byte(data, 1, FRAME_START_BYTE_2, "start byte 2") &&
125 check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
126 check_byte(data, FRAME_INDEX_RESERVED_SECTION, FRAME_FLAG_RESERVED_SECTION, "reserved section flag") &&
127 check_byte(data, FRAME_INDEX_TEMP_FLAG, FRAME_FLAG_TEMP, "temperature flag") &&
128 check_byte(data, FRAME_CHECKSUM_INDEX, sum, "checksum") &&
129 check_byte(data, FRAME_STOP_INDEX, FRAME_STOP_BYTE, "stop byte");
130}
131
132static bool validate_passive_frame(const uint8_t data[PASSIVE_FRAME_SIZE]) {
133 if (data[0] != FRAME_START_BYTE_1 || data[1] != PASSIVE_START_BYTE_2 || data[22] != FRAME_STOP_BYTE)
134 return false;
135 return data[21] == sum_bytes(data, 21);
136}
137
138#ifdef USE_UFM01_METER_ID
139static bool validate_passive_with_id_frame(const uint8_t data[PASSIVE_FRAME_WITH_ID_SIZE]) {
140 if (data[0] != FRAME_START_BYTE_1 || data[1] != PASSIVE_START_BYTE_2_WITH_ID ||
141 data[PASSIVE_WITH_ID_STOP_INDEX] != FRAME_STOP_BYTE)
142 return false;
143 uint8_t sum = 0;
144 for (size_t i = 0; i < PASSIVE_WITH_ID_CHECKSUM_INDEX; ++i)
145 sum += data[i];
146 return data[PASSIVE_WITH_ID_CHECKSUM_INDEX] == sum;
147}
148#endif
149
150// Start index of each run in a passive reply; each run maps to the matching run of the active frame
151struct PassiveLayout {
152 uint8_t acc_flow; // flag + 6 bytes, active 8..14
153 uint8_t instant_flow; // flag + 5 bytes, active 15..20
154 uint8_t temperature; // flag + 3 bytes, active 24..27
155 uint8_t status; // ST1, ST2, checksum, active 28..30
156};
157static constexpr PassiveLayout PASSIVE_NO_ID_LAYOUT{2, 9, 15, 19};
158#ifdef USE_UFM01_METER_ID
159static constexpr PassiveLayout PASSIVE_WITH_ID_LAYOUT{PASSIVE_WITH_ID_ACC_FLOW_FLAG_INDEX,
160 PASSIVE_WITH_ID_INSTANT_FLOW_FLAG_INDEX,
161 PASSIVE_WITH_ID_TEMP_FLAG_INDEX, PASSIVE_WITH_ID_ST1_INDEX};
162#endif
163
164static void passive_to_active_frame(const uint8_t *passive, const PassiveLayout &layout, uint8_t active[FRAME_SIZE]) {
165 std::memset(active, 0, FRAME_SIZE);
166 active[0] = FRAME_START_BYTE_1;
167 active[1] = FRAME_START_BYTE_2;
168 std::copy_n(&passive[layout.acc_flow], 7, &active[FRAME_ACC_FLOW_FLAG_INDEX]);
169 std::copy_n(&passive[layout.instant_flow], 6, &active[FRAME_INDEX_INSTANT_FLOW_FLAG]);
170 active[FRAME_INDEX_RESERVED_SECTION] = FRAME_FLAG_RESERVED_SECTION;
171 std::copy_n(&passive[layout.temperature], 4, &active[FRAME_INDEX_TEMP_FLAG]);
172 std::copy_n(&passive[layout.status], 3, &active[FRAME_ST1_INDEX]);
173 active[FRAME_STOP_INDEX] = FRAME_STOP_BYTE;
174}
175
176static float read_accumulated_flow(const uint8_t data[FRAME_SIZE]) {
177 return (data[FRAME_ACC_FLOW_FLAG_INDEX] == ACC_FLOW_M3_FLAG ? L_PER_M3 : 1.0f) *
178 (to_float(data[14]) * 10000000.0f + to_float(data[13]) * 100000.0f + to_float(data[12]) * 1000.0f +
179 to_float(data[11]) * 10.0f + to_float(data[10]) * 0.1f + to_float(data[9]) * 0.001f);
180}
181
182static float read_flow(const uint8_t data[FRAME_SIZE]) {
183 return (data[FRAME_FLOW_SIGN_INDEX] == FLOW_NEGATIVE_SIGN ? -1.0f : 1.0f) *
184 (to_float(data[19]) * 10000.0f + to_float(data[18]) * 100.0f + to_float(data[17]) +
185 to_float(data[16]) * 0.01f) *
186 M3_PER_L;
187}
188
189static constexpr size_t LOG_HEX_MAX_SIZE = std::max(FRAME_SIZE, PASSIVE_FRAME_MAX_SIZE);
190
191static void log_hex(const uint8_t *data, size_t len) {
192 char hex_buf[format_hex_pretty_size(LOG_HEX_MAX_SIZE)];
193 ESP_LOGD(TAG, "%s", format_hex_pretty_to(hex_buf, data, len, ' '));
194}
195
196static float read_temperature(const uint8_t data[FRAME_SIZE]) {
197 // happens sometimes before getting a real reading
198 if (data[27] == 0x00 && (data[26] == 0x00 || data[26] == 0x70) && data[25] == 0x00) {
199 return NAN;
200 }
201 return to_float(data[27]) * 100.0f + to_float(data[26]) + to_float(data[25]) * 0.01f;
202}
203
204static bool read_ufc_chip_error(const uint8_t data[FRAME_SIZE]) { return data[FRAME_ST2_INDEX] & ST2_UFC_ERROR_MASK; }
205
206static bool read_flow_direction_wrong(const uint8_t data[FRAME_SIZE]) {
207 return data[FRAME_ST2_INDEX] & ST2_FLOW_DIRECTION_WRONG_MASK;
208}
209
210static bool read_empty_tube(const uint8_t data[FRAME_SIZE]) { return data[FRAME_ST1_INDEX] & ST1_EMPTY_TUBE_MASK; }
211
212static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
213 return data[FRAME_ST2_INDEX] & ST2_FLOW_RATE_OUT_OF_RANGE_MASK;
214}
215
216void UFM01Component::flush_rx_() {
217 while (this->available()) {
218 uint8_t byte;
219 this->read_byte(&byte);
220 }
221 this->read_index_ = 0;
222}
223
224void UFM01Component::send_command_no_wait_(const std::array<uint8_t, 7> &command) {
225 this->flush_rx_();
226 this->write_array(command);
227 this->flush();
228}
229
230bool UFM01Component::consume_ack_() {
231 while (this->available()) {
232 uint8_t byte;
233 if (!this->read_byte(&byte))
234 return false;
235 if (byte == COMMAND_ACK)
236 return true;
237 ESP_LOGV(TAG, "Unexpected byte while waiting for command ACK: 0x%02X", byte);
238 }
239 return false;
240}
241
242#ifdef USE_UFM01_CLEAR_ACCUMULATED_FLOW_ACTION
243bool UFM01Component::can_start_clear_action_() const {
244 return
245#ifdef USE_UFM01_SOFTWARE_VERSION
246 !this->software_version_read_pending_ &&
247#endif
248 (this->operating_mode_ == OperatingMode::ACTIVE_STREAM ||
249 (this->operating_mode_ == OperatingMode::PASSIVE_POLL && !this->passive_read_pending_));
250}
251
252void UFM01Component::request_clear_accumulated_flow() {
253 if (this->clear_pending_) {
254 ESP_LOGD(TAG, "Clear accumulated flow already pending");
255 return;
256 }
257 this->clear_pending_ = true;
258 this->pending_clear_sent_ = false;
259 this->pending_clear_start_ms_ = App.get_loop_component_start_time();
260}
261
262void UFM01Component::finish_pending_clear_() {
263 this->clear_pending_ = false;
264 this->pending_clear_sent_ = false;
265}
266
267void UFM01Component::loop_pending_clear_action_(uint32_t now) {
268 if (!this->clear_pending_)
269 return;
270 if (!this->pending_clear_sent_) {
271 if (!this->can_start_clear_action_()) {
272 if (now - this->pending_clear_start_ms_ < CLEAR_QUEUE_TIMEOUT_MS)
273 return;
274 ESP_LOGW(TAG, "Clear accumulated flow timed out waiting for an idle UART");
275 this->finish_pending_clear_();
276 return;
277 }
278 this->pending_clear_start_ms_ = now;
279 this->pending_clear_sent_ = true;
280 this->send_command_no_wait_(CLEAR_ACCUMULATED_FLOW);
281 return;
282 }
283 if (this->consume_ack_()) {
284 ESP_LOGI(TAG, "Clear accumulated flow acknowledged");
285 this->finish_pending_clear_();
286 return;
287 }
288 if (now - this->pending_clear_start_ms_ < COMMAND_ACK_TIMEOUT_MS)
289 return;
290 ESP_LOGW(TAG, "Clear accumulated flow not acknowledged");
291 this->finish_pending_clear_();
292}
293#endif // USE_UFM01_CLEAR_ACCUMULATED_FLOW_ACTION
294
295float UFM01Component::get_setup_priority() const { return setup_priority::LATE; }
296
298 ESP_LOGI(TAG, "Setting up UFM-01...");
299 this->startup_wait_ms_ = STARTUP_DELAY_MS;
300 this->set_startup_phase_(StartupPhase::WAIT, App.get_loop_component_start_time());
301}
302
303void UFM01Component::dump_config() {
304 ESP_LOGCONFIG(TAG, "UFM-01:");
305#ifdef USE_SENSOR
306 LOG_SENSOR(" ", "Accumulated Flow", this->accumulated_flow_sensor_);
307 LOG_SENSOR(" ", "Flow", this->flow_sensor_);
308 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
309#endif
310#ifdef USE_BINARY_SENSOR
311 LOG_BINARY_SENSOR(" ", "UFC Chip Error", this->ufc_chip_error_binary_sensor_);
312 LOG_BINARY_SENSOR(" ", "Flow Direction Wrong", this->flow_direction_wrong_binary_sensor_);
313 LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
314 LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
315#endif
316#ifdef USE_UFM01_METER_ID
317 LOG_TEXT_SENSOR(" ", "Meter ID", this->meter_id_text_sensor_);
318#endif
319#ifdef USE_UFM01_SOFTWARE_VERSION
320 LOG_TEXT_SENSOR(" ", "Software Version", this->software_version_text_sensor_);
321#endif
322}
323
324// The binary sensors keep their last state while readings are stale; only flow and temperature go unknown
325void UFM01Component::publish_stale_flow_and_temperature_() {
326#ifdef USE_SENSOR
327 if (this->flow_sensor_ != nullptr)
328 this->flow_sensor_->publish_state(NAN);
329 if (this->temperature_sensor_ != nullptr)
330 this->temperature_sensor_->publish_state(NAN);
331#endif
332}
333
334#ifdef USE_UFM01_METER_ID
335void UFM01Component::publish_meter_id_from_frame_(const uint8_t data[FRAME_SIZE]) {
336 if (this->meter_id_text_sensor_ == nullptr || this->meter_id_published_)
337 return;
338
339 char meter_id_str[DEVICE_ID_STRING_LENGTH + 1];
340 if (!bcd_to_digits(&data[FRAME_DEVICE_ID_INDEX], DEVICE_ID_LENGTH, meter_id_str)) {
341 if (!this->meter_id_invalid_logged_) {
342 ESP_LOGW(TAG, "Meter ID in frame is zero or not BCD, leaving the sensor unknown");
343 this->meter_id_invalid_logged_ = true;
344 }
345 return;
346 }
347
348 this->meter_id_text_sensor_->publish_state(meter_id_str);
349 this->meter_id_published_ = true;
350 ESP_LOGI(TAG, "UFM-01 meter ID: %s", meter_id_str);
351}
352#endif
353
354#ifdef USE_UFM01_SOFTWARE_VERSION
355void UFM01Component::start_software_version_read_(uint32_t now) {
356 this->send_command_no_wait_(GET_SOFTWARE_VERSION);
357 this->passive_index_ = 0;
358 this->passive_start_ms_ = now;
359 this->software_version_read_pending_ = true;
360}
361
362ReadResult UFM01Component::continue_software_version_read_(uint32_t now) {
363 while (this->available() && this->passive_index_ < SOFTWARE_VERSION_RESPONSE_SIZE) {
364 uint8_t byte;
365 if (!this->read_byte(&byte))
366 break;
367 // The tail of an in-flight active-stream frame can arrive before the reply
368 if (this->passive_index_ == 0 && byte != COMMAND_ACK)
369 continue;
370 this->passive_frame_[this->passive_index_++] = byte;
371 }
372
373 if (this->passive_index_ < SOFTWARE_VERSION_RESPONSE_SIZE) {
374 if (now - this->passive_start_ms_ < SOFTWARE_VERSION_READ_TIMEOUT_MS)
376 ESP_LOGD(TAG, "software version read timeout (%u/%u bytes)", this->passive_index_,
377 static_cast<unsigned>(SOFTWARE_VERSION_RESPONSE_SIZE));
379 }
380
381 if (!validate_software_version_response(this->passive_frame_)) {
382 log_hex(this->passive_frame_, SOFTWARE_VERSION_RESPONSE_SIZE);
383 ESP_LOGW(TAG, "invalid software version response");
385 }
386
387 if (this->software_version_text_sensor_ != nullptr && !this->software_version_published_) {
388 char version_str[SOFTWARE_VERSION_STRING_LENGTH + 1];
389 if (!bcd_to_digits(&this->passive_frame_[1], SOFTWARE_VERSION_LENGTH, version_str)) {
390 ESP_LOGW(TAG, "invalid BCD data in software version response");
392 }
393 this->software_version_text_sensor_->publish_state(version_str);
394 this->software_version_published_ = true;
395 ESP_LOGI(TAG, "UFM-01 software version: %s", version_str);
396 }
397
399}
400
401void UFM01Component::finish_software_version_read_(ReadResult result, uint32_t now) {
402 this->software_version_read_pending_ = false;
403 this->last_software_version_attempt_ms_ = now;
404 if (result == ReadResult::READ_RESULT_FAILURE &&
405 ++this->software_version_failures_ == SOFTWARE_VERSION_MAX_ATTEMPTS) {
406 ESP_LOGW(TAG, "Software version not read after %u attempts, giving up", SOFTWARE_VERSION_MAX_ATTEMPTS);
407 }
408}
409
410// Returns true while a software version read owns the UART, so the caller skips its own work
411bool UFM01Component::service_software_version_(uint32_t now) {
412 if (this->software_version_read_pending_) {
413 const ReadResult result = this->continue_software_version_read_(now);
415 this->finish_software_version_read_(result, now);
416 return true;
417 }
418 if (this->software_version_text_sensor_ == nullptr || this->software_version_published_ ||
419 this->software_version_failures_ >= SOFTWARE_VERSION_MAX_ATTEMPTS || this->passive_read_pending_)
420 return false;
421 if (this->last_software_version_attempt_ms_ != 0 &&
422 now - this->last_software_version_attempt_ms_ < SOFTWARE_VERSION_RETRY_MS)
423 return false;
424 this->start_software_version_read_(now);
425 return true;
426}
427#endif // USE_UFM01_SOFTWARE_VERSION
428
429void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now) {
430#ifdef USE_UFM01_METER_ID
431 this->publish_meter_id_from_frame_(data);
432#endif
433 bool empty_tube = read_empty_tube(data);
434#ifdef USE_BINARY_SENSOR
435 if (this->ufc_chip_error_binary_sensor_ != nullptr)
436 this->ufc_chip_error_binary_sensor_->publish_state(read_ufc_chip_error(data));
437 if (this->flow_direction_wrong_binary_sensor_ != nullptr)
438 this->flow_direction_wrong_binary_sensor_->publish_state(read_flow_direction_wrong(data));
439 if (this->empty_tube_binary_sensor_ != nullptr)
440 this->empty_tube_binary_sensor_->publish_state(empty_tube);
441 if (this->flow_rate_out_of_range_binary_sensor_ != nullptr)
442 this->flow_rate_out_of_range_binary_sensor_->publish_state(read_flow_rate_out_of_range(data));
443#endif
444
445#ifdef USE_SENSOR
446 // Total volume remains valid when the tube is dry; flow and temperature are not.
447 if (this->accumulated_flow_sensor_ != nullptr)
448 this->accumulated_flow_sensor_->publish_state(read_accumulated_flow(data));
449
450 if (empty_tube) {
451 this->publish_stale_flow_and_temperature_();
452 } else {
453 if (this->flow_sensor_ != nullptr)
454 this->flow_sensor_->publish_state(read_flow(data));
455 if (this->temperature_sensor_ != nullptr)
456 this->temperature_sensor_->publish_state(read_temperature(data));
457 }
458#endif
459 this->last_valid_frame_ms_ = now;
460 this->status_clear_warning();
461 this->status_clear_error();
462}
463
464bool UFM01Component::process_active_stream_(uint32_t now) {
465 bool got_valid_frame = false;
466
467 while (this->available()) {
468 if (!this->read_byte(&this->data_[this->read_index_])) {
469 ESP_LOGW(TAG, "unable to read byte");
470 this->read_index_ = 0;
471 continue;
472 }
473 if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
474 (this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
475 ESP_LOGD(TAG, "not start of data at %" PRId32 " (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
476 this->read_index_ = 0;
477 continue;
478 }
479 if (++this->read_index_ < static_cast<int32_t>(FRAME_SIZE))
480 continue;
481
482 if (validate_active_frame(this->data_)) {
483 this->on_active_frame_(this->data_, now);
484 this->read_index_ = 0;
485 got_valid_frame = true;
486 continue;
487 }
488
489 log_hex(this->data_, sizeof(this->data_));
490 ESP_LOGW(TAG, "unable to read data");
491 for (int32_t i = 2;
492 i < static_cast<int32_t>(FRAME_STOP_INDEX) && this->read_index_ == static_cast<int32_t>(FRAME_SIZE); ++i) {
493 if ((this->data_[i] == FRAME_START_BYTE_1) && (this->data_[i + 1] == FRAME_START_BYTE_2)) {
494 for (int32_t j = i; j < static_cast<int32_t>(FRAME_SIZE); ++j)
495 this->data_[j - i] = this->data_[j];
496 this->read_index_ = static_cast<int32_t>(FRAME_SIZE) - i;
497 }
498 }
499 if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
500 this->read_index_ = 0;
501 }
502
503 return got_valid_frame;
504}
505
506void UFM01Component::set_mode_(OperatingMode mode, uint32_t now) {
507 this->operating_mode_ = mode;
508 this->phase_start_ms_ = now;
509 this->passive_read_pending_ = false;
510#ifdef USE_UFM01_SOFTWARE_VERSION
511 this->software_version_read_pending_ = false;
512#endif
513 this->consecutive_passive_failures_ = 0;
514}
515
516void UFM01Component::set_startup_phase_(StartupPhase phase, uint32_t now) {
517 this->startup_phase_ = phase;
518 this->phase_start_ms_ = now;
519}
520
521void UFM01Component::send_reset_(const LogString *status, uint32_t now) {
522 this->status_set_warning(status);
523 this->reset_retried_ = false;
524 this->send_command_no_wait_(RESET_DEVICE);
525 this->set_startup_phase_(StartupPhase::RESET_WAIT_ACK, now);
526}
527
528void UFM01Component::enter_active_stream_(const LogString *reason, uint32_t now) {
529 ESP_LOGI(TAG, "UFM-01 active stream %s", LOG_STR_ARG(reason));
530 this->set_mode_(OperatingMode::ACTIVE_STREAM, now);
531}
532
533void UFM01Component::enter_passive_from_stale_(uint32_t now) {
534 ESP_LOGW(TAG, "Active stream stale, switching to passive polling");
535 this->publish_stale_flow_and_temperature_();
536 // Flushes leftover stream bytes first; the ACK is not awaited, the next command's flush discards it
537 this->send_command_no_wait_(PASSIVE_MODE);
538 this->set_mode_(OperatingMode::PASSIVE_POLL, now);
539 this->last_poll_ms_ = now;
540 this->status_set_warning(LOG_STR("UFM-01 passive poll"));
541}
542
543void UFM01Component::restart_startup_(const LogString *reason, uint32_t now) {
544 ESP_LOGW(TAG, "%s, re-initializing UFM-01", LOG_STR_ARG(reason));
545 this->publish_stale_flow_and_temperature_();
546 this->set_mode_(OperatingMode::STARTUP, now);
547 this->send_reset_(LOG_STR("re-initializing UFM-01"), now);
548}
549
550void UFM01Component::note_passive_poll_result_(bool success, uint32_t now) {
551 if (success) {
552 this->consecutive_passive_failures_ = 0;
553 return;
554 }
555 this->status_set_warning(LOG_STR("UFM-01 passive poll failed"));
556 if (++this->consecutive_passive_failures_ < PASSIVE_FAIL_ESCALATE_COUNT)
557 return;
558 this->restart_startup_(LOG_STR("Passive poll failed repeatedly"), now);
559}
560
561void UFM01Component::start_passive_read_(uint32_t now) {
562#ifdef USE_UFM01_METER_ID
563 if (this->meter_id_text_sensor_ != nullptr && !this->meter_id_published_) {
564 this->passive_expected_size_ = PASSIVE_FRAME_WITH_ID_SIZE;
565 this->passive_start_byte_2_ = PASSIVE_START_BYTE_2_WITH_ID;
566 this->send_command_no_wait_(READ_SENSOR_DATA_WITH_ID);
567 } else
568#endif
569 {
570 this->passive_expected_size_ = PASSIVE_FRAME_SIZE;
571 this->passive_start_byte_2_ = PASSIVE_START_BYTE_2;
572 this->send_command_no_wait_(READ_SENSOR_DATA_NO_ID);
573 }
574 this->passive_index_ = 0;
575 this->passive_start_ms_ = now;
576}
577
578ReadResult UFM01Component::continue_passive_read_(uint32_t now) {
579 const uint8_t expected_size = this->passive_expected_size_;
580
581 while (this->available() && this->passive_index_ < expected_size) {
582 uint8_t byte;
583 if (!this->read_byte(&byte))
584 break;
585
586 if (this->passive_index_ == 0 && byte != FRAME_START_BYTE_1)
587 continue;
588 if (this->passive_index_ == 1 && byte != this->passive_start_byte_2_) {
589 // The mismatched byte may itself be the start of the real frame
590 this->passive_index_ = (byte == FRAME_START_BYTE_1) ? 1 : 0;
591 continue;
592 }
593 this->passive_frame_[this->passive_index_++] = byte;
594 }
595
596 if (this->passive_index_ < expected_size) {
597 if (now - this->passive_start_ms_ < PASSIVE_READ_TIMEOUT_MS)
599 ESP_LOGD(TAG, "passive read timeout (%u/%u bytes)", this->passive_index_, expected_size);
601 }
602
603 uint8_t active_frame[FRAME_SIZE];
604#ifdef USE_UFM01_METER_ID
605 if (expected_size == PASSIVE_FRAME_WITH_ID_SIZE) {
606 if (!validate_passive_with_id_frame(this->passive_frame_)) {
607 log_hex(this->passive_frame_, PASSIVE_FRAME_WITH_ID_SIZE);
608 ESP_LOGW(TAG, "invalid passive frame with ID");
610 }
611 passive_to_active_frame(this->passive_frame_, PASSIVE_WITH_ID_LAYOUT, active_frame);
612 // Device ID and the byte after it
613 std::copy_n(&this->passive_frame_[PASSIVE_WITH_ID_DEVICE_ID_INDEX], DEVICE_ID_LENGTH + 1,
614 &active_frame[FRAME_DEVICE_ID_INDEX]);
615 } else
616#endif
617 {
618 if (!validate_passive_frame(this->passive_frame_)) {
619 log_hex(this->passive_frame_, PASSIVE_FRAME_SIZE);
620 ESP_LOGW(TAG, "invalid passive frame");
622 }
623 passive_to_active_frame(this->passive_frame_, PASSIVE_NO_ID_LAYOUT, active_frame);
624 active_frame[FRAME_DEVICE_ID_INDEX + DEVICE_ID_LENGTH] = 0x01;
625 }
626 this->on_active_frame_(active_frame, now);
628}
629
630void UFM01Component::loop_startup_(uint32_t now) {
631 const uint32_t elapsed = now - this->phase_start_ms_;
632
633 switch (this->startup_phase_) {
635 // Pick up an already-streaming device without resetting it
636 if (this->process_active_stream_(now)) {
637 this->enter_active_stream_(LOG_STR("started"), now);
638 return;
639 }
640 if (elapsed < this->startup_wait_ms_)
641 return;
642 ESP_LOGD(TAG, "Running startup sequence");
643 this->send_reset_(LOG_STR("initializing UFM-01"), now);
644 return;
645
647 if (this->consume_ack_()) {
648 this->set_startup_phase_(StartupPhase::POST_RESET_WAIT, now);
649 return;
650 }
651 if (elapsed < COMMAND_ACK_TIMEOUT_MS)
652 return;
653 if (!this->reset_retried_) {
654 ESP_LOGW(TAG, "Reset not acknowledged, retrying in %" PRIu32 " ms", RESET_RETRY_DELAY_MS);
655 this->set_startup_phase_(StartupPhase::RESET_RETRY_WAIT, now);
656 } else {
657 ESP_LOGW(TAG, "Reset failed during startup");
658 this->set_startup_phase_(StartupPhase::POST_RESET_WAIT, now);
659 }
660 return;
661
663 if (elapsed < RESET_RETRY_DELAY_MS)
664 return;
665 this->reset_retried_ = true;
666 this->send_command_no_wait_(RESET_DEVICE);
667 this->set_startup_phase_(StartupPhase::RESET_WAIT_ACK, now);
668 return;
669
671 if (elapsed < POST_RESET_DELAY_MS)
672 return;
673#ifdef USE_UFM01_SOFTWARE_VERSION
674 if (this->software_version_text_sensor_ != nullptr && !this->software_version_published_ &&
675 this->software_version_failures_ < SOFTWARE_VERSION_MAX_ATTEMPTS) {
676 this->start_software_version_read_(now);
677 this->set_startup_phase_(StartupPhase::SOFTWARE_VERSION_WAIT_REPLY, now);
678 return;
679 }
680#endif
681 this->send_command_no_wait_(ACTIVE_MODE);
682 this->set_startup_phase_(StartupPhase::ACTIVE_WAIT_FRAME, now);
683 return;
684
685#ifdef USE_UFM01_SOFTWARE_VERSION
687 const ReadResult result = this->continue_software_version_read_(now);
689 return;
690 if (result == ReadResult::READ_RESULT_SUCCESS) {
691 ESP_LOGD(TAG, "Software version read during startup");
692 } else {
693 ESP_LOGW(TAG, "Software version read failed during startup, continuing");
694 }
695 this->finish_software_version_read_(result, now);
696 this->send_command_no_wait_(ACTIVE_MODE);
697 this->set_startup_phase_(StartupPhase::ACTIVE_WAIT_FRAME, now);
698 return;
699 }
700#endif
701
703 // The command ACK (0xE5) is consumed by the frame parser as noise
704 if (this->process_active_stream_(now)) {
705 this->enter_active_stream_(LOG_STR("started"), now);
706 return;
707 }
708 if (elapsed < ACTIVE_FRAME_TIMEOUT_MS)
709 return;
710 this->send_command_no_wait_(PASSIVE_MODE);
711 this->set_startup_phase_(StartupPhase::SET_PASSIVE_WAIT_ACK, now);
712 return;
713
715 if (this->consume_ack_()) {
716 ESP_LOGD(TAG, "Passive mode acknowledged during startup");
717 } else if (elapsed < COMMAND_ACK_TIMEOUT_MS) {
718 return;
719 } else {
720 ESP_LOGW(TAG, "SET_PASSIVE_MODE not acknowledged during startup, continuing");
721 }
722 this->start_passive_read_(now);
723 this->set_startup_phase_(StartupPhase::PASSIVE_WAIT_REPLY, now);
724 return;
725
727 switch (this->continue_passive_read_(now)) {
729 return;
731 ESP_LOGI(TAG, "UFM-01 using passive polling");
732 this->set_mode_(OperatingMode::PASSIVE_POLL, now);
733 this->last_poll_ms_ = now;
734 return;
736 ESP_LOGW(TAG, "Startup failed, retrying in %" PRIu32 " ms", STARTUP_RETRY_MS);
737 this->startup_wait_ms_ = STARTUP_RETRY_MS;
738 this->set_startup_phase_(StartupPhase::WAIT, now);
739 return;
740 }
741 }
742}
743
744void UFM01Component::loop_active_stream_(uint32_t now) {
745#ifdef USE_UFM01_SOFTWARE_VERSION
746 if (this->service_software_version_(now))
747 return;
748#endif
749
750 this->process_active_stream_(now);
751
752 if (this->last_valid_frame_ms_ != 0 && now - this->last_valid_frame_ms_ > ACTIVE_STALE_MS)
753 this->enter_passive_from_stale_(now);
754}
755
756void UFM01Component::loop_passive_poll_(uint32_t now) {
757#ifdef USE_UFM01_SOFTWARE_VERSION
758 if (this->service_software_version_(now))
759 return;
760#endif
761
762 if (this->passive_read_pending_) {
763 const ReadResult result = this->continue_passive_read_(now);
765 return;
766 this->passive_read_pending_ = false;
767 this->note_passive_poll_result_(result == ReadResult::READ_RESULT_SUCCESS, now);
768 return;
769 }
770
771 if (this->process_active_stream_(now)) {
772 this->enter_active_stream_(LOG_STR("resumed"), now);
773 return;
774 }
775
776 if (now - this->last_poll_ms_ >= PASSIVE_POLL_INTERVAL_MS) {
777 this->last_poll_ms_ = now;
778 this->start_passive_read_(now);
779 this->passive_read_pending_ = true;
780 }
781}
782
783void UFM01Component::loop() {
785#ifdef USE_UFM01_CLEAR_ACCUMULATED_FLOW_ACTION
786 this->loop_pending_clear_action_(now);
787 if (this->clear_pending_ && this->pending_clear_sent_)
788 return;
789#endif
790 switch (this->operating_mode_) {
792 this->loop_startup_(now);
793 return;
795 this->loop_active_stream_(now);
796 return;
798 this->loop_passive_poll_(now);
799 return;
800 }
801}
802
803} // namespace esphome::ufm01
BedjetMode mode
BedJet operating mode.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
void status_clear_error()
Definition component.h:297
void status_clear_warning()
Definition component.h:291
UARTFlushResult flush()
Definition uart.h:51
bool read_byte(uint8_t *data)
Definition uart.h:36
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
std::vector< uint8_t > & out
Definition gif.cpp:49
std::span< const uint8_t > data
constexpr float LATE
For components that should be initialized at the very end of the setup process.
Definition component.h:59
ESPHOME_LOG_TAG(TAG, "ufm01")
constexpr uint8_t sum_bytes(const uint8_t *data, size_t len, uint8_t sum=0)
Add up the bytes of data with size len, truncated to 8 bits: the checksum many serial and IR protocol...
Definition helpers.h:886
const void size_t len
Definition hal.h:68
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:498
size_t size_t pos
Definition helpers.h:1179
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1533
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t