mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d8d422056 | ||
|
|
a8a8acb918 | ||
|
|
578b6798a4 | ||
|
|
5f15b0f9b5 | ||
|
|
eb6d343750 | ||
|
|
237781a97d | ||
|
|
e4d6e1fdb2 | ||
|
|
8c9e195e8f |
@@ -28,30 +28,23 @@ void CSE7766Component::loop() {
|
||||
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
this->read_available<CSE7766_RAW_DATA_SIZE>(
|
||||
[this](uint8_t byte) {
|
||||
this->raw_data_[this->raw_data_index_] = byte;
|
||||
if (!this->check_byte_()) {
|
||||
this->raw_data_index_ = 0;
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->raw_data_[this->raw_data_index_] = buf[i];
|
||||
if (!this->check_byte_()) {
|
||||
this->raw_data_index_ = 0;
|
||||
this->status_set_warning();
|
||||
continue;
|
||||
}
|
||||
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
this->parse_data_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
this->parse_data_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
|
||||
}
|
||||
}
|
||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
|
||||
},
|
||||
avail);
|
||||
}
|
||||
|
||||
bool CSE7766Component::check_byte_() {
|
||||
|
||||
@@ -135,153 +135,140 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
}
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t bi = 0; bi < to_read; bi++) {
|
||||
uint8_t byte = buf[bi];
|
||||
this->read_available([this](uint8_t byte) {
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
continue;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG,
|
||||
"Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
return;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
this->read_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
});
|
||||
}
|
||||
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -181,21 +180,11 @@ void DFRobotSEN0609Component::setup() {
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::loop() {
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
const size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
if (!this->read_failed_) {
|
||||
ESP_LOGW(TAG, "UART read failed");
|
||||
}
|
||||
this->read_failed_ = true;
|
||||
break;
|
||||
}
|
||||
if (this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); })) {
|
||||
this->read_failed_ = false;
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++)
|
||||
this->readline_(buf[i]);
|
||||
} else if (!this->read_failed_) {
|
||||
ESP_LOGW(TAG, "UART read failed");
|
||||
this->read_failed_ = true;
|
||||
}
|
||||
this->run_sequencer_();
|
||||
}
|
||||
|
||||
@@ -274,20 +274,7 @@ void LD2410Component::restart_and_read_all_info() {
|
||||
}
|
||||
|
||||
void LD2410Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
|
||||
}
|
||||
|
||||
void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "ld2410s.h"
|
||||
#include "ld2410s_number.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -95,19 +94,8 @@ void LD2410SComponent::dump_config() {
|
||||
}
|
||||
|
||||
void LD2410SComponent::loop() {
|
||||
uint8_t chunk[16];
|
||||
// Take the count once so one pass handles a bounded number of bytes
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
const size_t len = std::min(avail, sizeof(chunk));
|
||||
if (!this->read_array(chunk, len)) {
|
||||
ESP_LOGV(TAG, "UART read of %u bytes failed", static_cast<unsigned>(len));
|
||||
break;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->receive_byte_(chunk[i]);
|
||||
}
|
||||
avail -= len;
|
||||
if (!this->read_available<16>([this](uint8_t b) { this->receive_byte_(b); })) {
|
||||
ESP_LOGV(TAG, "UART read failed");
|
||||
}
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->sequence_ == nullptr) {
|
||||
|
||||
@@ -316,20 +316,7 @@ void LD2412Component::restart_and_read_all_info() {
|
||||
}
|
||||
|
||||
void LD2412Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
|
||||
}
|
||||
|
||||
void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
|
||||
@@ -543,20 +543,8 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i], buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>(
|
||||
[this, &buffer](uint8_t b) { this->readline_(b, buffer.data(), buffer.size()); });
|
||||
}
|
||||
|
||||
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
|
||||
@@ -288,20 +288,7 @@ void LD2450Component::dump_config() {
|
||||
}
|
||||
|
||||
void LD2450Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
|
||||
}
|
||||
|
||||
// Count targets in zone (single pass for both still and moving)
|
||||
|
||||
@@ -82,19 +82,7 @@ void LD2460Component::dump_config() {
|
||||
}
|
||||
|
||||
void LD2460Component::loop() {
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
|
||||
}
|
||||
|
||||
void LD2460Component::handle_periodic_data_() {
|
||||
|
||||
@@ -532,18 +532,9 @@ void Nextion::set_string_return_(const std::string &value) {
|
||||
}
|
||||
|
||||
void Nextion::process_serial_() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
this->command_data_.append(reinterpret_cast<const char *>(buf), to_read);
|
||||
}
|
||||
this->read_available_chunks([this](const uint8_t *data, size_t len) {
|
||||
this->command_data_.append(reinterpret_cast<const char *>(data), len);
|
||||
});
|
||||
}
|
||||
// nextion.tech/instruction-set/
|
||||
void Nextion::process_nextion_commands_() {
|
||||
|
||||
@@ -86,47 +86,31 @@ void Pipsolar::loop() {
|
||||
}
|
||||
|
||||
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
this->read_available([this](uint8_t byte) {
|
||||
// make sure data and null terminator fit in buffer
|
||||
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
|
||||
this->read_pos_ = 0;
|
||||
this->discard_available();
|
||||
ESP_LOGW(TAG, "response data too long, discarding.");
|
||||
return false;
|
||||
}
|
||||
avail -= to_read;
|
||||
bool done = false;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
uint8_t byte = buf[i];
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
|
||||
// make sure data and null terminator fit in buffer
|
||||
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
|
||||
this->read_pos_ = 0;
|
||||
this->discard_available();
|
||||
ESP_LOGW(TAG, "response data too long, discarding.");
|
||||
done = true;
|
||||
break;
|
||||
// end of answer
|
||||
if (byte == 0x0D) {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->discard_available();
|
||||
if (this->state_ == STATE_POLL) {
|
||||
this->state_ = STATE_POLL_COMPLETE;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
|
||||
// end of answer
|
||||
if (byte == 0x0D) {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->discard_available();
|
||||
if (this->state_ == STATE_POLL) {
|
||||
this->state_ = STATE_POLL_COMPLETE;
|
||||
}
|
||||
if (this->state_ == STATE_COMMAND) {
|
||||
this->state_ = STATE_COMMAND_COMPLETE;
|
||||
}
|
||||
done = true;
|
||||
break;
|
||||
if (this->state_ == STATE_COMMAND) {
|
||||
this->state_ = STATE_COMMAND_COMPLETE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (this->state_ == STATE_COMMAND) {
|
||||
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {
|
||||
|
||||
@@ -85,49 +85,38 @@ void RD03DComponent::dump_config() {
|
||||
}
|
||||
|
||||
void RD03DComponent::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
uint8_t byte = buf[i];
|
||||
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
|
||||
this->read_available([this](uint8_t byte) {
|
||||
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
|
||||
|
||||
// Check if we're looking for frame header
|
||||
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
|
||||
if (byte == FRAME_HEADER[this->buffer_pos_]) {
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
} else if (byte == FRAME_HEADER[0]) {
|
||||
// Start over if we see a potential new header
|
||||
this->buffer_[0] = byte;
|
||||
this->buffer_pos_ = 1;
|
||||
} else {
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Accumulate data bytes
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
|
||||
// Check if we have a complete frame
|
||||
if (this->buffer_pos_ == FRAME_SIZE) {
|
||||
// Validate footer
|
||||
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
|
||||
this->process_frame_();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
|
||||
this->buffer_[FRAME_SIZE - 1]);
|
||||
}
|
||||
// Check if we're looking for frame header
|
||||
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
|
||||
if (byte == FRAME_HEADER[this->buffer_pos_]) {
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
} else if (byte == FRAME_HEADER[0]) {
|
||||
// Start over if we see a potential new header
|
||||
this->buffer_[0] = byte;
|
||||
this->buffer_pos_ = 1;
|
||||
} else {
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate data bytes
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
|
||||
// Check if we have a complete frame
|
||||
if (this->buffer_pos_ == FRAME_SIZE) {
|
||||
// Validate footer
|
||||
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
|
||||
this->process_frame_();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
|
||||
this->buffer_[FRAME_SIZE - 1]);
|
||||
}
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void RD03DComponent::process_frame_() {
|
||||
|
||||
@@ -201,27 +201,21 @@ void RFBridgeComponent::loop() {
|
||||
this->last_bridge_byte_ = now;
|
||||
}
|
||||
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) {
|
||||
this->rx_buffer_.clear();
|
||||
this->bucket_frame_candidate_ = false;
|
||||
}
|
||||
if (this->parse_bridge_byte_(buf[i])) {
|
||||
ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]);
|
||||
this->last_bridge_byte_ = now;
|
||||
} else {
|
||||
this->rx_buffer_.clear();
|
||||
this->bucket_frame_candidate_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->read_available(
|
||||
[this, now](uint8_t byte) {
|
||||
if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) {
|
||||
this->rx_buffer_.clear();
|
||||
this->bucket_frame_candidate_ = false;
|
||||
}
|
||||
if (this->parse_bridge_byte_(byte)) {
|
||||
ESP_LOGVV(TAG, "Parsed: 0x%02X", byte);
|
||||
this->last_bridge_byte_ = now;
|
||||
} else {
|
||||
this->rx_buffer_.clear();
|
||||
this->bucket_frame_candidate_ = false;
|
||||
}
|
||||
},
|
||||
avail);
|
||||
}
|
||||
|
||||
void RFBridgeComponent::send_code(RFBridgeData data) {
|
||||
|
||||
@@ -110,20 +110,7 @@ void MR24HPC1Component::update_() {
|
||||
|
||||
// main loop
|
||||
void MR24HPC1Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->r24_split_data_frame_(buf[i]); // split data frame
|
||||
}
|
||||
}
|
||||
this->read_available([this](uint8_t b) { this->r24_split_data_frame_(b); });
|
||||
|
||||
if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) &&
|
||||
(this->sg_start_query_data_ > CUSTOM_FUNCTION_QUERY_TIME_OF_ENTER_UNMANNED) && (!this->check_dev_inf_sign_)) {
|
||||
|
||||
@@ -29,23 +29,12 @@ void MR60BHA2Component::dump_config() {
|
||||
|
||||
// main loop
|
||||
void MR60BHA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
this->read_available([this](uint8_t b) {
|
||||
this->rx_message_.push_back(b);
|
||||
if (!this->validate_message_()) {
|
||||
this->rx_message_.clear();
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->rx_message_.push_back(buf[i]);
|
||||
if (!this->validate_message_()) {
|
||||
this->rx_message_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,20 +46,7 @@ void MR60FDA2Component::setup() {
|
||||
|
||||
// main loop
|
||||
void MR60FDA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->split_frame_(buf[i]); // split data frame
|
||||
}
|
||||
}
|
||||
this->read_available([this](uint8_t b) { this->split_frame_(b); });
|
||||
}
|
||||
|
||||
// The checksum is the inverted XOR of the bytes
|
||||
|
||||
@@ -39,13 +39,16 @@ from esphome.const import (
|
||||
CONF_REFERENCE_TEMPERATURE,
|
||||
CONF_SEND_EVERY,
|
||||
CONF_SEND_FIRST_AT,
|
||||
CONF_SENSOR_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TEMPERATURE_OFFSET,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WINDOW_SIZE,
|
||||
@@ -310,6 +313,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
|
||||
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
|
||||
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
|
||||
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
|
||||
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
|
||||
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
|
||||
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
|
||||
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
|
||||
@@ -1281,6 +1285,23 @@ async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"rh_correction",
|
||||
RHCorrectionFilter,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TEMPERATURE_OFFSET): cv.templatable(cv.float_),
|
||||
cv.Required(CONF_SENSOR_ID): cv.use_id(Sensor),
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rh_correction_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
|
||||
sensor = await cg.get_variable(config[CONF_SENSOR_ID])
|
||||
offset = await cg.templatable(config[CONF_TEMPERATURE_OFFSET], [], cg.float_)
|
||||
return cg.new_Pvariable(filter_id, offset, sensor, config[CONF_USE_FAHRENHEIT])
|
||||
|
||||
|
||||
def _mean(xs):
|
||||
return sum(xs) / len(xs)
|
||||
|
||||
|
||||
@@ -493,6 +493,48 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
return temp;
|
||||
}
|
||||
|
||||
// RHCorrectionFilter
|
||||
static constexpr float MAGNUS_B = 17.67f, MAGNUS_C = 243.5f;
|
||||
|
||||
static bool rh_correction_temp_in_band(float t) { return t >= -40.0f && t <= 50.0f; }
|
||||
|
||||
optional<float> RHCorrectionFilter::new_value(float value) {
|
||||
if (!this->temperature_sensor_->has_state()) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): no temperature yet, passing humidity through uncorrected", this);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
float offset = this->temperature_offset_.value();
|
||||
float tc = this->temperature_sensor_->get_state();
|
||||
if (this->use_fahrenheit_) {
|
||||
tc = fahrenheit_to_celsius(tc);
|
||||
offset /= 1.8f;
|
||||
}
|
||||
const float tnc = tc - offset;
|
||||
// A NaN temperature or offset is a faulted input: keep the last corrected value
|
||||
if (std::isnan(tnc)) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): temperature or offset is NaN, dropping humidity updates", this);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (!rh_correction_temp_in_band(tc) || !rh_correction_temp_in_band(tnc)) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): Temperature out of range -40..50 °C. Tc=%f Tnc=%f", this, tc, tnc);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
this->warned_ = false;
|
||||
const float rh =
|
||||
clamp(value * std::exp(MAGNUS_B * MAGNUS_C * (tnc - tc) / ((tnc + MAGNUS_C) * (tc + MAGNUS_C))), 0.0f, 100.0f);
|
||||
ESP_LOGVV(TAG, "RHCorrectionFilter(%p)::new_value(%f) -> %f", this, value, rh);
|
||||
return rh;
|
||||
}
|
||||
|
||||
// StreamingFilter (base class)
|
||||
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
|
||||
: window_size_(window_size), send_first_at_(send_first_at) {}
|
||||
|
||||
@@ -647,6 +647,26 @@ class ToNTCTemperatureFilter : public Filter {
|
||||
double c_;
|
||||
};
|
||||
|
||||
/** RH Temperature correction filter
|
||||
*
|
||||
* Allows for Relative Humidity Correction when a temperature offset is applied to the temperature sensor.
|
||||
* Uses the Magnus formula, rh = rh_nc * exp(b*(t_nc/(c+t_nc) - t/(c+t))) with b = 17.67, c = 243.5 °C;
|
||||
* see https://en.wikipedia.org/wiki/Dew_point
|
||||
*/
|
||||
class RHCorrectionFilter : public Filter {
|
||||
public:
|
||||
RHCorrectionFilter(TemplatableFn<float> offset, const Sensor *temperature_sensor, bool use_fahrenheit)
|
||||
: temperature_offset_(offset), temperature_sensor_(temperature_sensor), use_fahrenheit_(use_fahrenheit) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
TemplatableFn<float> temperature_offset_;
|
||||
const Sensor *temperature_sensor_;
|
||||
bool use_fahrenheit_;
|
||||
bool warned_{false};
|
||||
};
|
||||
|
||||
/** Base class for streaming filters (batch windows where window_size == send_every).
|
||||
*
|
||||
* When window_size equals send_every, we don't need a sliding window.
|
||||
|
||||
@@ -54,20 +54,7 @@ void Tuya::setup() {
|
||||
}
|
||||
|
||||
void Tuya::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->handle_char_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available([this](uint8_t b) { this->handle_char_(b); });
|
||||
process_command_queue_();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: temperature
|
||||
filters:
|
||||
- offset: -7.3
|
||||
- platform: template
|
||||
id: constant_offset
|
||||
filters:
|
||||
- rh_correction:
|
||||
sensor_id: temperature
|
||||
temperature_offset: -7.3
|
||||
- platform: template
|
||||
id: templated_offset
|
||||
filters:
|
||||
- rh_correction:
|
||||
sensor_id: temperature
|
||||
temperature_offset: !lambda return -7.3;
|
||||
use_fahrenheit: true
|
||||
@@ -0,0 +1,60 @@
|
||||
"""The rh_correction filter: schema and the constructor call it generates."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.sensor import validate_filters
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filters",
|
||||
[
|
||||
pytest.param([{"rh_correction": {"sensor_id": "t"}}], id="no_offset"),
|
||||
pytest.param([{"rh_correction": {"temperature_offset": -7.3}}], id="no_sensor"),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
"rh_correction": {
|
||||
"sensor_id": "t",
|
||||
"temperature_offset": -7.3,
|
||||
"use_fahrenheit": "maybe",
|
||||
}
|
||||
}
|
||||
],
|
||||
id="bad_fahrenheit",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rh_correction_rejects_incomplete_config(filters: list[dict]) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
validate_filters(filters)
|
||||
|
||||
|
||||
def test_rh_correction_defaults_to_celsius() -> None:
|
||||
config = validate_filters(
|
||||
[{"rh_correction": {"sensor_id": "t", "temperature_offset": -7.3}}]
|
||||
)
|
||||
assert config[0]["rh_correction"]["use_fahrenheit"] is False
|
||||
|
||||
|
||||
def test_rh_correction_codegen(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
main_cpp = generate_main("tests/component_tests/sensor/rh_correction.yaml")
|
||||
|
||||
# A constant offset is wrapped in a stateless lambda so the filter stores a function pointer
|
||||
assert re.search(
|
||||
r"sensor::RHCorrectionFilter\(\[\]\(\) -> float \{\s*return -7\.3f;\s*\}, "
|
||||
r"temperature, false\);\s*constant_offset->add_filter\(",
|
||||
main_cpp,
|
||||
)
|
||||
# A templated offset keeps the user's lambda body and passes use_fahrenheit through
|
||||
assert re.search(
|
||||
r"sensor::RHCorrectionFilter\(\[\]\(\) -> float \{[^}]*return -7\.3;[^}]*\}, "
|
||||
r"temperature, true\);\s*templated_offset->add_filter\(",
|
||||
main_cpp,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
# filter.cpp only compiles with the define the real to_code emits for configured filters
|
||||
cg.add_define("USE_SENSOR_FILTER")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -254,3 +254,21 @@ sensor:
|
||||
lambda: return 42.0;
|
||||
update_interval: 1s
|
||||
state_class: "measurement_angle"
|
||||
|
||||
# Testing rh_correction filter
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "RH Correction Filter Fahrenheit"
|
||||
filters:
|
||||
- rh_correction:
|
||||
sensor_id: source_sensor
|
||||
temperature_offset: -10.0
|
||||
use_fahrenheit: true
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "RH Correction Filter Template"
|
||||
filters:
|
||||
- rh_correction:
|
||||
sensor_id: source_sensor
|
||||
temperature_offset: !lambda return -10.0;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
|
||||
#include "esphome/components/sensor/filter.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::sensor::testing {
|
||||
|
||||
// 40 % read 7.3 °C warm at a true 25 °C, in single precision Magnus arithmetic
|
||||
static constexpr float CORRECTED = 61.13f;
|
||||
|
||||
struct RhFixture {
|
||||
Sensor temperature;
|
||||
RHCorrectionFilter filter;
|
||||
|
||||
RhFixture(float (*offset)(), bool use_fahrenheit = false) : filter(offset, &this->temperature, use_fahrenheit) {}
|
||||
};
|
||||
|
||||
TEST(RHCorrectionFilter, CorrectsForTheOffset) {
|
||||
RhFixture f([]() -> float { return -7.3f; });
|
||||
f.temperature.publish_state(25.0f);
|
||||
EXPECT_NEAR(f.filter.new_value(40.0f).value_or(NAN), CORRECTED, 0.05f);
|
||||
}
|
||||
|
||||
TEST(RHCorrectionFilter, PositiveOffsetLowersTheHumidity) {
|
||||
RhFixture f([]() -> float { return 7.3f; });
|
||||
f.temperature.publish_state(25.0f);
|
||||
EXPECT_LT(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
|
||||
}
|
||||
|
||||
TEST(RHCorrectionFilter, FahrenheitReadingsGiveTheSameResult) {
|
||||
// 77 °F is 25 °C and -13.14 °F is -7.3 °C of offset
|
||||
RhFixture f([]() -> float { return -13.14f; }, true);
|
||||
f.temperature.publish_state(77.0f);
|
||||
EXPECT_NEAR(f.filter.new_value(40.0f).value_or(NAN), CORRECTED, 0.05f);
|
||||
}
|
||||
|
||||
TEST(RHCorrectionFilter, ClampsToOneHundredPercent) {
|
||||
RhFixture f([]() -> float { return -7.3f; });
|
||||
f.temperature.publish_state(25.0f);
|
||||
EXPECT_FLOAT_EQ(f.filter.new_value(90.0f).value_or(NAN), 100.0f);
|
||||
}
|
||||
|
||||
TEST(RHCorrectionFilter, PassesThroughWithoutAUsableTemperature) {
|
||||
RhFixture f([]() -> float { return -7.3f; });
|
||||
// No temperature reading yet
|
||||
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
|
||||
// Out of the -40..50 °C band
|
||||
f.temperature.publish_state(60.0f);
|
||||
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
|
||||
// The band also covers the reconstructed enclosure temperature
|
||||
f.temperature.publish_state(45.0f);
|
||||
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
|
||||
}
|
||||
|
||||
TEST(RHCorrectionFilter, DropsTheUpdateOnNaN) {
|
||||
RhFixture f([]() -> float { return -7.3f; });
|
||||
f.temperature.publish_state(NAN);
|
||||
EXPECT_FALSE(f.filter.new_value(40.0f).has_value());
|
||||
|
||||
RhFixture nan_offset([]() -> float { return NAN; });
|
||||
nan_offset.temperature.publish_state(25.0f);
|
||||
EXPECT_FALSE(nan_offset.filter.new_value(40.0f).has_value());
|
||||
}
|
||||
|
||||
} // namespace esphome::sensor::testing
|
||||
@@ -0,0 +1,102 @@
|
||||
esphome:
|
||||
name: test-rh-correction
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Temperature C"
|
||||
id: temperature_c
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Temperature F"
|
||||
id: temperature_f
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Humidity Source"
|
||||
id: humidity_source
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: copy
|
||||
source_id: humidity_source
|
||||
name: "RH Celsius"
|
||||
id: rh_celsius
|
||||
accuracy_decimals: 2
|
||||
filters:
|
||||
- rh_correction:
|
||||
sensor_id: temperature_c
|
||||
temperature_offset: -7.3
|
||||
|
||||
- platform: copy
|
||||
source_id: humidity_source
|
||||
name: "RH Fahrenheit"
|
||||
id: rh_fahrenheit
|
||||
accuracy_decimals: 2
|
||||
filters:
|
||||
- rh_correction:
|
||||
sensor_id: temperature_f
|
||||
temperature_offset: -13.14
|
||||
use_fahrenheit: true
|
||||
|
||||
script:
|
||||
- id: test_rh_correction
|
||||
then:
|
||||
# Both temperature sensors read 25 °C after a -7.3 °C (-13.14 °F) offset
|
||||
- sensor.template.publish:
|
||||
id: temperature_c
|
||||
state: 25.0
|
||||
- sensor.template.publish:
|
||||
id: temperature_f
|
||||
state: 77.0
|
||||
- sensor.template.publish:
|
||||
id: humidity_source
|
||||
state: 40.0
|
||||
- delay: 20ms
|
||||
# Clamped to 100 %
|
||||
- sensor.template.publish:
|
||||
id: humidity_source
|
||||
state: 90.0
|
||||
- delay: 20ms
|
||||
# Out of band: passes through unchanged
|
||||
- sensor.template.publish:
|
||||
id: temperature_c
|
||||
state: 60.0
|
||||
- sensor.template.publish:
|
||||
id: temperature_f
|
||||
state: 140.0
|
||||
- sensor.template.publish:
|
||||
id: humidity_source
|
||||
state: 40.0
|
||||
- delay: 20ms
|
||||
# NaN temperature: the update is dropped
|
||||
- sensor.template.publish:
|
||||
id: temperature_c
|
||||
state: !lambda "return NAN;"
|
||||
- sensor.template.publish:
|
||||
id: temperature_f
|
||||
state: !lambda "return NAN;"
|
||||
- sensor.template.publish:
|
||||
id: humidity_source
|
||||
state: 50.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: temperature_c
|
||||
state: 25.0
|
||||
- sensor.template.publish:
|
||||
id: temperature_f
|
||||
state: 77.0
|
||||
- sensor.template.publish:
|
||||
id: humidity_source
|
||||
state: 40.0
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test RH Correction"
|
||||
on_press:
|
||||
- script.execute: test_rh_correction
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Test the sensor rh_correction filter numerically."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aioesphomeapi import ButtonInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, SensorStateCollector, require_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
from .wait_utils import wait_or_fail
|
||||
|
||||
# 40 % at 25 °C, measured 7.3 °C warmer, in single precision Magnus arithmetic
|
||||
CORRECTED = pytest.approx(61.13, abs=0.05)
|
||||
# Corrected, clamped, passed through out of band, corrected again; the NaN step is dropped
|
||||
EXPECTED = [CORRECTED, 100.0, 40.0, CORRECTED]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_filters_rh_correction(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
collector = SensorStateCollector(sensor_names=["rh_celsius", "rh_fahrenheit"])
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
collector.build_key_mapping(entities)
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(
|
||||
initial_state_helper.on_state_wrapper(collector.on_state)
|
||||
)
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
done = collector.add_waiter(
|
||||
lambda: all(
|
||||
len(values) == len(EXPECTED)
|
||||
for values in collector.sensor_states.values()
|
||||
)
|
||||
)
|
||||
button = require_entity(entities, "test_rh_correction", ButtonInfo, exact=True)
|
||||
client.button_command(button.key)
|
||||
await wait_or_fail(
|
||||
done,
|
||||
2.0,
|
||||
"Timed out waiting for the corrected humidity values",
|
||||
values=collector.sensor_states,
|
||||
)
|
||||
|
||||
assert collector.sensor_states["rh_celsius"] == EXPECTED
|
||||
# The same readings in °F give the same corrected humidity
|
||||
assert collector.sensor_states["rh_fahrenheit"] == EXPECTED
|
||||
Reference in New Issue
Block a user