mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
Merge remote-tracking branch 'origin/rp2040-upload-improvements' into integration
This commit is contained in:
@@ -55,18 +55,24 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === LabelAction.ADD) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: pr_number, labels: [LABEL_NAME]
|
||||
});
|
||||
console.log(`Added '${LABEL_NAME}' label`);
|
||||
} else if (action === LabelAction.REMOVE) {
|
||||
try {
|
||||
try {
|
||||
if (action === LabelAction.ADD) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: pr_number, labels: [LABEL_NAME]
|
||||
});
|
||||
console.log(`Added '${LABEL_NAME}' label`);
|
||||
} else if (action === LabelAction.REMOVE) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number: pr_number, name: LABEL_NAME
|
||||
});
|
||||
console.log(`Removed '${LABEL_NAME}' label`);
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 403) {
|
||||
console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`);
|
||||
} else if (error.status === 404) {
|
||||
console.log(`Label '${LABEL_NAME}' not present, nothing to remove`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -260,13 +260,17 @@ def choose_upload_log_host(
|
||||
]
|
||||
|
||||
# Add RP2040 BOOTSEL device option when uploading
|
||||
bootsel_permission_error = False
|
||||
if (
|
||||
purpose == Purpose.UPLOADING
|
||||
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||
and (picotool := _find_picotool()) is not None
|
||||
and detect_rp2040_bootsel(picotool) > 0
|
||||
):
|
||||
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
|
||||
bootsel = detect_rp2040_bootsel(picotool)
|
||||
if bootsel.device_count > 0:
|
||||
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
|
||||
elif bootsel.permission_error:
|
||||
bootsel_permission_error = True
|
||||
|
||||
if purpose == Purpose.LOGGING:
|
||||
if has_mqtt_logging():
|
||||
@@ -291,6 +295,17 @@ def choose_upload_log_host(
|
||||
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
|
||||
):
|
||||
if bootsel_permission_error:
|
||||
_LOGGER.warning(
|
||||
"An RP2040 device in BOOTSEL mode was detected but could "
|
||||
"not be accessed due to USB permissions."
|
||||
)
|
||||
if sys.platform.startswith("linux"):
|
||||
_LOGGER.warning(
|
||||
"You may need to add a udev rule for RP2040 devices. "
|
||||
"See: https://github.com/raspberrypi/picotool"
|
||||
"/blob/master/udev/60-picotool.rules"
|
||||
)
|
||||
if not options:
|
||||
raise EsphomeError(
|
||||
f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}"
|
||||
@@ -876,7 +891,8 @@ def upload_using_picotool(config: ConfigType) -> int:
|
||||
if sys.platform.startswith("linux"):
|
||||
msg += (
|
||||
" You may need to add udev rules for RP2040 devices."
|
||||
" See: https://github.com/raspberrypi/picotool#linux-permissions"
|
||||
" See: https://github.com/raspberrypi/picotool"
|
||||
"/blob/master/udev/60-picotool.rules"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
else:
|
||||
|
||||
@@ -144,9 +144,12 @@ void Anova::update() {
|
||||
return;
|
||||
|
||||
if (this->current_request_ < 2) {
|
||||
auto *pkt = this->codec_->get_read_device_status_request();
|
||||
if (this->current_request_ == 0)
|
||||
this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
|
||||
AnovaPacket *pkt;
|
||||
if (this->current_request_ == 0) {
|
||||
pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
|
||||
} else {
|
||||
pkt = this->codec_->get_read_device_status_request();
|
||||
}
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
|
||||
@@ -155,6 +155,18 @@ APIConnection::~APIConnection() {
|
||||
voice_assistant::global_voice_assistant->client_subscription(this, false);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) {
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
for (auto *proxy : App.get_serial_proxies()) {
|
||||
if (proxy->get_api_connection() == this) {
|
||||
proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::destroy_active_iterator_() {
|
||||
|
||||
@@ -373,6 +373,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
@@ -380,6 +381,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
||||
// Normal memory access
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(data + 1, reason_str, reason_len);
|
||||
|
||||
@@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() {
|
||||
}
|
||||
|
||||
// Set gain
|
||||
if (this->gain_ < 0 || static_cast<size_t>(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) ||
|
||||
static_cast<size_t>(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) {
|
||||
ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_);
|
||||
return false;
|
||||
}
|
||||
if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) ||
|
||||
!this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) {
|
||||
ESP_LOGE(TAG, "Failed to write AT581X gain registers");
|
||||
|
||||
@@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
|
||||
const auto &data = service_data.data;
|
||||
|
||||
if (data.size() < 10) {
|
||||
ESP_LOGW(TAG, "Service data too short: %zu", data.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t protocol_version = data[0] >> 4;
|
||||
if (protocol_version != 1 && protocol_version != 2) {
|
||||
ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
|
||||
@@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
// Some b-parasite versions have an (optional) illuminance sensor.
|
||||
bool has_illuminance = data[0] & 0x1;
|
||||
|
||||
if (has_illuminance && data.size() < 18) {
|
||||
ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counter for deduplicating messages.
|
||||
uint8_t counter = data[1] & 0x0f;
|
||||
if (last_processed_counter_ == counter) {
|
||||
|
||||
@@ -136,7 +136,6 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
return {};
|
||||
} else {
|
||||
this->steady_ = false;
|
||||
this->output(value);
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -138,23 +138,24 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se
|
||||
|
||||
this->write_byte(BL0906_READ_COMMAND);
|
||||
this->write_byte(address);
|
||||
if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) {
|
||||
if (bl0906_checksum(address, &buffer) == buffer.checksum) {
|
||||
if (signed_result) {
|
||||
data_s24.l = buffer.l;
|
||||
data_s24.m = buffer.m;
|
||||
data_s24.h = buffer.h;
|
||||
} else {
|
||||
data_u24.l = buffer.l;
|
||||
data_u24.m = buffer.m;
|
||||
data_u24.h = buffer.h;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
|
||||
while (read() >= 0)
|
||||
;
|
||||
return;
|
||||
}
|
||||
if (!this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) {
|
||||
ESP_LOGW(TAG, "Read failed");
|
||||
return;
|
||||
}
|
||||
if (bl0906_checksum(address, &buffer) != buffer.checksum) {
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
|
||||
while (read() >= 0)
|
||||
;
|
||||
return;
|
||||
}
|
||||
if (signed_result) {
|
||||
data_s24.l = buffer.l;
|
||||
data_s24.m = buffer.m;
|
||||
data_s24.h = buffer.h;
|
||||
} else {
|
||||
data_u24.l = buffer.l;
|
||||
data_u24.m = buffer.m;
|
||||
data_u24.h = buffer.h;
|
||||
}
|
||||
// Power
|
||||
if (reference == BL0906_PREF) {
|
||||
@@ -190,11 +191,9 @@ void BL0906::bias_correction_(uint8_t address, float measurements, float correct
|
||||
float i_rms0 = measurements * ki;
|
||||
float i_rms = correction * ki;
|
||||
int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256;
|
||||
data.l = value << 24 >> 24;
|
||||
data.m = value << 16 >> 24;
|
||||
if (value < 0) {
|
||||
data.h = (value << 8 >> 24) | 0b10000000;
|
||||
}
|
||||
data.l = value & 0xFF;
|
||||
data.m = (value >> 8) & 0xFF;
|
||||
data.h = (value >> 16) & 0xFF;
|
||||
data.address = bl0906_checksum(address, &data);
|
||||
ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address);
|
||||
this->write_byte(BL0906_WRITE_COMMAND);
|
||||
|
||||
@@ -16,12 +16,27 @@ namespace ble_scanner {
|
||||
class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
public:
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
|
||||
// Format JSON using stack buffer to avoid heap allocations from string concatenation
|
||||
char buf[128];
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
// Escape special characters in the device name for valid JSON
|
||||
const char *name = device.get_name().c_str();
|
||||
char escaped_name[128];
|
||||
size_t pos = 0;
|
||||
for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) {
|
||||
uint8_t c = static_cast<uint8_t>(*name);
|
||||
if (c == '"' || c == '\\') {
|
||||
escaped_name[pos++] = '\\';
|
||||
escaped_name[pos++] = c;
|
||||
} else if (c < 0x20) {
|
||||
pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c);
|
||||
} else {
|
||||
escaped_name[pos++] = c;
|
||||
}
|
||||
}
|
||||
escaped_name[pos] = '\0';
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}",
|
||||
static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(),
|
||||
device.get_name().c_str());
|
||||
static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), escaped_name);
|
||||
this->publish_state(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -147,8 +147,11 @@ void BME280Component::setup() {
|
||||
this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1);
|
||||
this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2);
|
||||
this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3);
|
||||
this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F);
|
||||
this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4);
|
||||
// h4 and h5 are signed 12-bit values; shift left then arithmetic right shift to sign-extend
|
||||
int16_t h4_raw = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F);
|
||||
this->calibration_.h4 = static_cast<int16_t>(h4_raw << 4) >> 4;
|
||||
int16_t h5_raw = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4);
|
||||
this->calibration_.h5 = static_cast<int16_t>(h5_raw << 4) >> 4;
|
||||
this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6);
|
||||
|
||||
uint8_t humid_control_val = 0;
|
||||
|
||||
@@ -32,8 +32,8 @@ enum BME680Oversampling {
|
||||
/// Struct for storing calibration data for the BME680.
|
||||
struct BME680CalibrationData {
|
||||
uint16_t t1;
|
||||
uint16_t t2;
|
||||
uint8_t t3;
|
||||
int16_t t2;
|
||||
int8_t t3;
|
||||
|
||||
uint16_t p1;
|
||||
int16_t p2;
|
||||
|
||||
@@ -271,10 +271,16 @@ void BME680BSECComponent::read_() {
|
||||
int64_t curr_time_ns = this->get_time_ns_();
|
||||
|
||||
if (this->bme680_settings_.trigger_measurement) {
|
||||
uint32_t start = millis();
|
||||
while (this->bme680_.power_mode != BME680_SLEEP_MODE) {
|
||||
if (millis() - start > 50) {
|
||||
ESP_LOGE(TAG, "Timeout waiting for BME680 to enter sleep mode");
|
||||
return;
|
||||
}
|
||||
this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_);
|
||||
if (this->bme680_status_ != BME680_OK) {
|
||||
ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_);
|
||||
ESP_LOGE(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace esphome {
|
||||
namespace bmi160 {
|
||||
|
||||
static const char *const TAG = "bmi160";
|
||||
static constexpr uint32_t GYRO_WAKEUP_TIMEOUT_MS = 100;
|
||||
|
||||
const uint8_t BMI160_REGISTER_CHIPID = 0x00;
|
||||
|
||||
@@ -144,7 +145,7 @@ void BMI160Component::internal_setup_(int stage) {
|
||||
}
|
||||
ESP_LOGV(TAG, " Waiting for gyroscope to wake up");
|
||||
// wait between 51 & 81ms, doing 100 to be safe
|
||||
this->set_timeout(10, [this]() { this->internal_setup_(2); });
|
||||
this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); });
|
||||
break;
|
||||
|
||||
case 2:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "canbus.h"
|
||||
#include <algorithm>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -82,7 +83,7 @@ void Canbus::loop() {
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
// show data received
|
||||
for (int i = 0; i < can_message.can_data_length_code; i++) {
|
||||
for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) {
|
||||
ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]);
|
||||
data.push_back(can_message.data[i]);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ static const char *const TAG = "captive_portal.dns";
|
||||
// DNS constants
|
||||
static constexpr uint16_t DNS_PORT = 53;
|
||||
static constexpr uint16_t DNS_QR_FLAG = 1 << 15;
|
||||
static constexpr uint16_t DNS_AA_FLAG = 1 << 10;
|
||||
static constexpr uint16_t DNS_OPCODE_MASK = 0x7800;
|
||||
static constexpr uint16_t DNS_QTYPE_A = 0x0001;
|
||||
static constexpr uint16_t DNS_QCLASS_IN = 0x0001;
|
||||
@@ -162,8 +163,8 @@ void DNSServer::process_next_request() {
|
||||
}
|
||||
|
||||
// Build DNS response by modifying the request in-place
|
||||
header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative
|
||||
header->an_count = htons(1); // One answer
|
||||
header->flags = htons(DNS_QR_FLAG | DNS_AA_FLAG); // Response + Authoritative
|
||||
header->an_count = htons(1); // One answer
|
||||
|
||||
// Add answer section after the question
|
||||
size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::cse7766 {
|
||||
|
||||
@@ -192,12 +193,12 @@ void CSE7766Component::parse_data_() {
|
||||
this->apparent_power_sensor_->publish_state(apparent_power);
|
||||
}
|
||||
if (have_power && this->reactive_power_sensor_ != nullptr) {
|
||||
const float reactive_power = apparent_power - power;
|
||||
if (reactive_power < 0.0f) {
|
||||
ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power);
|
||||
const float q_squared = apparent_power * apparent_power - power * power;
|
||||
if (q_squared < 0.0f) {
|
||||
ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared);
|
||||
this->reactive_power_sensor_->publish_state(0.0f);
|
||||
} else {
|
||||
this->reactive_power_sensor_->publish_state(reactive_power);
|
||||
this->reactive_power_sensor_->publish_state(std::sqrt(q_squared));
|
||||
}
|
||||
}
|
||||
if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) {
|
||||
|
||||
@@ -350,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
|
||||
valid_daikin_frame = true;
|
||||
size_t bytes_count = data.size() / 2 / 8;
|
||||
size_t buf_size = bytes_count * 3 + 1;
|
||||
std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill)
|
||||
// Header (20) + state (19) = 39 bytes max; truncates gracefully via buf_append_printf
|
||||
char buf[40 * 3 + 1] = {};
|
||||
constexpr size_t buf_size = sizeof(buf);
|
||||
size_t buf_pos = 0;
|
||||
for (size_t i = 0; i < bytes_count; i++) {
|
||||
uint8_t byte = 0;
|
||||
@@ -363,9 +364,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte);
|
||||
buf_pos = buf_append_printf(buf, buf_size, buf_pos, "%02x ", byte);
|
||||
}
|
||||
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size());
|
||||
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf, data.size());
|
||||
}
|
||||
if (!valid_daikin_frame) {
|
||||
char sbuf[16 * 10 + 1] = {0};
|
||||
|
||||
@@ -32,8 +32,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
|
||||
auto code = call.get_code();
|
||||
switch (state) {
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
if (this->get_requires_code_to_arm() && code.has_value()) {
|
||||
if (*code != "1234") {
|
||||
if (this->get_requires_code_to_arm()) {
|
||||
if (!code.has_value() || *code != "1234") {
|
||||
this->status_momentary_error("invalid_code", 5000);
|
||||
return;
|
||||
}
|
||||
@@ -41,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
|
||||
this->publish_state(ACP_STATE_ARMED_AWAY);
|
||||
break;
|
||||
case ACP_STATE_DISARMED:
|
||||
if (this->get_requires_code() && code.has_value()) {
|
||||
if (*code != "1234") {
|
||||
if (this->get_requires_code()) {
|
||||
if (!code.has_value() || *code != "1234") {
|
||||
this->status_momentary_error("invalid_code", 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include "e131.h"
|
||||
#ifdef USE_NETWORK
|
||||
@@ -57,7 +58,7 @@ union E131RawPacket {
|
||||
|
||||
// We need to have at least one `1` value
|
||||
// Get the offset of `property_values[1]`
|
||||
const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *) nullptr)->property_values[1]);
|
||||
const size_t E131_MIN_PACKET_SIZE = offsetof(E131RawPacket, property_values) + sizeof(uint8_t);
|
||||
|
||||
bool E131Component::join_igmp_groups_() {
|
||||
if (this->listen_method_ != E131_MULTICAST)
|
||||
|
||||
@@ -152,7 +152,7 @@ void ES8388::dump_config() {
|
||||
|
||||
bool ES8388::set_volume(float volume) {
|
||||
volume = clamp(volume, 0.0f, 1.0f);
|
||||
uint8_t value = remap<uint8_t, float>(volume, 0.0f, 1.0f, -96, 0);
|
||||
uint8_t value = remap<uint8_t, float>(volume, 0.0f, 1.0f, 192, 0);
|
||||
ESP_LOGD(TAG, "Setting ES8388_DACCONTROL4 / ES8388_DACCONTROL5 to 0x%02X (volume: %f)", value, volume);
|
||||
ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL4, value));
|
||||
ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL5, value));
|
||||
@@ -163,7 +163,7 @@ bool ES8388::set_volume(float volume) {
|
||||
float ES8388::volume() {
|
||||
uint8_t value;
|
||||
ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL4, &value));
|
||||
return remap<float, uint8_t>(value, -96, 0, 0.0f, 1.0f);
|
||||
return remap<float, uint8_t>(value, 192, 0, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
bool ES8388::set_mute_state_(bool mute_state) {
|
||||
|
||||
@@ -514,6 +514,11 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
|
||||
continue; // Possible zero padded advertisement data
|
||||
}
|
||||
|
||||
// Validate field fits in remaining payload
|
||||
if (offset + field_length > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
// first byte of adv record is adv record type
|
||||
const uint8_t record_type = payload[offset++];
|
||||
const uint8_t *record = &payload[offset];
|
||||
@@ -544,7 +549,7 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
|
||||
// CSS 1.5 TX POWER LEVEL
|
||||
// "The TX Power Level data type indicates the transmitted power level of the packet containing the data type."
|
||||
// CSS 1: Optional in this context (may appear more than once in a block).
|
||||
this->tx_powers_.push_back(*payload);
|
||||
this->tx_powers_.push_back(*record);
|
||||
break;
|
||||
}
|
||||
case ESP_BLE_AD_TYPE_APPEARANCE: {
|
||||
|
||||
@@ -146,6 +146,10 @@ void ESP32Camera::dump_config() {
|
||||
}
|
||||
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
if (s == nullptr) {
|
||||
ESP_LOGE(TAG, " Camera sensor not available");
|
||||
return;
|
||||
}
|
||||
auto st = s->status;
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" JPEG Quality: %u\n"
|
||||
@@ -483,6 +487,9 @@ void ESP32Camera::request_image(camera::CameraRequester requester) { this->singl
|
||||
camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; }
|
||||
void ESP32Camera::update_camera_parameters() {
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
if (s == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* update image */
|
||||
s->set_vflip(s, this->vertical_flip_);
|
||||
s->set_hmirror(s, this->horizontal_mirror_);
|
||||
|
||||
@@ -84,7 +84,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
|
||||
this->rtc_ = global_preferences->make_preference<uint8_t[SZ]>(1944399030U ^ this->name_hash_);
|
||||
bool hasdata = this->rtc_.load(&temp);
|
||||
if (hasdata) {
|
||||
this->value_.assign(temp + 1, temp[0]);
|
||||
this->value_.assign(temp + 1, static_cast<uint8_t>(temp[0]));
|
||||
}
|
||||
this->last_checked_value_.assign(this->value_);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps,
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::stepper_stop() {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) {
|
||||
uint8_t status = 0;
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, &status, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Send stop stepper failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
|
||||
@@ -114,7 +114,7 @@ void HeatpumpIRClimate::setup() {
|
||||
this->current_temperature = state;
|
||||
|
||||
IRSenderESPHome esp_sender(this->transmitter_);
|
||||
this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature + 0.5)));
|
||||
this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature)));
|
||||
|
||||
// current temperature changed, publish state
|
||||
this->publish_state();
|
||||
|
||||
@@ -96,7 +96,7 @@ class HitachiClimate : public climate_ir::ClimateIR {
|
||||
void set_power_(bool on);
|
||||
uint8_t get_mode_();
|
||||
void set_mode_(uint8_t mode);
|
||||
void set_temp_(uint8_t celsius, bool set_previous = false);
|
||||
void set_temp_(uint8_t celsius, bool set_previous = true);
|
||||
uint8_t get_fan_();
|
||||
void set_fan_(uint8_t speed);
|
||||
void set_swing_v_toggle_(bool on);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
static constexpr uint32_t PAYLOAD_TIMEOUT_MS = 20;
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
@@ -133,6 +134,21 @@ void HlkFm22xComponent::recv_command_() {
|
||||
checksum ^= byte;
|
||||
length |= byte;
|
||||
|
||||
// Wait for remaining data (payload + checksum) to arrive.
|
||||
// Header bytes are already consumed, so we must finish reading this message.
|
||||
uint32_t start = millis();
|
||||
while (this->available() < length + 1) {
|
||||
if (millis() - start > PAYLOAD_TIMEOUT_MS) {
|
||||
ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length);
|
||||
// Drain any partial payload bytes to resync the parser
|
||||
while (this->available() > 0) {
|
||||
this->read();
|
||||
}
|
||||
return;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
// Read up to buffer size; discard excess bytes while still computing checksum
|
||||
// GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes)
|
||||
// but handlers only need the first few bytes
|
||||
|
||||
@@ -61,7 +61,7 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
ID_REGISTERS,
|
||||
} error_code_;
|
||||
} error_code_{NONE};
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice {
|
||||
const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333)
|
||||
const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC)
|
||||
|
||||
float max_count_;
|
||||
float min_count_;
|
||||
float max_count_{max_count_a_};
|
||||
float min_count_{min_count_a_};
|
||||
bool measurement_running_ = false;
|
||||
|
||||
uint8_t raw_data_[7]; // holds output data
|
||||
|
||||
@@ -110,9 +110,17 @@ void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) {
|
||||
|
||||
for (int i = 0; i < buffer_len; i++) {
|
||||
if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) {
|
||||
if (tx_msg_len + 2 >= static_cast<int>(sizeof(tx_msg))) {
|
||||
ESP_LOGE(TAG, "TX message overflow");
|
||||
return;
|
||||
}
|
||||
tx_msg[tx_msg_len++] = 0x1b;
|
||||
tx_msg[tx_msg_len++] = buffer[i] ^ 0xff;
|
||||
} else {
|
||||
if (tx_msg_len + 1 >= static_cast<int>(sizeof(tx_msg))) {
|
||||
ESP_LOGE(TAG, "TX message overflow");
|
||||
return;
|
||||
}
|
||||
tx_msg[tx_msg_len++] = buffer[i];
|
||||
}
|
||||
}
|
||||
@@ -216,8 +224,8 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
|
||||
uint8_t unit_idx = msg[4];
|
||||
uint8_t mantissa_range = msg[5];
|
||||
|
||||
if (mantissa_range > 4) {
|
||||
ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range);
|
||||
if (mantissa_range > 4 || msg_len < 7 + mantissa_range) {
|
||||
ESP_LOGE(TAG, "Received invalid message (mantissa size %d, msg_len %d)", mantissa_range, msg_len);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -413,24 +413,29 @@ void LD2412Component::handle_periodic_data_() {
|
||||
this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
|
||||
}
|
||||
if (engineering_mode) {
|
||||
/*
|
||||
Moving distance range: 18th byte
|
||||
Still distance range: 19th byte
|
||||
Moving energy: 20~28th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
|
||||
// Engineering mode needs at least LIGHT_SENSOR + 1 bytes
|
||||
if (this->buffer_pos_ < LIGHT_SENSOR + 1) {
|
||||
ESP_LOGW(TAG, "Engineering mode packet too short: %u", this->buffer_pos_);
|
||||
} else {
|
||||
/*
|
||||
Moving distance range: 18th byte
|
||||
Still distance range: 19th byte
|
||||
Moving energy: 20~28th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Still energy: 29~37th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Light sensor value
|
||||
*/
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
|
||||
}
|
||||
/*
|
||||
Still energy: 29~37th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
|
||||
}
|
||||
/*
|
||||
Light sensor: 38th bytes
|
||||
*/
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
|
||||
} else {
|
||||
for (auto &gate_move_sensor : this->gate_move_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
|
||||
|
||||
@@ -133,7 +133,7 @@ static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
|
||||
static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; };
|
||||
|
||||
static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
|
||||
@@ -168,7 +168,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
uint32_t presence_millis_ = 0;
|
||||
uint32_t still_presence_millis_ = 0;
|
||||
uint32_t moving_presence_millis_ = 0;
|
||||
uint16_t timeout_ = 5;
|
||||
uint32_t timeout_ = 5;
|
||||
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||
uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
|
||||
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
|
||||
|
||||
@@ -76,6 +76,9 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n
|
||||
init_result = ledc_timer_config(&timer_conf);
|
||||
if (init_result != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth);
|
||||
if (bit_depth <= 1) {
|
||||
break;
|
||||
}
|
||||
// try again with a lower bit depth
|
||||
timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(--bit_depth);
|
||||
}
|
||||
|
||||
@@ -324,6 +324,8 @@ class AddressableFireworksEffect : public AddressableLightEffect {
|
||||
target *= 170;
|
||||
view = target;
|
||||
}
|
||||
if (it.size() < 2)
|
||||
return;
|
||||
int last = it.size() - 1;
|
||||
it[0].set(it[0].get() + (it[1].get() * 128));
|
||||
for (int i = 1; i < last; i++) {
|
||||
|
||||
@@ -163,8 +163,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti
|
||||
void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_))
|
||||
return;
|
||||
size_t start = this->current_page_;
|
||||
do {
|
||||
this->current_page_ = (this->current_page_ + 1) % this->pages_.size();
|
||||
if (this->current_page_ == start)
|
||||
return; // all pages have skip=true (guaranteed not to happen by YAML validation)
|
||||
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
|
||||
this->show_page(this->current_page_, anim, time);
|
||||
}
|
||||
@@ -172,8 +175,11 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_))
|
||||
return;
|
||||
size_t start = this->current_page_;
|
||||
do {
|
||||
this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size();
|
||||
if (this->current_page_ == start)
|
||||
return; // all pages have skip=true (guaranteed not to happen by YAML validation)
|
||||
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
|
||||
this->show_page(this->current_page_, anim, time);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MAX9611Component),
|
||||
cv.Required(CONF_SHUNT_RESISTANCE): cv.resistance,
|
||||
cv.Required(CONF_SHUNT_RESISTANCE): cv.All(
|
||||
cv.resistance, cv.Range(min=1e-6)
|
||||
),
|
||||
cv.Required(CONF_GAIN): cv.enum(MAX9611_GAIN, upper=True),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
|
||||
@@ -47,12 +47,12 @@ void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr
|
||||
case mcp23xxx_base::MCP23XXX_RISING:
|
||||
this->update_reg(pin, true, gpinten);
|
||||
this->update_reg(pin, true, intcon);
|
||||
this->update_reg(pin, true, defval);
|
||||
this->update_reg(pin, false, defval);
|
||||
break;
|
||||
case mcp23xxx_base::MCP23XXX_FALLING:
|
||||
this->update_reg(pin, true, gpinten);
|
||||
this->update_reg(pin, true, intcon);
|
||||
this->update_reg(pin, false, defval);
|
||||
this->update_reg(pin, true, defval);
|
||||
break;
|
||||
case mcp23xxx_base::MCP23XXX_NO_INTERRUPT:
|
||||
this->update_reg(pin, false, gpinten);
|
||||
|
||||
@@ -59,12 +59,12 @@ void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr
|
||||
case mcp23xxx_base::MCP23XXX_RISING:
|
||||
this->update_reg(pin, true, gpinten);
|
||||
this->update_reg(pin, true, intcon);
|
||||
this->update_reg(pin, true, defval);
|
||||
this->update_reg(pin, false, defval);
|
||||
break;
|
||||
case mcp23xxx_base::MCP23XXX_FALLING:
|
||||
this->update_reg(pin, true, gpinten);
|
||||
this->update_reg(pin, true, intcon);
|
||||
this->update_reg(pin, false, defval);
|
||||
this->update_reg(pin, true, defval);
|
||||
break;
|
||||
case mcp23xxx_base::MCP23XXX_NO_INTERRUPT:
|
||||
this->update_reg(pin, false, gpinten);
|
||||
|
||||
@@ -506,6 +506,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo
|
||||
cfg3 = MCP_12MHZ_40KBPS_CFG3;
|
||||
break;
|
||||
case (canbus::CAN_50KBPS): // 50Kbps
|
||||
cfg1 = MCP_12MHZ_50KBPS_CFG1;
|
||||
cfg2 = MCP_12MHZ_50KBPS_CFG2;
|
||||
cfg3 = MCP_12MHZ_50KBPS_CFG3;
|
||||
break;
|
||||
|
||||
@@ -79,8 +79,8 @@ void Mcp4461Component::dump_config() {
|
||||
// reworked to be a one-line intentionally, as output would not be in order
|
||||
if (i < 4) {
|
||||
ESP_LOGCONFIG(TAG, " ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, A: %s, B: %s, W: %s", i,
|
||||
this->reg_[i].state, ONOFF(this->reg_[i].terminal_hw), ONOFF(this->reg_[i].terminal_a),
|
||||
ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w), ONOFF(this->reg_[i].enabled));
|
||||
this->reg_[i].state, ONOFF(this->reg_[i].enabled), ONOFF(this->reg_[i].terminal_hw),
|
||||
ONOFF(this->reg_[i].terminal_a), ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w));
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state);
|
||||
}
|
||||
@@ -315,9 +315,9 @@ void Mcp4461Component::disable_wiper_(Mcp4461WiperIdx wiper) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Disabling wiper %u", wiper_idx);
|
||||
this->reg_[wiper_idx].enabled = true;
|
||||
this->reg_[wiper_idx].enabled = false;
|
||||
if (wiper_idx < 4) {
|
||||
this->reg_[wiper_idx].terminal_hw = true;
|
||||
this->reg_[wiper_idx].terminal_hw = false;
|
||||
this->reg_[wiper_idx].update_terminal = true;
|
||||
}
|
||||
}
|
||||
@@ -490,7 +490,7 @@ void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) {
|
||||
ESP_LOGW(TAG, "Unknown terminal %c specified", terminal);
|
||||
return;
|
||||
}
|
||||
this->reg_[wiper_idx].update_terminal = false;
|
||||
this->reg_[wiper_idx].update_terminal = true;
|
||||
}
|
||||
|
||||
void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) {
|
||||
@@ -517,7 +517,7 @@ void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) {
|
||||
ESP_LOGW(TAG, "Unknown terminal %c specified", terminal);
|
||||
return;
|
||||
}
|
||||
this->reg_[wiper_idx].update_terminal = false;
|
||||
this->reg_[wiper_idx].update_terminal = true;
|
||||
}
|
||||
|
||||
uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) {
|
||||
|
||||
@@ -58,7 +58,7 @@ class MCP4728Component : public Component, public i2c::I2CDevice {
|
||||
void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain);
|
||||
|
||||
private:
|
||||
DACInputData reg_[4];
|
||||
DACInputData reg_[4]{};
|
||||
bool store_in_eeprom_ = false;
|
||||
bool update_ = false;
|
||||
};
|
||||
|
||||
@@ -28,12 +28,14 @@ class UARTStream : public Stream {
|
||||
int available() override { return this->uart_->available(); }
|
||||
int read() override {
|
||||
uint8_t data;
|
||||
this->uart_->read_byte(&data);
|
||||
if (!this->uart_->read_byte(&data))
|
||||
return -1;
|
||||
return data;
|
||||
}
|
||||
int peek() override {
|
||||
uint8_t data;
|
||||
this->uart_->peek_byte(&data);
|
||||
if (!this->uart_->peek_byte(&data))
|
||||
return -1;
|
||||
return data;
|
||||
}
|
||||
size_t write(uint8_t data) override {
|
||||
|
||||
@@ -27,7 +27,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; }
|
||||
|
||||
protected:
|
||||
MMC5603Datarate datarate_;
|
||||
MMC5603Datarate datarate_{MMC5603_DATARATE_75_0_HZ};
|
||||
sensor::Sensor *x_sensor_{nullptr};
|
||||
sensor::Sensor *y_sensor_{nullptr};
|
||||
sensor::Sensor *z_sensor_{nullptr};
|
||||
@@ -37,7 +37,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
ID_REGISTERS,
|
||||
} error_code_;
|
||||
} error_code_{NONE};
|
||||
};
|
||||
|
||||
} // namespace mmc5603
|
||||
|
||||
@@ -59,6 +59,10 @@ bool ModbusController::send_next_command_() {
|
||||
|
||||
// Queue incoming response
|
||||
void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
if (this->command_queue_.empty()) {
|
||||
ESP_LOGW(TAG, "Received modbus data but command queue is empty");
|
||||
return;
|
||||
}
|
||||
auto ¤t_command = this->command_queue_.front();
|
||||
if (current_command != nullptr) {
|
||||
if (this->module_offline_) {
|
||||
@@ -92,6 +96,9 @@ void ModbusController::process_modbus_data_(const ModbusCommandItem *response) {
|
||||
|
||||
void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) {
|
||||
ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code);
|
||||
if (this->command_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
// Remove pending command waiting for a response
|
||||
auto ¤t_command = this->command_queue_.front();
|
||||
if (current_command != nullptr) {
|
||||
@@ -175,6 +182,11 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
|
||||
uint16_t payload_offset;
|
||||
|
||||
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||
if (data.size() < 5) {
|
||||
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
|
||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
|
||||
@@ -188,8 +200,19 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
if (data.size() < 5 + payload_size) {
|
||||
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
|
||||
5 + payload_size);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
payload_offset = 5;
|
||||
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
||||
if (data.size() < 4) {
|
||||
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
number_of_registers = 1;
|
||||
payload_offset = 2;
|
||||
} else {
|
||||
|
||||
@@ -126,18 +126,18 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
// Copy measurements over into my array.
|
||||
{
|
||||
u_int8_t measurements_index = 0;
|
||||
for (u_int8_t i = 0; i < 3; i++) {
|
||||
measurements_time[measurements_index] = mopeka_data->val[i].time_0 + 1;
|
||||
measurements_value[measurements_index] = mopeka_data->val[i].value_0;
|
||||
for (const auto &val : mopeka_data->val) {
|
||||
measurements_time[measurements_index] = val.time_0 + 1;
|
||||
measurements_value[measurements_index] = val.value_0;
|
||||
measurements_index++;
|
||||
measurements_time[measurements_index] = mopeka_data->val[i].time_1 + 1;
|
||||
measurements_value[measurements_index] = mopeka_data->val[i].value_1;
|
||||
measurements_time[measurements_index] = val.time_1 + 1;
|
||||
measurements_value[measurements_index] = val.value_1;
|
||||
measurements_index++;
|
||||
measurements_time[measurements_index] = mopeka_data->val[i].time_2 + 1;
|
||||
measurements_value[measurements_index] = mopeka_data->val[i].value_2;
|
||||
measurements_time[measurements_index] = val.time_2 + 1;
|
||||
measurements_value[measurements_index] = val.value_2;
|
||||
measurements_index++;
|
||||
measurements_time[measurements_index] = mopeka_data->val[i].time_3 + 1;
|
||||
measurements_value[measurements_index] = mopeka_data->val[i].value_3;
|
||||
measurements_time[measurements_index] = val.time_3 + 1;
|
||||
measurements_value[measurements_index] = val.value_3;
|
||||
measurements_index++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-stru
|
||||
bool slow_update_rate : 1;
|
||||
bool sync_pressed : 1;
|
||||
|
||||
mopeka_std_values val[4];
|
||||
mopeka_std_values val[3];
|
||||
} __attribute__((packed));
|
||||
|
||||
class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
|
||||
|
||||
@@ -44,8 +44,10 @@ MQTTClientComponent::MQTTClientComponent() {
|
||||
void MQTTClientComponent::setup() {
|
||||
this->mqtt_backend_.set_on_message(
|
||||
[this](const char *topic, const char *payload, size_t len, size_t index, size_t total) {
|
||||
if (index == 0)
|
||||
if (index == 0) {
|
||||
this->payload_buffer_.clear();
|
||||
this->payload_buffer_.reserve(total);
|
||||
}
|
||||
|
||||
// append new payload, may contain incomplete MQTT message
|
||||
this->payload_buffer_.append(payload, len);
|
||||
|
||||
@@ -646,16 +646,12 @@ void Nextion::process_nextion_commands_() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (to_process_length == 0) {
|
||||
ESP_LOGE(TAG, "Numeric return but no data");
|
||||
if (to_process_length < 4) {
|
||||
ESP_LOGE(TAG, "Numeric return but insufficient data (need 4, got %zu)", to_process_length);
|
||||
break;
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
value += to_process[i] << (8 * i);
|
||||
}
|
||||
int value = static_cast<int>(encode_uint32(to_process[3], to_process[2], to_process[1], to_process[0]));
|
||||
|
||||
NextionQueue *nb = this->nextion_queue_.front();
|
||||
if (!nb || !nb->component) {
|
||||
@@ -751,10 +747,8 @@ void Nextion::process_nextion_commands_() {
|
||||
index = to_process.find('\0');
|
||||
variable_name = to_process.substr(0, index);
|
||||
// // Get variable name
|
||||
int value = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
value += to_process[i + index + 1] << (8 * i);
|
||||
}
|
||||
int value = static_cast<int>(
|
||||
encode_uint32(to_process[index + 4], to_process[index + 3], to_process[index + 2], to_process[index + 1]));
|
||||
|
||||
ESP_LOGN(TAG, "Sensor: %s=%d", variable_name.c_str(), value);
|
||||
|
||||
|
||||
@@ -86,6 +86,12 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
|
||||
ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
|
||||
EspClass::getFreeHeap());
|
||||
upload_first_chunk_sent_ = true;
|
||||
if (recv_string.empty()) {
|
||||
ESP_LOGW(TAG, "No response from display during upload");
|
||||
allocator.deallocate(buffer, 4096);
|
||||
buffer = nullptr;
|
||||
return -1;
|
||||
}
|
||||
if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
|
||||
char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
|
||||
ESP_LOGD(
|
||||
|
||||
@@ -108,6 +108,12 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
|
||||
static_cast<uint32_t>(esp_get_free_heap_size()));
|
||||
#endif
|
||||
upload_first_chunk_sent_ = true;
|
||||
if (recv_string.empty()) {
|
||||
ESP_LOGW(TAG, "No response from display during upload");
|
||||
allocator.deallocate(buffer, 4096);
|
||||
buffer = nullptr;
|
||||
return -1;
|
||||
}
|
||||
if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
|
||||
char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
|
||||
ESP_LOGD(
|
||||
|
||||
@@ -192,8 +192,13 @@ bool Pipsolar::send_next_command_() {
|
||||
if (!this->command_queue_[this->command_queue_position_].empty()) {
|
||||
const char *command = this->command_queue_[this->command_queue_position_].c_str();
|
||||
uint8_t byte_command[16];
|
||||
uint8_t length = this->command_queue_[this->command_queue_position_].length();
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
size_t length = this->command_queue_[this->command_queue_position_].length();
|
||||
if (length > sizeof(byte_command)) {
|
||||
ESP_LOGE(TAG, "Command too long: %zu", length);
|
||||
this->command_queue_[this->command_queue_position_].clear();
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i);
|
||||
}
|
||||
this->state_ = STATE_COMMAND;
|
||||
|
||||
@@ -34,7 +34,8 @@ uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) {
|
||||
}
|
||||
|
||||
uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) {
|
||||
if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) {
|
||||
auto encoded = tx.encode();
|
||||
if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) {
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
return nfc::STATUS_FAILED;
|
||||
|
||||
@@ -34,7 +34,8 @@ uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) {
|
||||
}
|
||||
|
||||
uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) {
|
||||
if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) {
|
||||
auto encoded = tx.encode();
|
||||
if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) {
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
return nfc::STATUS_FAILED;
|
||||
|
||||
@@ -146,16 +146,19 @@ void Rtttl::loop() {
|
||||
}
|
||||
#endif // USE_SPEAKER
|
||||
|
||||
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
|
||||
while (this->position_ < this->rtttl_.length()) {
|
||||
char c = this->rtttl_[this->position_];
|
||||
if (c != ',' && c != ' ')
|
||||
break;
|
||||
this->position_++;
|
||||
}
|
||||
|
||||
if (this->position_ >= this->rtttl_.length()) {
|
||||
this->finish_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
|
||||
while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') {
|
||||
this->position_++;
|
||||
}
|
||||
|
||||
// First, get note duration, if available
|
||||
uint8_t note_denominator = this->get_integer_();
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
|
||||
switch (this->bits_per_pixel_) {
|
||||
case 1:
|
||||
this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1);
|
||||
this->width_bytes_ = (this->width_ + 7) / 8;
|
||||
this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4;
|
||||
break;
|
||||
case 24:
|
||||
this->width_bytes_ = this->width_ * 3;
|
||||
@@ -92,15 +93,26 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
case 1: {
|
||||
while (index < size) {
|
||||
uint8_t current_byte = buffer[index];
|
||||
bool end_of_row = false;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
size_t x = (this->paint_index_ % static_cast<size_t>(this->width_)) + i;
|
||||
size_t x = this->paint_index_ % static_cast<size_t>(this->width_);
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
|
||||
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
|
||||
this->draw(x, y, 1, 1, c);
|
||||
this->paint_index_++;
|
||||
// End of pixel row: skip remaining bits in this byte
|
||||
if (x + 1 >= static_cast<size_t>(this->width_)) {
|
||||
end_of_row = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->paint_index_ += 8;
|
||||
this->current_index_++;
|
||||
index++;
|
||||
// End of pixel row: skip row padding bytes (4-byte alignment)
|
||||
if (end_of_row && this->padding_bytes_ > 0) {
|
||||
index += this->padding_bytes_;
|
||||
this->current_index_ += this->padding_bytes_;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP
|
||||
const float temperature = temp_sign == 0 ? temp_val : -1 * temp_val;
|
||||
|
||||
const float humidity = data[0] * 0.5f;
|
||||
const float pressure = (uint16_t(data[3] << 8) + uint16_t(data[4]) + 50000.0f) / 100.0f;
|
||||
const float acceleration_x = (int16_t(data[5] << 8) + int16_t(data[6])) / 1000.0f;
|
||||
const float acceleration_y = (int16_t(data[7] << 8) + int16_t(data[8])) / 1000.0f;
|
||||
const float acceleration_z = (int16_t(data[9] << 8) + int16_t(data[10])) / 1000.0f;
|
||||
const float battery_voltage = (uint16_t(data[11] << 8) + uint16_t(data[12])) / 1000.0f;
|
||||
const float pressure = (encode_uint16(data[3], data[4]) + 50000.0f) / 100.0f;
|
||||
const float acceleration_x = static_cast<int16_t>(encode_uint16(data[5], data[6])) / 1000.0f;
|
||||
const float acceleration_y = static_cast<int16_t>(encode_uint16(data[7], data[8])) / 1000.0f;
|
||||
const float acceleration_z = static_cast<int16_t>(encode_uint16(data[9], data[10])) / 1000.0f;
|
||||
const float battery_voltage = encode_uint16(data[11], data[12]) / 1000.0f;
|
||||
|
||||
result.humidity = humidity;
|
||||
result.temperature = temperature;
|
||||
@@ -43,19 +43,19 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP
|
||||
if (adv_data.size() != 24)
|
||||
return false;
|
||||
|
||||
const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f;
|
||||
const float humidity = (uint16_t(data[2] << 8) | uint16_t(data[3])) / 400.0f;
|
||||
const float pressure = ((uint16_t(data[4] << 8) | uint16_t(data[5])) + 50000.0f) / 100.0f;
|
||||
const float acceleration_x = (int16_t(data[6] << 8) + int16_t(data[7])) / 1000.0f;
|
||||
const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f;
|
||||
const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f;
|
||||
const float temperature = static_cast<int16_t>(encode_uint16(data[0], data[1])) * 0.005f;
|
||||
const float humidity = encode_uint16(data[2], data[3]) / 400.0f;
|
||||
const float pressure = (encode_uint16(data[4], data[5]) + 50000.0f) / 100.0f;
|
||||
const float acceleration_x = static_cast<int16_t>(encode_uint16(data[6], data[7])) / 1000.0f;
|
||||
const float acceleration_y = static_cast<int16_t>(encode_uint16(data[8], data[9])) / 1000.0f;
|
||||
const float acceleration_z = static_cast<int16_t>(encode_uint16(data[10], data[11])) / 1000.0f;
|
||||
|
||||
const uint16_t power_info = (uint16_t(data[12] << 8) | data[13]);
|
||||
const uint16_t power_info = encode_uint16(data[12], data[13]);
|
||||
const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f;
|
||||
const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f;
|
||||
|
||||
const float movement_counter = float(data[14]);
|
||||
const float measurement_sequence_number = float(uint16_t(data[15] << 8) | uint16_t(data[16]));
|
||||
const float measurement_sequence_number = float(encode_uint16(data[15], data[16]));
|
||||
|
||||
result.temperature = data[0] == 0x7F && data[1] == 0xFF ? NAN : temperature;
|
||||
result.humidity = data[2] == 0xFF && data[3] == 0xFF ? NAN : humidity;
|
||||
|
||||
@@ -75,7 +75,7 @@ void RX8130Component::read_time() {
|
||||
.second = bcd2dec(date[0] & 0x7f),
|
||||
.minute = bcd2dec(date[1] & 0x7f),
|
||||
.hour = bcd2dec(date[2] & 0x3f),
|
||||
.day_of_week = bcd2dec(date[3] & 0x7f),
|
||||
.day_of_week = static_cast<uint8_t>((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1),
|
||||
.day_of_month = bcd2dec(date[4] & 0x3f),
|
||||
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
|
||||
.month = bcd2dec(date[5] & 0x1f),
|
||||
@@ -103,7 +103,7 @@ void RX8130Component::write_time() {
|
||||
buff[0] = dec2bcd(now.second);
|
||||
buff[1] = dec2bcd(now.minute);
|
||||
buff[2] = dec2bcd(now.hour);
|
||||
buff[3] = dec2bcd(now.day_of_week);
|
||||
buff[3] = 1 << (now.day_of_week - 1);
|
||||
buff[4] = dec2bcd(now.day_of_month);
|
||||
buff[5] = dec2bcd(now.month);
|
||||
buff[6] = dec2bcd(now.year % 100);
|
||||
|
||||
@@ -199,7 +199,7 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
|
||||
}
|
||||
break;
|
||||
case DISTANCE_TYPE_BUFFER:
|
||||
if (data[0] != 0) {
|
||||
if (length >= 1 && data[0] != 0) {
|
||||
if (this->distance_sensor_ != nullptr && length >= 8) {
|
||||
uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]);
|
||||
float distance_float;
|
||||
|
||||
+182
-156
@@ -2,13 +2,16 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::sen6x {
|
||||
|
||||
static const char *const TAG = "sen6x";
|
||||
|
||||
static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts
|
||||
static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete
|
||||
static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
|
||||
// Single numeric timeout ID — the chain is sequential so only one is active at a time.
|
||||
static constexpr uint32_t TIMEOUT_POLL = 1;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
|
||||
@@ -182,179 +185,202 @@ void SEN6XComponent::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t read_cmd;
|
||||
uint8_t read_words;
|
||||
set_read_command_and_words(this->sen6x_type_, read_cmd, read_words);
|
||||
// Cancel any in-flight polling from a previous update() cycle.
|
||||
this->cancel_timeout(TIMEOUT_POLL);
|
||||
|
||||
const uint8_t poll_retries = 24;
|
||||
auto poll_ready = std::make_shared<std::function<void(uint8_t)>>();
|
||||
*poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) {
|
||||
const uint8_t attempt = static_cast<uint8_t>(poll_retries - retries_left + 1);
|
||||
ESP_LOGV(TAG, "Data ready polling attempt %u", attempt);
|
||||
set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_);
|
||||
|
||||
if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) {
|
||||
// Polling uses chained timeouts to guarantee each I2C operation completes
|
||||
// before the next begins. The flow is:
|
||||
//
|
||||
// poll_data_ready_()
|
||||
// -> write_command (data ready status)
|
||||
// -> timeout I2C_READ_DELAY
|
||||
// -> read_data (check ready flag)
|
||||
// -> if not ready: timeout POLL_INTERVAL -> poll_data_ready_() (retry)
|
||||
// -> if ready: read_measurements_()
|
||||
// -> write_command (read measurement)
|
||||
// -> timeout I2C_READ_DELAY
|
||||
// -> parse_and_publish_measurements_()
|
||||
//
|
||||
// All timeouts share a single ID (TIMEOUT_POLL) since only one is active
|
||||
// at a time. cancel_timeout in update() stops any in-flight chain.
|
||||
this->poll_retries_remaining_ = POLL_RETRIES;
|
||||
this->poll_data_ready_();
|
||||
}
|
||||
|
||||
void SEN6XComponent::poll_data_ready_() {
|
||||
if (this->poll_retries_remaining_ == 0) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Data not ready");
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Data ready polling attempt %u",
|
||||
static_cast<unsigned>(POLL_RETRIES - this->poll_retries_remaining_ + 1));
|
||||
this->poll_retries_remaining_--;
|
||||
|
||||
if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() {
|
||||
uint16_t raw_read_status;
|
||||
if (!this->read_data(&raw_read_status, 1)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_);
|
||||
ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() {
|
||||
uint16_t raw_read_status;
|
||||
if (!this->read_data(&raw_read_status, 1)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
if ((raw_read_status & 0x0001) == 0) {
|
||||
// Not ready yet; schedule next attempt after POLL_INTERVAL.
|
||||
this->set_timeout(TIMEOUT_POLL, POLL_INTERVAL, [this]() { this->poll_data_ready_(); });
|
||||
return;
|
||||
}
|
||||
|
||||
if ((raw_read_status & 0x0001) == 0) {
|
||||
if (retries_left == 0) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Data not ready");
|
||||
return;
|
||||
}
|
||||
this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); });
|
||||
return;
|
||||
}
|
||||
this->read_measurements_();
|
||||
});
|
||||
}
|
||||
|
||||
if (!this->write_command(read_cmd)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
void SEN6XComponent::read_measurements_() {
|
||||
if (!this->write_command(this->read_cmd_)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this, read_words]() {
|
||||
uint16_t measurements[10];
|
||||
this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_measurements_(); });
|
||||
}
|
||||
|
||||
if (!this->read_data(measurements, read_words)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
int8_t voc_index = -1;
|
||||
int8_t nox_index = -1;
|
||||
int8_t hcho_index = -1;
|
||||
int8_t co2_index = -1;
|
||||
bool co2_uint16 = false;
|
||||
switch (this->sen6x_type_) {
|
||||
case SEN62:
|
||||
break;
|
||||
case SEN63C:
|
||||
co2_index = 6;
|
||||
break;
|
||||
case SEN65:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
break;
|
||||
case SEN66:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
co2_index = 8;
|
||||
co2_uint16 = true;
|
||||
break;
|
||||
case SEN68:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
break;
|
||||
case SEN69C:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
co2_index = 9;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
void SEN6XComponent::parse_and_publish_measurements_() {
|
||||
uint16_t measurements[10];
|
||||
|
||||
float pm_1_0 = measurements[0] / 10.0f;
|
||||
if (measurements[0] == 0xFFFF)
|
||||
pm_1_0 = NAN;
|
||||
float pm_2_5 = measurements[1] / 10.0f;
|
||||
if (measurements[1] == 0xFFFF)
|
||||
pm_2_5 = NAN;
|
||||
float pm_4_0 = measurements[2] / 10.0f;
|
||||
if (measurements[2] == 0xFFFF)
|
||||
pm_4_0 = NAN;
|
||||
float pm_10_0 = measurements[3] / 10.0f;
|
||||
if (measurements[3] == 0xFFFF)
|
||||
pm_10_0 = NAN;
|
||||
float humidity = static_cast<int16_t>(measurements[4]) / 100.0f;
|
||||
if (measurements[4] == 0x7FFF)
|
||||
humidity = NAN;
|
||||
float temperature = static_cast<int16_t>(measurements[5]) / 200.0f;
|
||||
if (measurements[5] == 0x7FFF)
|
||||
temperature = NAN;
|
||||
if (!this->read_data(measurements, this->read_words_)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
int8_t voc_index = -1;
|
||||
int8_t nox_index = -1;
|
||||
int8_t hcho_index = -1;
|
||||
int8_t co2_index = -1;
|
||||
bool co2_uint16 = false;
|
||||
switch (this->sen6x_type_) {
|
||||
case SEN62:
|
||||
break;
|
||||
case SEN63C:
|
||||
co2_index = 6;
|
||||
break;
|
||||
case SEN65:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
break;
|
||||
case SEN66:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
co2_index = 8;
|
||||
co2_uint16 = true;
|
||||
break;
|
||||
case SEN68:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
break;
|
||||
case SEN69C:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
co2_index = 9;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float voc = NAN;
|
||||
float nox = NAN;
|
||||
float hcho = NAN;
|
||||
float co2 = NAN;
|
||||
float pm_1_0 = measurements[0] / 10.0f;
|
||||
if (measurements[0] == 0xFFFF)
|
||||
pm_1_0 = NAN;
|
||||
float pm_2_5 = measurements[1] / 10.0f;
|
||||
if (measurements[1] == 0xFFFF)
|
||||
pm_2_5 = NAN;
|
||||
float pm_4_0 = measurements[2] / 10.0f;
|
||||
if (measurements[2] == 0xFFFF)
|
||||
pm_4_0 = NAN;
|
||||
float pm_10_0 = measurements[3] / 10.0f;
|
||||
if (measurements[3] == 0xFFFF)
|
||||
pm_10_0 = NAN;
|
||||
float humidity = static_cast<int16_t>(measurements[4]) / 100.0f;
|
||||
if (measurements[4] == 0x7FFF)
|
||||
humidity = NAN;
|
||||
float temperature = static_cast<int16_t>(measurements[5]) / 200.0f;
|
||||
if (measurements[5] == 0x7FFF)
|
||||
temperature = NAN;
|
||||
|
||||
if (voc_index >= 0) {
|
||||
voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f;
|
||||
if (measurements[voc_index] == 0x7FFF)
|
||||
voc = NAN;
|
||||
}
|
||||
if (nox_index >= 0) {
|
||||
nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f;
|
||||
if (measurements[nox_index] == 0x7FFF)
|
||||
nox = NAN;
|
||||
}
|
||||
float voc = NAN;
|
||||
float nox = NAN;
|
||||
float hcho = NAN;
|
||||
float co2 = NAN;
|
||||
|
||||
if (hcho_index >= 0) {
|
||||
const uint16_t hcho_raw = measurements[hcho_index];
|
||||
hcho = hcho_raw / 10.0f;
|
||||
if (hcho_raw == 0xFFFF)
|
||||
hcho = NAN;
|
||||
}
|
||||
if (voc_index >= 0) {
|
||||
voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f;
|
||||
if (measurements[voc_index] == 0x7FFF)
|
||||
voc = NAN;
|
||||
}
|
||||
if (nox_index >= 0) {
|
||||
nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f;
|
||||
if (measurements[nox_index] == 0x7FFF)
|
||||
nox = NAN;
|
||||
}
|
||||
|
||||
if (co2_index >= 0) {
|
||||
if (co2_uint16) {
|
||||
const uint16_t co2_raw = measurements[co2_index];
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0xFFFF)
|
||||
co2 = NAN;
|
||||
} else {
|
||||
const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]);
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0x7FFF)
|
||||
co2 = NAN;
|
||||
}
|
||||
}
|
||||
if (hcho_index >= 0) {
|
||||
const uint16_t hcho_raw = measurements[hcho_index];
|
||||
hcho = hcho_raw / 10.0f;
|
||||
if (hcho_raw == 0xFFFF)
|
||||
hcho = NAN;
|
||||
}
|
||||
|
||||
if (!this->startup_complete_) {
|
||||
ESP_LOGD(TAG, "Startup delay, ignoring values");
|
||||
this->status_clear_warning();
|
||||
return;
|
||||
}
|
||||
if (co2_index >= 0) {
|
||||
if (co2_uint16) {
|
||||
const uint16_t co2_raw = measurements[co2_index];
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0xFFFF)
|
||||
co2 = NAN;
|
||||
} else {
|
||||
const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]);
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0x7FFF)
|
||||
co2 = NAN;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->pm_1_0_sensor_ != nullptr)
|
||||
this->pm_1_0_sensor_->publish_state(pm_1_0);
|
||||
if (this->pm_2_5_sensor_ != nullptr)
|
||||
this->pm_2_5_sensor_->publish_state(pm_2_5);
|
||||
if (this->pm_4_0_sensor_ != nullptr)
|
||||
this->pm_4_0_sensor_->publish_state(pm_4_0);
|
||||
if (this->pm_10_0_sensor_ != nullptr)
|
||||
this->pm_10_0_sensor_->publish_state(pm_10_0);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
if (this->voc_sensor_ != nullptr)
|
||||
this->voc_sensor_->publish_state(voc);
|
||||
if (this->nox_sensor_ != nullptr)
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
if (this->hcho_sensor_ != nullptr)
|
||||
this->hcho_sensor_->publish_state(hcho);
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(co2);
|
||||
if (!this->startup_complete_) {
|
||||
ESP_LOGD(TAG, "Startup delay, ignoring values");
|
||||
this->status_clear_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
});
|
||||
});
|
||||
};
|
||||
if (this->pm_1_0_sensor_ != nullptr)
|
||||
this->pm_1_0_sensor_->publish_state(pm_1_0);
|
||||
if (this->pm_2_5_sensor_ != nullptr)
|
||||
this->pm_2_5_sensor_->publish_state(pm_2_5);
|
||||
if (this->pm_4_0_sensor_ != nullptr)
|
||||
this->pm_4_0_sensor_->publish_state(pm_4_0);
|
||||
if (this->pm_10_0_sensor_ != nullptr)
|
||||
this->pm_10_0_sensor_->publish_state(pm_10_0);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
if (this->voc_sensor_ != nullptr)
|
||||
this->voc_sensor_->publish_state(voc);
|
||||
if (this->nox_sensor_ != nullptr)
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
if (this->hcho_sensor_ != nullptr)
|
||||
this->hcho_sensor_->publish_state(hcho);
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(co2);
|
||||
|
||||
(*poll_ready)(poll_retries);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
|
||||
|
||||
@@ -30,13 +30,19 @@ class SEN6XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
|
||||
protected:
|
||||
Sen6xType infer_type_from_product_name_(const std::string &product_name);
|
||||
void poll_data_ready_();
|
||||
void read_measurements_();
|
||||
void parse_and_publish_measurements_();
|
||||
|
||||
bool initialized_{false};
|
||||
std::string product_name_;
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
std::string serial_number_;
|
||||
uint16_t read_cmd_{0};
|
||||
uint8_t firmware_version_major_{0};
|
||||
uint8_t firmware_version_minor_{0};
|
||||
uint8_t poll_retries_remaining_{0};
|
||||
uint8_t read_words_{0};
|
||||
bool startup_complete_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ class SerialProxy : public uart::UARTDevice, public Component {
|
||||
/// @param data_size Number of data bits (5-8)
|
||||
void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size);
|
||||
|
||||
/// Get the currently subscribed API connection (nullptr if none)
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
|
||||
/// Handle a subscribe/unsubscribe request from an API client
|
||||
void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type);
|
||||
|
||||
|
||||
@@ -14,11 +14,26 @@ void SMT100Component::update() {
|
||||
void SMT100Component::loop() {
|
||||
while (this->available() != 0) {
|
||||
if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) {
|
||||
int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10);
|
||||
float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr);
|
||||
float moisture = (float) strtod((strtok(nullptr, ",")), nullptr);
|
||||
float temperature = (float) strtod((strtok(nullptr, ",")), nullptr);
|
||||
float voltage = (float) strtod((strtok(nullptr, ",")), nullptr);
|
||||
char *token = strtok(this->readline_buffer_, ",");
|
||||
if (!token)
|
||||
continue;
|
||||
int counts = (int) strtol(token, nullptr, 10);
|
||||
token = strtok(nullptr, ",");
|
||||
if (!token)
|
||||
continue;
|
||||
float permittivity = (float) strtod(token, nullptr);
|
||||
token = strtok(nullptr, ",");
|
||||
if (!token)
|
||||
continue;
|
||||
float moisture = (float) strtod(token, nullptr);
|
||||
token = strtok(nullptr, ",");
|
||||
if (!token)
|
||||
continue;
|
||||
float temperature = (float) strtod(token, nullptr);
|
||||
token = strtok(nullptr, ",");
|
||||
if (!token)
|
||||
continue;
|
||||
float voltage = (float) strtod(token, nullptr);
|
||||
|
||||
if (this->counts_sensor_ != nullptr) {
|
||||
counts_sensor_->publish_state(counts);
|
||||
|
||||
@@ -36,11 +36,13 @@ void ST7701S::setup() {
|
||||
config.de_gpio_num = this->de_pin_->get_pin();
|
||||
config.pclk_gpio_num = this->pclk_pin_->get_pin();
|
||||
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
|
||||
}
|
||||
|
||||
void ST7701S::loop() {
|
||||
|
||||
@@ -99,7 +99,7 @@ struct MessageHeader {
|
||||
|
||||
// payload_size returns the amount of payload bytes to be read from the uart
|
||||
// buffer after reading the header.
|
||||
uint32_t payload_size() { return this->len - sizeof(this->type); }
|
||||
uint32_t payload_size() { return this->len > sizeof(this->type) ? this->len - sizeof(this->type) : 0; }
|
||||
} __attribute__((packed));
|
||||
|
||||
// StatusType denotes which 'page' of information needs to be retrieved.
|
||||
|
||||
@@ -951,10 +951,10 @@ void ToshibaClimate::transmit_ras_2819t_() {
|
||||
}
|
||||
|
||||
uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) {
|
||||
const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
|
||||
RAC_PT1411HWRU_SWING_HEADER};
|
||||
static constexpr uint8_t HEADERS[] = {RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
|
||||
RAC_PT1411HWRU_SWING_HEADER};
|
||||
|
||||
for (auto i : header) {
|
||||
for (auto i : HEADERS) {
|
||||
if ((message[0] == i) && (message[1] == static_cast<uint8_t>(~i)))
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -496,6 +496,11 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
|
||||
ESP_LOGE(TAG, "Too many requests queued");
|
||||
return false;
|
||||
}
|
||||
if (length > trq->transfer->data_buffer_size) {
|
||||
ESP_LOGE(TAG, "transfer_in: data length %u exceeds buffer size %u", length, trq->transfer->data_buffer_size);
|
||||
this->release_trq(trq);
|
||||
return false;
|
||||
}
|
||||
trq->callback = callback;
|
||||
trq->transfer->callback = transfer_callback;
|
||||
trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN;
|
||||
|
||||
@@ -65,7 +65,7 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) {
|
||||
auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset);
|
||||
auto data_desc = usb_parse_interface_descriptor(config_desc, i, 0, &conf_offset);
|
||||
if (!data_desc) {
|
||||
ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed");
|
||||
break;
|
||||
|
||||
@@ -106,10 +106,15 @@ std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev
|
||||
}
|
||||
|
||||
void RingBuffer::push(uint8_t item) {
|
||||
if (this->get_free_space() == 0)
|
||||
return;
|
||||
this->buffer_[this->insert_pos_] = item;
|
||||
this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
|
||||
}
|
||||
void RingBuffer::push(const uint8_t *data, size_t len) {
|
||||
size_t free = this->get_free_space();
|
||||
if (len > free)
|
||||
len = free;
|
||||
for (size_t i = 0; i != len; i++) {
|
||||
this->buffer_[this->insert_pos_] = *data++;
|
||||
this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
|
||||
|
||||
@@ -87,9 +87,9 @@ void VL53L0XSensor::setup() {
|
||||
reg(0x94) = 0x6B;
|
||||
reg(0x83) = 0x00;
|
||||
|
||||
this->timeout_start_us_ = micros();
|
||||
uint32_t timeout_start_us = micros();
|
||||
while (reg(0x83).get() == 0x00) {
|
||||
if (this->timeout_us_ > 0 && ((uint16_t) (micros() - this->timeout_start_us_) > this->timeout_us_)) {
|
||||
if (this->timeout_us_ > 0 && (micros() - timeout_start_us > this->timeout_us_)) {
|
||||
ESP_LOGE(TAG, "'%s' - setup timeout", this->name_.c_str());
|
||||
this->mark_failed();
|
||||
return;
|
||||
|
||||
@@ -64,8 +64,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c
|
||||
bool waiting_for_interrupt_{false};
|
||||
uint8_t stop_variable_;
|
||||
|
||||
uint16_t timeout_start_us_;
|
||||
uint16_t timeout_us_{};
|
||||
uint32_t timeout_us_{};
|
||||
|
||||
static std::list<VL53L0XSensor *> vl53_sensors; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool enable_pin_setup_complete; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -639,8 +639,6 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
static bool first_scan = false;
|
||||
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
@@ -657,23 +655,13 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
config.show_hidden = 1;
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
|
||||
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
|
||||
if (first_scan) {
|
||||
if (passive) {
|
||||
config.scan_time.passive = 200;
|
||||
} else {
|
||||
config.scan_time.active.min = 100;
|
||||
config.scan_time.active.max = 200;
|
||||
}
|
||||
if (passive) {
|
||||
config.scan_time.passive = 500;
|
||||
} else {
|
||||
if (passive) {
|
||||
config.scan_time.passive = 500;
|
||||
} else {
|
||||
config.scan_time.active.min = 400;
|
||||
config.scan_time.active.max = 500;
|
||||
}
|
||||
config.scan_time.active.min = 400;
|
||||
config.scan_time.active.max = 500;
|
||||
}
|
||||
#endif
|
||||
first_scan = false;
|
||||
bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
|
||||
if (!ret) {
|
||||
ESP_LOGV(TAG, "wifi_station_scan failed");
|
||||
|
||||
@@ -56,7 +56,7 @@ CONFIG_SCHEMA = (
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_K_VALUE, default=4096): cv.uint16_t,
|
||||
cv.Optional(CONF_K_VALUE, default=4096): cv.int_range(min=1, max=65535),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
|
||||
@@ -615,6 +615,10 @@ class EsphomeCore:
|
||||
self.address_cache: AddressCache | None = None
|
||||
# Cached config hash (computed lazily)
|
||||
self._config_hash: int | None = None
|
||||
# True if compiling for C++ unit tests
|
||||
self.cpp_testing = False
|
||||
# Allowlist of components whose to_code should run during C++ testing
|
||||
self.cpp_testing_codegen: set[str] = set()
|
||||
|
||||
def reset(self):
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||
@@ -644,6 +648,8 @@ class EsphomeCore:
|
||||
self.current_component = None
|
||||
self.address_cache = None
|
||||
self._config_hash = None
|
||||
self.cpp_testing = False
|
||||
self.cpp_testing_codegen = set()
|
||||
PIN_SCHEMA_REGISTRY.reset()
|
||||
|
||||
@contextmanager
|
||||
@@ -996,6 +1002,15 @@ class EsphomeCore:
|
||||
"""
|
||||
self.platform_counts[platform_name] += 1
|
||||
|
||||
def testing_ensure_platform_registered(self, platform_name: str) -> None:
|
||||
"""Ensure a platform has at least one entity registered for testing.
|
||||
|
||||
Used during C++ test builds to guarantee USE_* defines are emitted
|
||||
without needing a real component variable.
|
||||
"""
|
||||
if not self.platform_counts[platform_name]:
|
||||
self.platform_counts[platform_name] = 1
|
||||
|
||||
def register_controller(self) -> None:
|
||||
"""Track registration of a Controller for ControllerRegistry StaticVector sizing."""
|
||||
controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0)
|
||||
|
||||
@@ -71,6 +71,11 @@ class ComponentManifest:
|
||||
|
||||
@property
|
||||
def to_code(self) -> Callable[[Any], None] | None:
|
||||
if CORE.cpp_testing:
|
||||
# During C++ testing, only run to_code for allowlisted components
|
||||
name = self.module.__package__.rsplit(".", 1)[-1]
|
||||
if name not in CORE.cpp_testing_codegen:
|
||||
return None
|
||||
return getattr(self.module, "to_code", None)
|
||||
|
||||
@property
|
||||
|
||||
+22
-5
@@ -1,5 +1,6 @@
|
||||
import collections
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -401,11 +402,19 @@ def get_picotool_path(cc_path: str) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def detect_rp2040_bootsel(picotool_path: str | Path) -> int:
|
||||
@dataclass
|
||||
class BootselResult:
|
||||
"""Result of RP2040 BOOTSEL detection."""
|
||||
|
||||
device_count: int
|
||||
permission_error: bool = False
|
||||
|
||||
|
||||
def detect_rp2040_bootsel(picotool_path: str | Path) -> BootselResult:
|
||||
"""Detect RP2040/RP2350 devices in BOOTSEL mode using picotool.
|
||||
|
||||
Returns the number of devices found (by counting 'type:' lines in output),
|
||||
matching PlatformIO's detection approach.
|
||||
Returns a BootselResult with the number of devices found (by counting
|
||||
'type:' lines in output), and whether a permission error was detected.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -414,9 +423,17 @@ def detect_rp2040_bootsel(picotool_path: str | Path) -> int:
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout.count(b"type:")
|
||||
device_count = result.stdout.count(b"type:")
|
||||
if device_count > 0:
|
||||
return BootselResult(device_count)
|
||||
# Check stderr for permission issues — picotool can see the device
|
||||
# on the USB bus but can't connect without proper permissions
|
||||
combined = result.stderr + result.stdout
|
||||
if b"unable to connect" in combined or b"LIBUSB_ERROR_ACCESS" in combined:
|
||||
return BootselResult(0, permission_error=True)
|
||||
return BootselResult(0)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return 0
|
||||
return BootselResult(0)
|
||||
|
||||
|
||||
def get_esp32_arduino_flash_error_help() -> str | None:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"]
|
||||
requires = ["setuptools==82.0.1", "wheel>=0.43,<0.47"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
|
||||
+95
-58
@@ -10,9 +10,10 @@ from helpers import get_all_components, get_all_dependencies, root_path
|
||||
|
||||
from esphome.__main__ import command_compile, parse_args
|
||||
from esphome.config import validate_config
|
||||
from esphome.const import CONF_PLATFORM
|
||||
from esphome.core import CORE
|
||||
from esphome.loader import get_component
|
||||
from esphome.platformio_api import get_idedata
|
||||
from esphome.yaml_util import load_yaml
|
||||
|
||||
# This must coincide with the version in /platformio.ini
|
||||
PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2"
|
||||
@@ -20,6 +21,13 @@ PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2"
|
||||
# Path to /tests/components
|
||||
COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components"
|
||||
|
||||
# Components whose to_code should run during C++ test builds.
|
||||
# Most components don't need code generation for tests; only these
|
||||
# essential ones (platform setup, logging, core config) are needed.
|
||||
# Note: "core" is the esphome core config module (esphome/core/config.py),
|
||||
# which registers under package name "core" not "esphome".
|
||||
CPP_TESTING_CODEGEN_COMPONENTS = {"core", "host", "logger"}
|
||||
|
||||
|
||||
def hash_components(components: list[str]) -> str:
|
||||
key = ",".join(components)
|
||||
@@ -30,12 +38,14 @@ def filter_components_without_tests(components: list[str]) -> list[str]:
|
||||
"""Filter out components that do not have a corresponding test file.
|
||||
|
||||
This is done by checking if the component's directory contains at
|
||||
least a .cpp file.
|
||||
least a .cpp or .h file.
|
||||
"""
|
||||
filtered_components: list[str] = []
|
||||
for component in components:
|
||||
test_dir = COMPONENTS_TESTS_DIR / component
|
||||
if test_dir.is_dir() and any(test_dir.glob("*.cpp")):
|
||||
if test_dir.is_dir() and (
|
||||
any(test_dir.glob("*.cpp")) or any(test_dir.glob("*.h"))
|
||||
):
|
||||
filtered_components.append(component)
|
||||
else:
|
||||
print(
|
||||
@@ -45,38 +55,6 @@ def filter_components_without_tests(components: list[str]) -> list[str]:
|
||||
return filtered_components
|
||||
|
||||
|
||||
# Name of optional per-component YAML config merged into the test build
|
||||
# before validation so that platform defines (USE_SENSOR, etc.) are generated.
|
||||
CPP_TEST_CONFIG_FILE = "cpp_test.yaml"
|
||||
|
||||
|
||||
def load_component_test_configs(components: list[str]) -> dict:
|
||||
"""Load cpp_test.yaml files from test component directories.
|
||||
|
||||
These configs are merged into the base test config *before* validation
|
||||
so that entity registration runs during code generation, which causes
|
||||
the corresponding USE_* defines to be emitted.
|
||||
"""
|
||||
merged: dict = {}
|
||||
for component in components:
|
||||
config_file = COMPONENTS_TESTS_DIR / component / CPP_TEST_CONFIG_FILE
|
||||
if not config_file.exists():
|
||||
continue
|
||||
component_config = load_yaml(config_file)
|
||||
if not component_config:
|
||||
continue
|
||||
for key, value in component_config.items():
|
||||
if (
|
||||
key in merged
|
||||
and isinstance(merged[key], list)
|
||||
and isinstance(value, list)
|
||||
):
|
||||
merged[key].extend(value)
|
||||
else:
|
||||
merged[key] = value
|
||||
return merged
|
||||
|
||||
|
||||
def create_test_config(config_name: str, includes: list[str]) -> dict:
|
||||
"""Create ESPHome test configuration for C++ unit tests.
|
||||
|
||||
@@ -114,11 +92,52 @@ def create_test_config(config_name: str, includes: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def get_platform_components(components: list[str]) -> list[str]:
|
||||
"""Discover platform sub-components referenced by test directory structure.
|
||||
|
||||
For each component being tested, any sub-directory named after a platform
|
||||
domain (e.g. ``sensor``, ``binary_sensor``) is treated as a request to
|
||||
include that ``<domain>.<component>`` platform in the build. The sub-
|
||||
directory must name a valid platform domain; anything else raises an error
|
||||
so that typos are caught early.
|
||||
|
||||
Returns:
|
||||
List of ``"domain.component"`` strings, one per discovered sub-directory.
|
||||
"""
|
||||
platform_components: list[str] = []
|
||||
for component in components:
|
||||
test_dir = COMPONENTS_TESTS_DIR / component
|
||||
if not test_dir.is_dir():
|
||||
continue
|
||||
# Each sub-directory name is expected to be a platform domain
|
||||
# (e.g. tests/components/bthome/sensor/ → sensor.bthome).
|
||||
for domain_dir in test_dir.iterdir():
|
||||
if not domain_dir.is_dir():
|
||||
continue
|
||||
domain = domain_dir.name
|
||||
domain_module = get_component(domain)
|
||||
if domain_module is None or not domain_module.is_platform_component:
|
||||
raise ValueError(
|
||||
f"Component tests for '{component}' reference non-existing or invalid domain '{domain}'"
|
||||
f" in its directory structure. See ({COMPONENTS_TESTS_DIR / component / domain})."
|
||||
)
|
||||
platform_components.append(f"{domain}.{component}")
|
||||
return platform_components
|
||||
|
||||
|
||||
# Exit codes for run_tests
|
||||
EXIT_OK = 0
|
||||
EXIT_SKIPPED = 1
|
||||
EXIT_COMPILE_ERROR = 2
|
||||
EXIT_CONFIG_ERROR = 3
|
||||
EXIT_NO_EXECUTABLE = 4
|
||||
|
||||
|
||||
def run_tests(selected_components: list[str]) -> int:
|
||||
# Skip tests on Windows
|
||||
if os.name == "nt":
|
||||
print("Skipping esphome tests on Windows", file=sys.stderr)
|
||||
return 1
|
||||
return EXIT_SKIPPED
|
||||
|
||||
# Remove components that do not have tests
|
||||
components = filter_components_without_tests(selected_components)
|
||||
@@ -128,45 +147,63 @@ def run_tests(selected_components: list[str]) -> int:
|
||||
"No components specified or no tests found for the specified components.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
return EXIT_OK
|
||||
|
||||
components = sorted(components)
|
||||
|
||||
# Obtain possible dependencies for the requested components.
|
||||
# Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag,
|
||||
# which causes core/time.h to include components/time/posix_tz.h.
|
||||
components_with_dependencies = sorted(
|
||||
get_all_dependencies(set(components) | {"time"})
|
||||
)
|
||||
|
||||
# Build a list of include folders, one folder per component containing tests.
|
||||
# A special replacement main.cpp is located in /tests/components/main.cpp
|
||||
# Build a list of include folders relative to COMPONENTS_TESTS_DIR. These folders will
|
||||
# be added along with their subfolders.
|
||||
# "main.cpp" is a special entry that points to /tests/components/main.cpp,
|
||||
# which provides a custom test runner entry-point replacing the default one.
|
||||
# Each remaining entry is a component folder whose *.cpp files are compiled.
|
||||
includes: list[str] = ["main.cpp"] + components
|
||||
|
||||
# Obtain a list of platform components to be tested:
|
||||
try:
|
||||
platform_components = get_platform_components(components)
|
||||
except ValueError as e:
|
||||
print(f"Error obtaining platform components: {e}")
|
||||
return EXIT_CONFIG_ERROR
|
||||
|
||||
components = sorted(components + platform_components)
|
||||
|
||||
# Create a unique name for this config based on the actual components being tested
|
||||
# to maximize cache during testing
|
||||
config_name: str = "cpptests-" + hash_components(components)
|
||||
|
||||
config = create_test_config(config_name, includes)
|
||||
# Obtain possible dependencies for the requested components.
|
||||
# Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag,
|
||||
# which causes core/time.h to include components/time/posix_tz.h.
|
||||
components_with_dependencies: list[str] = sorted(
|
||||
get_all_dependencies(set(components) | {"time"}, cpp_testing=True)
|
||||
)
|
||||
|
||||
# Merge component-specific test configs (e.g. sensor instances) before
|
||||
# validation so that entity registration and USE_* defines work.
|
||||
extra_config = load_component_test_configs(components)
|
||||
config.update(extra_config)
|
||||
config = create_test_config(config_name, includes)
|
||||
|
||||
CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml"
|
||||
CORE.dashboard = None
|
||||
CORE.cpp_testing = True
|
||||
CORE.cpp_testing_codegen = CPP_TESTING_CODEGEN_COMPONENTS
|
||||
|
||||
# Validate config will expand the above with defaults:
|
||||
config = validate_config(config, {})
|
||||
|
||||
# Add all components and dependencies to the base configuration after validation, so their files
|
||||
# are added to the build. Use setdefault to avoid overwriting entries that were
|
||||
# already validated (e.g. sensor instances from cpp_test.yaml).
|
||||
for key in components_with_dependencies:
|
||||
config.setdefault(key, {})
|
||||
# are added to the build.
|
||||
for component_name in components_with_dependencies:
|
||||
if "." in component_name:
|
||||
# Format is always "domain.component" (exactly one dot),
|
||||
# as produced by get_platform_components().
|
||||
domain, component = component_name.split(".", maxsplit=1)
|
||||
domain_list = config.setdefault(domain, [])
|
||||
CORE.testing_ensure_platform_registered(domain)
|
||||
domain_list.append({CONF_PLATFORM: component})
|
||||
else:
|
||||
config.setdefault(component_name, [])
|
||||
|
||||
print(f"Testing components: {', '.join(components)}")
|
||||
dependencies = set(components_with_dependencies) - set(components)
|
||||
deps_str = ", ".join(dependencies) if dependencies else "None"
|
||||
print(f"Testing components: {', '.join(components)}. Dependencies: {deps_str}")
|
||||
CORE.config = config
|
||||
args = parse_args(["program", "compile", str(CORE.config_path)])
|
||||
try:
|
||||
@@ -179,13 +216,13 @@ def run_tests(selected_components: list[str]) -> int:
|
||||
print(
|
||||
f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}"
|
||||
)
|
||||
return 2
|
||||
return EXIT_COMPILE_ERROR
|
||||
|
||||
# After a successful compilation, locate the executable and run it:
|
||||
idedata = get_idedata(config)
|
||||
if idedata is None:
|
||||
print("Cannot find executable")
|
||||
return 1
|
||||
return EXIT_NO_EXECUTABLE
|
||||
|
||||
program_path: str = idedata.raw["prog_path"]
|
||||
run_cmd: list[str] = [program_path]
|
||||
|
||||
+17
-4
@@ -15,6 +15,8 @@ from typing import Any
|
||||
|
||||
import colorama
|
||||
|
||||
from esphome.loader import get_platform
|
||||
|
||||
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", "..")))
|
||||
basepath = os.path.join(root_path, "esphome")
|
||||
temp_folder = os.path.join(root_path, ".temp")
|
||||
@@ -624,11 +626,15 @@ def get_usable_cpu_count() -> int:
|
||||
)
|
||||
|
||||
|
||||
def get_all_dependencies(component_names: set[str]) -> set[str]:
|
||||
def get_all_dependencies(
|
||||
component_names: set[str], cpp_testing: bool = False
|
||||
) -> set[str]:
|
||||
"""Get all dependencies for a set of components.
|
||||
|
||||
Args:
|
||||
component_names: Set of component names to get dependencies for
|
||||
cpp_testing: If True, set CORE.cpp_testing so AUTO_LOAD callables that
|
||||
conditionally include testing-only dependencies work correctly
|
||||
|
||||
Returns:
|
||||
Set of all components including dependencies and auto-loaded components
|
||||
@@ -646,6 +652,7 @@ def get_all_dependencies(component_names: set[str]) -> set[str]:
|
||||
|
||||
# Reset CORE to ensure clean state
|
||||
CORE.reset()
|
||||
CORE.cpp_testing = cpp_testing
|
||||
|
||||
# Set up fake config path for component loading
|
||||
root = Path(__file__).parent.parent
|
||||
@@ -660,7 +667,11 @@ def get_all_dependencies(component_names: set[str]) -> set[str]:
|
||||
new_components: set[str] = set()
|
||||
|
||||
for comp_name in all_components:
|
||||
comp = get_component(comp_name)
|
||||
if "." in comp_name:
|
||||
domain, platform = comp_name.split(".", maxsplit=1)
|
||||
comp = get_platform(domain, platform)
|
||||
else:
|
||||
comp = get_component(comp_name)
|
||||
if not comp:
|
||||
continue
|
||||
|
||||
@@ -705,8 +716,10 @@ def get_components_from_integration_fixtures() -> set[str]:
|
||||
if not config:
|
||||
continue
|
||||
|
||||
# Add all top-level component keys
|
||||
components.update(config.keys())
|
||||
# Add all top-level component keys (skip YAML anchor keys starting with '.')
|
||||
components.update(
|
||||
k for k in config if isinstance(k, str) and not k.startswith(".")
|
||||
)
|
||||
|
||||
# Add platform components (e.g., output.template)
|
||||
for value in config.values():
|
||||
|
||||
@@ -7,10 +7,23 @@
|
||||
testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing`
|
||||
(where "component" is the component under test), for example: `esphome::uart::testing`.
|
||||
|
||||
### Platform components
|
||||
|
||||
For components that expose to a platform component, create a folder under your component test folder with the platform component name, e.g. `binary_sensor` and
|
||||
include the relevant `.cpp` and `.h` test files there.
|
||||
|
||||
### Override component code generation for testing
|
||||
|
||||
When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not
|
||||
need to generate configuration code for testing.
|
||||
|
||||
If you do need to generate code to for example configure compilation flags or add libraries,
|
||||
add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`.
|
||||
|
||||
## Running component unit tests
|
||||
|
||||
(from the repository root)
|
||||
|
||||
```bash
|
||||
./script/cpp_unit_test.py component1 component2 ...
|
||||
```
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, AddBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_binary_sensor("motion", &bs);
|
||||
ASSERT_EQ(transport.binary_sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.binary_sensors_[0].id, "motion");
|
||||
EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, AddRemoteBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_remote_binary_sensor("host1", "remote_motion", &bs);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
binary_sensor::BinarySensor local_bs;
|
||||
local_bs.state = true;
|
||||
encoder.add_binary_sensor("motion", &local_bs);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
binary_sensor::BinarySensor remote_bs;
|
||||
decoder.add_remote_binary_sensor("sender", "motion", &remote_bs);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_TRUE(remote_bs.state);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 10.0f;
|
||||
s2.state = 20.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
binary_sensor::BinarySensor bs1;
|
||||
bs1.state = true;
|
||||
encoder.add_binary_sensor("bs1", &bs1);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
binary_sensor::BinarySensor rbs1;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
decoder.add_remote_binary_sensor("sender", "bs1", &rbs1);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, 10.0f);
|
||||
EXPECT_FLOAT_EQ(rs2.state, 20.0f);
|
||||
EXPECT_TRUE(rbs1.state);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -1,11 +0,0 @@
|
||||
# Extra component configuration required by C++ unit tests.
|
||||
# Loaded by cpp_unit_test.py and merged into the test build config
|
||||
# before validation, so that platform defines (USE_SENSOR, etc.) are generated.
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: test_cpp_sensor
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: test_cpp_binary_sensor
|
||||
@@ -65,198 +65,6 @@ TEST(PacketTransportTest, SetProviderEncryption) {
|
||||
EXPECT_EQ(transport.providers_["host1"].encryption_key, key);
|
||||
}
|
||||
|
||||
// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) ---
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, AddSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_sensor("temp", &s);
|
||||
ASSERT_EQ(transport.sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.sensors_[0].id, "temp");
|
||||
EXPECT_EQ(transport.sensors_[0].sensor, &s);
|
||||
EXPECT_TRUE(transport.sensors_[0].updated);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, AddRemoteSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_remote_sensor("host1", "remote_temp", &s);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
TEST(PacketTransportTest, AddBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_binary_sensor("motion", &bs);
|
||||
ASSERT_EQ(transport.binary_sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.binary_sensors_[0].id, "motion");
|
||||
EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, AddRemoteBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_remote_binary_sensor("host1", "remote_motion", &bs);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs);
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) ---
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, UnencryptedSensorRoundTrip) {
|
||||
// Encoder
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 42.5f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
// Decoder
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f; // sentinel
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
binary_sensor::BinarySensor local_bs;
|
||||
local_bs.state = true;
|
||||
encoder.add_binary_sensor("motion", &local_bs);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
binary_sensor::BinarySensor remote_bs;
|
||||
decoder.add_remote_binary_sensor("sender", "motion", &remote_bs);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_TRUE(remote_bs.state);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR)
|
||||
TEST(PacketTransportTest, MultipleSensorsRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 10.0f;
|
||||
s2.state = 20.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
binary_sensor::BinarySensor bs1;
|
||||
bs1.state = true;
|
||||
encoder.add_binary_sensor("bs1", &bs1);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
binary_sensor::BinarySensor rbs1;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
decoder.add_remote_binary_sensor("sender", "bs1", &rbs1);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, 10.0f);
|
||||
EXPECT_FLOAT_EQ(rs2.state, 20.0f);
|
||||
EXPECT_TRUE(rbs1.state);
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Encrypted round-trip ---
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, EncryptedSensorRoundTrip) {
|
||||
std::vector<uint8_t> key(32);
|
||||
for (int i = 0; i < 32; i++)
|
||||
key[i] = i;
|
||||
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
encoder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 99.9f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
decoder.set_provider_encryption("sender", key);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f);
|
||||
}
|
||||
|
||||
// --- Selective send ---
|
||||
|
||||
TEST(PacketTransportTest, SendDataOnlyUpdated) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 1.0f;
|
||||
s2.state = 2.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
// Mark s1 as not updated, only s2 as updated
|
||||
encoder.sensors_[0].updated = false;
|
||||
encoder.sensors_[1].updated = true;
|
||||
|
||||
encoder.send_data_(false);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent
|
||||
EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Ping key tests ---
|
||||
|
||||
TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) {
|
||||
@@ -319,73 +127,6 @@ TEST(PacketTransportTest, PingKeyMaxLimit) {
|
||||
EXPECT_FALSE(transport.ping_keys_.contains("host4"));
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder: encrypted, owns a sensor
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
|
||||
// Requester sends a MAGIC_PING that the responder processes
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
responder.process_({ping.data(), ping.size()});
|
||||
ASSERT_EQ(responder.ping_keys_.size(), 1u);
|
||||
|
||||
// Responder sends sensor data — ping key should be embedded
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
// The requester decrypts the packet and finds its ping key echoed back,
|
||||
// which gates the sensor data — if the key is missing, data is blocked.
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester with ping-pong enabled expects a key that isn't in the packet
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Process error handling ---
|
||||
|
||||
TEST(PacketTransportTest, ProcessShortBuffer) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
TEST(PacketTransportSensorTest, AddSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_sensor("temp", &s);
|
||||
ASSERT_EQ(transport.sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.sensors_[0].id, "temp");
|
||||
EXPECT_EQ(transport.sensors_[0].sensor, &s);
|
||||
EXPECT_TRUE(transport.sensors_[0].updated);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, AddRemoteSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_remote_sensor("host1", "remote_temp", &s);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) {
|
||||
// Encoder
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 42.5f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
// Decoder
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f; // sentinel
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) {
|
||||
std::vector<uint8_t> key(32);
|
||||
for (int i = 0; i < 32; i++)
|
||||
key[i] = i;
|
||||
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
encoder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 99.9f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
decoder.set_provider_encryption("sender", key);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, SendDataOnlyUpdated) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 1.0f;
|
||||
s2.state = 2.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
// Mark s1 as not updated, only s2 as updated
|
||||
encoder.sensors_[0].updated = false;
|
||||
encoder.sensors_[1].updated = true;
|
||||
|
||||
encoder.send_data_(false);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent
|
||||
EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder: encrypted, owns a sensor
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
|
||||
// Requester sends a MAGIC_PING that the responder processes
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
responder.process_({ping.data(), ping.size()});
|
||||
ASSERT_EQ(responder.ping_keys_.size(), 1u);
|
||||
|
||||
// Responder sends sensor data — ping key should be embedded
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
// The requester decrypts the packet and finds its ping key echoed back,
|
||||
// which gates the sensor data — if the key is missing, data is blocked.
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester with ping-pong enabled expects a key that isn't in the packet
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -102,6 +102,18 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Common filter definitions
|
||||
.sensor_filters: &sensor_filters
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
.binary_filters: &binary_filters
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
@@ -111,107 +123,56 @@ sensor:
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
light:
|
||||
name: "Light"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
gate_0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
gate_1:
|
||||
move_energy:
|
||||
name: "Gate 1 Move Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 1 Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
gate_2:
|
||||
move_energy:
|
||||
name: "Gate 2 Move Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 2 Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
<<: *binary_filters
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
<<: *binary_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
<<: *binary_filters
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2412-eng-trunc
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid engineering mode frame (52 bytes, buffer_pos_=52)
|
||||
# Establishes baseline: gate_0_move=100, light=87
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10,
|
||||
0x57,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=200ms): Truncated engineering mode frame (24 bytes, buffer_pos_=24)
|
||||
# This frame has data_type=0x01 (engineering) but only enough data for the
|
||||
# basic target fields, not the gate energies or light sensor.
|
||||
# buffer_pos_=24 passes the old check (>= 12) but fails the new check (< 46).
|
||||
# Without the fix, indices 17-45 would read stale buffer data from Phase 1.
|
||||
#
|
||||
# Layout (24 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 0E 00 = length 14
|
||||
# [6] 01 = data type (engineering mode)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 1E 00 = moving distance 30
|
||||
# [11] 50 = moving energy 80
|
||||
# [12-13] 1E 00 = still distance 30
|
||||
# [14] 50 = still energy 80
|
||||
# [15-16] FF FF = garbage detection distance bytes
|
||||
# [17] FF = padding (would be gate data in full frame)
|
||||
# [18] 55 = data footer marker (at buffer_pos_ - 6)
|
||||
# [19] 00 = check byte
|
||||
# [20-23] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0E, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x50,
|
||||
0x1E, 0x00,
|
||||
0x50,
|
||||
0xFF, 0xFF,
|
||||
0xFF,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 3 (t=300ms): Valid recovery frame with different values
|
||||
# gate_0_move=50, light=42 — proves component recovered
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x32, 0x20, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x28, 0x20, 0x18, 0x10, 0x08,
|
||||
0x2A,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Common filter definitions
|
||||
.sensor_filters: &sensor_filters
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
.binary_filters: &binary_filters
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
<<: *sensor_filters
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
<<: *sensor_filters
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
<<: *sensor_filters
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
<<: *sensor_filters
|
||||
light:
|
||||
name: "Light"
|
||||
<<: *sensor_filters
|
||||
gate_0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
<<: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
<<: *binary_filters
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
<<: *binary_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
<<: *binary_filters
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -14,6 +14,12 @@ test_uart_mock_ld2412_engineering (engineering mode):
|
||||
2. Multi-byte still distance (291cm) using high byte > 0
|
||||
3. Gate energy sensor values
|
||||
4. Detection distance computed from target state
|
||||
|
||||
test_uart_mock_ld2412_engineering_truncated (truncated engineering mode):
|
||||
1. Valid engineering frame establishes baseline sensor values
|
||||
2. Truncated engineering frame (24 bytes) is rejected — gate/light sensors
|
||||
must not receive garbage from stale buffer data or frame footer bytes
|
||||
3. Recovery frame with different values proves the component survived
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -273,3 +279,122 @@ async def test_uart_mock_ld2412_engineering(
|
||||
)
|
||||
|
||||
assert pytest.approx(291.0) in collector.sensor_states["detection_distance"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2412_engineering_truncated(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that truncated engineering mode frames don't corrupt sensor values.
|
||||
|
||||
Without the fix, a 24-byte engineering mode frame passes the old buffer_pos_ >= 12
|
||||
check but reads indices 17-45 from stale buffer data, publishing garbage values
|
||||
(e.g. frame footer bytes 0xF8=248 as gate energy).
|
||||
"""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track the truncated frame warning
|
||||
truncated_warning_seen = loop.create_future()
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if (
|
||||
"Engineering mode packet too short" in line
|
||||
and not truncated_warning_seen.done()
|
||||
):
|
||||
truncated_warning_seen.set_result(True)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=[
|
||||
"moving_distance",
|
||||
"still_distance",
|
||||
"moving_energy",
|
||||
"still_energy",
|
||||
"detection_distance",
|
||||
"light",
|
||||
"gate_0_move_energy",
|
||||
"gate_0_still_energy",
|
||||
],
|
||||
binary_sensor_names=[
|
||||
"has_target",
|
||||
"has_moving_target",
|
||||
"has_still_target",
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see Phase 3 recovery values (gate_0_move=50)
|
||||
recovery_received = collector.add_waiter(
|
||||
lambda: pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"]
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
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)
|
||||
)
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
# Wait for Phase 1 — valid engineering frame establishes baseline
|
||||
try:
|
||||
await collector.wait_for_all(timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Phase 1 frame. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
)
|
||||
|
||||
# Phase 1 baseline: gate_0_move=100, light=87
|
||||
assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0)
|
||||
assert collector.sensor_states["light"][0] == pytest.approx(87.0)
|
||||
|
||||
# Wait for Phase 3 recovery frame (gate_0_move=50)
|
||||
try:
|
||||
await asyncio.wait_for(recovery_received, timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for recovery frame. Received:\n"
|
||||
f" gate_0_move_energy: {collector.sensor_states['gate_0_move_energy']}\n"
|
||||
f" light: {collector.sensor_states['light']}"
|
||||
)
|
||||
|
||||
# Verify the truncated frame warning was logged
|
||||
assert truncated_warning_seen.done(), (
|
||||
"Expected 'Engineering mode packet too short' warning in logs"
|
||||
)
|
||||
|
||||
# Phase 3 recovery: gate_0_move=50, light=42
|
||||
assert pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"]
|
||||
assert pytest.approx(42.0) in collector.sensor_states["light"]
|
||||
|
||||
# The critical assertion: gate_0_move_energy must never have received
|
||||
# garbage values from the truncated frame. Without the fix,
|
||||
# buffer_data_[17] = 0xFF = 255 would be published as gate_0_move.
|
||||
for value in collector.sensor_states["gate_0_move_energy"]:
|
||||
assert value == pytest.approx(100.0) or value == pytest.approx(50.0), (
|
||||
f"gate_0_move_energy got unexpected value {value} — "
|
||||
f"truncated frame likely leaked stale buffer data. "
|
||||
f"All values: {collector.sensor_states['gate_0_move_energy']}"
|
||||
)
|
||||
|
||||
@@ -1027,6 +1027,73 @@ def test_get_all_dependencies_empty_set() -> None:
|
||||
assert result == set()
|
||||
|
||||
|
||||
def test_get_all_dependencies_platform_component() -> None:
|
||||
"""Platform components (domain.component) are looked up via get_platform,
|
||||
not get_component."""
|
||||
platform_comp = Mock()
|
||||
platform_comp.dependencies = []
|
||||
platform_comp.auto_load = []
|
||||
|
||||
with (
|
||||
patch("esphome.loader.get_component") as mock_get_component,
|
||||
patch("helpers.get_platform") as mock_get_platform,
|
||||
):
|
||||
mock_get_platform.return_value = platform_comp
|
||||
mock_get_component.return_value = None
|
||||
|
||||
result = helpers.get_all_dependencies({"sensor.bthome"})
|
||||
|
||||
mock_get_platform.assert_called_once_with("sensor", "bthome")
|
||||
mock_get_component.assert_not_called()
|
||||
assert result == {"sensor.bthome"}
|
||||
|
||||
|
||||
def test_get_all_dependencies_platform_component_with_dependencies() -> None:
|
||||
"""Dependencies of a platform component are resolved transitively."""
|
||||
platform_comp = Mock()
|
||||
platform_comp.dependencies = ["sensor"]
|
||||
platform_comp.auto_load = []
|
||||
|
||||
sensor_comp = Mock()
|
||||
sensor_comp.dependencies = []
|
||||
sensor_comp.auto_load = []
|
||||
|
||||
with (
|
||||
patch("esphome.loader.get_component") as mock_get_component,
|
||||
patch("helpers.get_platform") as mock_get_platform,
|
||||
):
|
||||
mock_get_platform.return_value = platform_comp
|
||||
mock_get_component.side_effect = lambda name: (
|
||||
sensor_comp if name == "sensor" else None
|
||||
)
|
||||
|
||||
result = helpers.get_all_dependencies({"sensor.bthome"})
|
||||
|
||||
assert result == {"sensor.bthome", "sensor"}
|
||||
|
||||
|
||||
def test_get_all_dependencies_cpp_testing_flag() -> None:
|
||||
"""cpp_testing=True propagates to CORE.cpp_testing during resolution."""
|
||||
from esphome.core import CORE
|
||||
|
||||
with (
|
||||
patch("esphome.loader.get_component") as mock_get_component,
|
||||
patch("esphome.loader.get_platform"),
|
||||
):
|
||||
observed: list[bool] = []
|
||||
|
||||
def capturing_get_component(name: str):
|
||||
observed.append(CORE.cpp_testing)
|
||||
|
||||
mock_get_component.side_effect = capturing_get_component
|
||||
|
||||
helpers.get_all_dependencies({"some_comp"}, cpp_testing=True)
|
||||
|
||||
assert observed and all(observed), (
|
||||
"CORE.cpp_testing should be True during resolution"
|
||||
)
|
||||
|
||||
|
||||
def test_get_components_from_integration_fixtures() -> None:
|
||||
"""Test extraction of components from fixture YAML files."""
|
||||
yaml_content = {
|
||||
@@ -1057,6 +1124,30 @@ def test_get_components_from_integration_fixtures() -> None:
|
||||
assert components == expected_components
|
||||
|
||||
|
||||
def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None:
|
||||
"""Test that YAML anchor keys (starting with '.') are excluded."""
|
||||
yaml_content = {
|
||||
"sensor": [{"platform": "template", "name": "test"}],
|
||||
"esphome": {"name": "test"},
|
||||
".sensor_filters": {"filters": [{"timeout": "50ms"}]},
|
||||
".binary_filters": {"filters": [{"settle": "50ms"}]},
|
||||
}
|
||||
|
||||
mock_yaml_file = Mock()
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.glob") as mock_glob,
|
||||
patch("esphome.yaml_util.load_yaml", return_value=yaml_content),
|
||||
):
|
||||
mock_glob.return_value = [mock_yaml_file]
|
||||
|
||||
components = helpers.get_components_from_integration_fixtures()
|
||||
|
||||
assert ".sensor_filters" not in components
|
||||
assert ".binary_filters" not in components
|
||||
assert components == {"sensor", "esphome", "template"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"output,expected",
|
||||
[
|
||||
|
||||
@@ -841,6 +841,18 @@ class TestEsphomeCore:
|
||||
|
||||
assert "WiFi" in target.platformio_libraries
|
||||
|
||||
def test_testing_ensure_platform_registered__sets_count(self, target):
|
||||
"""Test testing_ensure_platform_registered sets count to 1 for new platform."""
|
||||
assert target.platform_counts["sensor"] == 0
|
||||
target.testing_ensure_platform_registered("sensor")
|
||||
assert target.platform_counts["sensor"] == 1
|
||||
|
||||
def test_testing_ensure_platform_registered__does_not_overwrite(self, target):
|
||||
"""Test testing_ensure_platform_registered preserves existing count."""
|
||||
target.platform_counts["sensor"] = 3
|
||||
target.testing_ensure_platform_registered("sensor")
|
||||
assert target.platform_counts["sensor"] == 3
|
||||
|
||||
def test_add_library__extracts_short_name_from_path(self, target):
|
||||
"""Test add_library extracts short name from library paths like owner/lib."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
|
||||
@@ -76,6 +76,7 @@ from esphome.const import (
|
||||
PLATFORM_RP2040,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.util import BootselResult
|
||||
|
||||
|
||||
def strip_ansi_codes(text: str) -> str:
|
||||
@@ -875,7 +876,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel(
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=1),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)),
|
||||
):
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
@@ -898,7 +899,7 @@ def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None:
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
|
||||
pytest.raises(EsphomeError, match="BOOTSEL"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
@@ -923,7 +924,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota(
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
|
||||
patch(
|
||||
"esphome.__main__.choose_prompt",
|
||||
return_value="192.168.1.100",
|
||||
@@ -952,7 +953,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
|
||||
"esphome.__main__._find_picotool",
|
||||
return_value=Path("/usr/bin/picotool"),
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
|
||||
caplog.at_level(logging.INFO, logger="esphome.__main__"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
@@ -963,6 +964,69 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
|
||||
assert "BOOTSEL" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_rp2040_permission_error_no_options(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test permission warning shown when BOOTSEL device found but not accessible."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch(
|
||||
"esphome.__main__.detect_rp2040_bootsel",
|
||||
return_value=BootselResult(0, permission_error=True),
|
||||
),
|
||||
patch("esphome.__main__.sys.platform", "linux"),
|
||||
pytest.raises(EsphomeError, match="BOOTSEL"),
|
||||
caplog.at_level(logging.WARNING, logger="esphome.__main__"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
purpose=Purpose.UPLOADING,
|
||||
)
|
||||
|
||||
assert "USB permissions" in caplog.text
|
||||
assert "udev" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_rp2040_permission_error_with_ota(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test permission warning shown with OTA fallback available."""
|
||||
setup_core(
|
||||
platform=PLATFORM_RP2040,
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch(
|
||||
"esphome.__main__.detect_rp2040_bootsel",
|
||||
return_value=BootselResult(0, permission_error=True),
|
||||
),
|
||||
patch(
|
||||
"esphome.__main__.choose_prompt",
|
||||
return_value="192.168.1.100",
|
||||
),
|
||||
caplog.at_level(logging.WARNING, logger="esphome.__main__"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
purpose=Purpose.UPLOADING,
|
||||
)
|
||||
|
||||
assert "USB permissions" in caplog.text
|
||||
|
||||
|
||||
def test_choose_upload_log_host_no_bootsel_for_non_rp2040(
|
||||
mock_no_serial_ports: Mock,
|
||||
) -> None:
|
||||
@@ -1000,7 +1064,7 @@ def test_choose_upload_log_host_rp2040_serial_and_bootsel(
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=1),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
default=None,
|
||||
|
||||
@@ -463,8 +463,9 @@ def test_detect_rp2040_bootsel_found() -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = b"Device Information\n type: RP2040\n"
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 1
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 1
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_multiple() -> None:
|
||||
@@ -472,8 +473,9 @@ def test_detect_rp2040_bootsel_multiple() -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = b"type: RP2040\ntype: RP2350\n"
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 2
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 2
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_none() -> None:
|
||||
@@ -482,16 +484,47 @@ def test_detect_rp2040_bootsel_none() -> None:
|
||||
mock_result.stdout = (
|
||||
b"No accessible RP2040/RP2350 devices in BOOTSEL mode were found.\n"
|
||||
)
|
||||
mock_result.stderr = b""
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 0
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_permission_error() -> None:
|
||||
"""Test BOOTSEL detection with device found but not accessible."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = (
|
||||
b"No accessible RP-series devices in BOOTSEL mode were found.\n"
|
||||
)
|
||||
mock_result.stderr = (
|
||||
b"RP2040 device at bus 5, address 24 appears to be in BOOTSEL mode, "
|
||||
b"but picotool was unable to connect. "
|
||||
b"Maybe try 'sudo' or check your permissions.\n"
|
||||
)
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is True
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_libusb_access_error() -> None:
|
||||
"""Test BOOTSEL detection with LIBUSB_ERROR_ACCESS."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = b""
|
||||
mock_result.stderr = b"LIBUSB_ERROR_ACCESS\n"
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is True
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_oserror() -> None:
|
||||
"""Test BOOTSEL detection handles OSError."""
|
||||
with patch("esphome.util.subprocess.run", side_effect=OSError("not found")):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 0
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_timeout() -> None:
|
||||
@@ -500,8 +533,9 @@ def test_detect_rp2040_bootsel_timeout() -> None:
|
||||
"esphome.util.subprocess.run",
|
||||
side_effect=subprocess.TimeoutExpired("picotool", 10),
|
||||
):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 0
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def _make_redirect(
|
||||
|
||||
Reference in New Issue
Block a user