Compare commits

..

4 Commits

Author SHA1 Message Date
J. Nick Koston
97e1a58787 [weikai] Replace bitset to_string with format_bin_to 2026-01-16 13:05:27 -10:00
J. Nick Koston
52ac9e1861 [remote_base] Replace unsafe sprintf with buf_append_printf; fix buffer overflow (#13257)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 16:56:47 -06:00
Clyde Stubbs
c5e4a60884 [select] Add condition for testing select option (#13267)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-01-17 08:35:40 +11:00
dependabot[bot]
a680884138 Bump ruff from 0.14.12 to 0.14.13 (#13275)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-16 20:29:02 +00:00
11 changed files with 150 additions and 70 deletions

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.11
rev: v0.14.13
hooks:
# Run the linter.
- id: ruff

View File

@@ -85,8 +85,8 @@ optional<AEHAData> AEHAProtocol::decode(RemoteReceiveData src) {
std::string AEHAProtocol::format_data_(const std::vector<uint8_t> &data) {
std::string out;
for (uint8_t byte : data) {
char buf[6];
sprintf(buf, "0x%02X,", byte);
char buf[8]; // "0x%02X," = 5 chars + null + margin
snprintf(buf, sizeof(buf), "0x%02X,", byte);
out += buf;
}
out.pop_back();

View File

@@ -1,4 +1,5 @@
#include "raw_protocol.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -8,36 +9,30 @@ static const char *const TAG = "remote.raw";
bool RawDumper::dump(RemoteReceiveData src) {
char buffer[256];
uint32_t buffer_offset = 0;
buffer_offset += sprintf(buffer, "Received Raw: ");
size_t pos = buf_append_printf(buffer, sizeof(buffer), 0, "Received Raw: ");
for (int32_t i = 0; i < src.size() - 1; i++) {
const int32_t value = src[i];
const uint32_t remaining_length = sizeof(buffer) - buffer_offset;
int written;
size_t prev_pos = pos;
if (i + 1 < src.size() - 1) {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value);
} else {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value);
}
if (written < 0 || written >= int(remaining_length)) {
// write failed, flush...
buffer[buffer_offset] = '\0';
if (pos >= sizeof(buffer) - 1) {
// buffer full, flush and continue
buffer[prev_pos] = '\0';
ESP_LOGI(TAG, "%s", buffer);
buffer_offset = 0;
written = sprintf(buffer, " ");
if (i + 1 < src.size() - 1) {
written += sprintf(buffer + written, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value);
} else {
written += sprintf(buffer + written, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value);
}
}
buffer_offset += written;
}
if (buffer_offset != 0) {
if (pos != 0) {
ESP_LOGI(TAG, "%s", buffer);
}
return true;

View File

@@ -1,4 +1,5 @@
#include "remote_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -169,36 +170,31 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
const auto &vec = this->temp_.get_data();
char buffer[256];
uint32_t buffer_offset = 0;
buffer_offset += sprintf(buffer, "Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait);
size_t pos = buf_append_printf(buffer, sizeof(buffer), 0,
"Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait);
for (size_t i = 0; i < vec.size(); i++) {
const int32_t value = vec[i];
const uint32_t remaining_length = sizeof(buffer) - buffer_offset;
int written;
size_t prev_pos = pos;
if (i + 1 < vec.size()) {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value);
} else {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value);
}
if (written < 0 || written >= int(remaining_length)) {
// write failed, flush...
buffer[buffer_offset] = '\0';
if (pos >= sizeof(buffer) - 1) {
// buffer full, flush and continue
buffer[prev_pos] = '\0';
ESP_LOGVV(TAG, "%s", buffer);
buffer_offset = 0;
written = sprintf(buffer, " ");
if (i + 1 < vec.size()) {
written += sprintf(buffer + written, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value);
} else {
written += sprintf(buffer + written, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value);
}
}
buffer_offset += written;
}
if (buffer_offset != 0) {
if (pos != 0) {
ESP_LOGVV(TAG, "%s", buffer);
}
#endif

View File

@@ -8,17 +8,20 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INDEX,
CONF_LAMBDA,
CONF_MODE,
CONF_MQTT_ID,
CONF_ON_VALUE,
CONF_OPERATION,
CONF_OPTION,
CONF_OPTIONS,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObjClass, TemplateArguments
from esphome.cpp_types import global_ns
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -38,6 +41,9 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
# Conditions
SelectIsCondition = select_ns.class_("SelectIsCondition", automation.Condition)
# Enums
SelectOperation = select_ns.enum("SelectOperation")
SELECT_OPERATION_OPTIONS = {
@@ -165,6 +171,41 @@ async def select_set_index_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"select.is",
SelectIsCondition,
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_OPTIONS): cv.All(
cv.ensure_list(cv.string_strict), cv.Length(min=1)
),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
}
).add_extra(cv.has_exactly_one_key(CONF_OPTIONS, CONF_LAMBDA)),
)
async def select_is_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
if options := config.get(CONF_OPTIONS):
# List of constant options
# Create a constexpr and pass that with a template length
arr_id = ID(
f"{condition_id}_data",
is_declaration=True,
type=global_ns.namespace("constexpr char * const"),
)
arg = cg.static_const_array(arr_id, cg.ArrayInitializer(*options))
template_arg = TemplateArguments(len(options), *template_arg)
else:
# Lambda
arg = await cg.process_lambda(
config[CONF_LAMBDA],
[(global_ns.namespace("StringRef &").operator("const"), "current")] + args,
return_type=cg.bool_,
)
template_arg = TemplateArguments(0, *template_arg)
return cg.new_Pvariable(condition_id, template_arg, paren, arg)
@automation.register_action(
"select.operation",
SelectOperationAction,

View File

@@ -66,4 +66,34 @@ template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
Select *select_;
};
template<size_t N, typename... Ts> class SelectIsCondition : public Condition<Ts...> {
public:
SelectIsCondition(Select *parent, const char *const *option_list) : parent_(parent), option_list_(option_list) {}
bool check(const Ts &...x) override {
auto current = this->parent_->current_option();
for (size_t i = 0; i != N; i++) {
if (current == this->option_list_[i]) {
return true;
}
}
return false;
}
protected:
Select *parent_;
const char *const *option_list_;
};
template<typename... Ts> class SelectIsCondition<0, Ts...> : public Condition<Ts...> {
public:
SelectIsCondition(Select *parent, std::function<bool(const StringRef &, const Ts &...)> &&f)
: parent_(parent), f_(f) {}
bool check(const Ts &...x) override { return this->f_(this->parent_->current_option(), x...); }
protected:
Select *parent_;
std::function<bool(const StringRef &, const Ts &...)> f_;
};
} // namespace esphome::select

View File

@@ -47,27 +47,29 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
size_t remaining = sizeof(packet);
// Write PRI - abort if this fails as packet would be malformed
offset = buf_append_printf(packet, sizeof(packet), 0, "<%d>", pri);
if (offset == 0) {
return; // PRI always produces at least "<0>" (3 chars), so 0 means error
int ret = snprintf(packet, remaining, "<%d>", pri);
if (ret <= 0 || static_cast<size_t>(ret) >= remaining) {
return;
}
remaining -= offset;
offset = ret;
remaining -= ret;
// Write timestamp directly into packet (RFC 5424: use "-" if time not valid or strftime fails)
auto now = this->time_->now();
size_t ts_written = now.is_valid() ? now.strftime(packet + offset, remaining, "%b %e %H:%M:%S") : 0;
if (ts_written > 0) {
offset += ts_written;
remaining -= ts_written;
} else if (remaining > 0) {
packet[offset++] = '-';
remaining--;
}
// Write hostname, tag, and message
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len,
message);
// Clamp to exclude null terminator position if buffer was filled
if (offset >= sizeof(packet)) {
offset = sizeof(packet) - 1;
ret = snprintf(packet + offset, remaining, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len, message);
if (ret > 0) {
// snprintf returns chars that would be written; clamp to actual buffer space
offset += std::min(static_cast<size_t>(ret), remaining > 0 ? remaining - 1 : 0);
}
if (offset > 0) {

View File

@@ -4,19 +4,13 @@
/// @details The classes declared in this file can be used by the Weikai family
#include "weikai.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace weikai {
static const char *const TAG = "weikai";
/// @brief convert an int to binary representation as C++ std::string
/// @param val integer to convert
/// @return a std::string
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
/// Convert std::string to C string
#define I2S2CS(val) (i2s(val).c_str())
/// @brief measure the time elapsed between two calls
/// @param last_time time of the previous call
/// @return the elapsed time in milliseconds
@@ -170,17 +164,18 @@ void WeikaiComponent::test_gpio_input_() {
static bool init_input{false};
static uint8_t state{0};
uint8_t value;
char bin_buf[9]; // 8 binary digits + null
if (!init_input) {
init_input = true;
// set all pins in input mode
this->reg(WKREG_GPDIR, 0) = 0x00;
ESP_LOGI(TAG, "initializing all pins to input mode");
state = this->reg(WKREG_GPDAT, 0);
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, I2S2CS(state));
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, format_bin_to(bin_buf, state));
}
value = this->reg(WKREG_GPDAT, 0);
if (value != state) {
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, I2S2CS(value));
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, format_bin_to(bin_buf, value));
state = value;
}
}
@@ -188,6 +183,7 @@ void WeikaiComponent::test_gpio_input_() {
void WeikaiComponent::test_gpio_output_() {
static bool init_output{false};
static uint8_t state{0};
char bin_buf[9]; // 8 binary digits + null
if (!init_output) {
init_output = true;
// set all pins in output mode
@@ -198,7 +194,7 @@ void WeikaiComponent::test_gpio_output_() {
}
state = ~state;
this->reg(WKREG_GPDAT, 0) = state;
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, I2S2CS(state));
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, format_bin_to(bin_buf, state));
delay(100); // NOLINT
}
#endif
@@ -208,7 +204,9 @@ void WeikaiComponent::test_gpio_output_() {
///////////////////////////////////////////////////////////////////////////////
bool WeikaiComponent::read_pin_val_(uint8_t pin) {
this->input_state_ = this->reg(WKREG_GPDAT, 0);
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin), I2S2CS(input_state_));
char bin_buf[9];
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin),
format_bin_to(bin_buf, this->input_state_));
return this->input_state_ & (1 << pin);
}
@@ -218,7 +216,9 @@ void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
} else {
this->output_state_ &= ~(1 << pin);
}
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value), I2S2CS(this->output_state_));
char bin_buf[9];
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
format_bin_to(bin_buf, this->output_state_));
this->reg(WKREG_GPDAT, 0) = this->output_state_;
}
@@ -232,7 +232,8 @@ void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) {
ESP_LOGE(TAG, "pin %d direction invalid", pin);
}
}
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, I2S2CS(this->pin_config_));
char bin_buf[9];
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, format_bin_to(bin_buf, this->pin_config_));
this->reg(WKREG_GPDIR, 0) = this->pin_config_; // TODO check ~
}
@@ -241,7 +242,6 @@ void WeikaiGPIOPin::setup() {
flags_ == gpio::FLAG_INPUT ? "Input"
: this->flags_ == gpio::FLAG_OUTPUT ? "Output"
: "NOT SPECIFIED");
// ESP_LOGCONFIG(TAG, "Setting GPIO pins mode to '%s' %02X", I2S2CS(this->flags_), this->flags_);
this->pin_mode(this->flags_);
}
@@ -297,8 +297,9 @@ void WeikaiChannel::set_line_param_() {
break; // no parity 000x
}
this->reg(WKREG_LCR) = lcr; // write LCR
char bin_buf[9];
ESP_LOGV(TAG, " line config: %d data_bits, %d stop_bits, parity %s register [%s]", this->data_bits_,
this->stop_bits_, p2s(this->parity_), I2S2CS(lcr));
this->stop_bits_, p2s(this->parity_), format_bin_to(bin_buf, lcr));
}
void WeikaiChannel::set_baudrate_() {
@@ -334,7 +335,8 @@ size_t WeikaiChannel::tx_in_fifo_() {
if (tfcnt == 0) {
uint8_t const fsr = this->reg(WKREG_FSR);
if (fsr & FSR_TFFULL) {
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", I2S2CS(fsr));
char bin_buf[9];
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", format_bin_to(bin_buf, fsr));
tfcnt = FIFO_SIZE;
}
}
@@ -346,14 +348,15 @@ size_t WeikaiChannel::rx_in_fifo_() {
size_t available = this->reg(WKREG_RFCNT);
uint8_t const fsr = this->reg(WKREG_FSR);
if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) {
char bin_buf[9];
if (fsr & FSR_RFOE)
ESP_LOGE(TAG, "Receive data overflow FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr));
if (fsr & FSR_RFLB)
ESP_LOGE(TAG, "Receive line break FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr));
if (fsr & FSR_RFFE)
ESP_LOGE(TAG, "Receive frame error FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr));
if (fsr & FSR_RFPE)
ESP_LOGE(TAG, "Receive parity error FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr));
}
if ((available == 0) && (fsr & FSR_RFDAT)) {
// here we should be very careful because we can have something like this:
@@ -362,11 +365,13 @@ size_t WeikaiChannel::rx_in_fifo_() {
// - so to be sure we need to do another read of RFCNT and if it is still zero -> buffer full
available = this->reg(WKREG_RFCNT);
if (available == 0) { // still zero ?
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", I2S2CS(fsr));
char bin_buf[9];
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", format_bin_to(bin_buf, fsr));
available = FIFO_SIZE;
}
}
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, I2S2CS(fsr));
char bin_buf2[9];
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, format_bin_to(bin_buf2, fsr));
return available;
}

View File

@@ -8,7 +8,6 @@
/// wk2132_i2c, wk2168_i2c, wk2204_i2c, wk2212_i2c
#pragma once
#include <bitset>
#include <memory>
#include <cinttypes>
#include "esphome/core/component.h"

View File

@@ -1,6 +1,6 @@
pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.12 # also change in .pre-commit-config.yaml when updating
ruff==0.14.13 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -53,6 +53,17 @@ binary_sensor:
// Garage Door is closed.
return false;
}
- platform: template
id: select_binary_sensor
name: Select is one or two
condition:
any:
- select.is:
id: template_select
options: [one, two]
- select.is:
id: template_select
lambda: return current == id(template_text).state;
- platform: template
id: other_binary_sensor
name: "Garage Door Closed"
@@ -320,6 +331,7 @@ valve:
text:
- platform: template
id: template_text
name: "Template text"
optimistic: true
min_length: 0