mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 01:26:24 -07:00
Compare commits
5 Commits
mqtt_resen
...
api_const
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01f71aead5 | ||
|
|
b44727aee6 | ||
|
|
1a55254258 | ||
|
|
baf2b0e3c9 | ||
|
|
9612134dd7 |
@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
|
||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||
// HA state max length is 255, so 256 byte buffer covers all cases
|
||||
char state_buf[256];
|
||||
size_t copy_len = msg.state.size();
|
||||
if (copy_len >= sizeof(state_buf)) {
|
||||
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
|
||||
// HA state max length is 255 characters, but attributes can be much longer
|
||||
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||
size_t state_len = msg.state.size();
|
||||
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
|
||||
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||
if (state_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||
}
|
||||
if (copy_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), copy_len);
|
||||
}
|
||||
state_buf[copy_len] = '\0';
|
||||
it.callback(StringRef(state_buf, copy_len));
|
||||
state_buf[state_len] = '\0';
|
||||
it.callback(StringRef(state_buf, state_len));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
|
||||
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
|
||||
uint8_t *buffer = buffer_alloc.get(len + 1);
|
||||
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
buffer[0] = a_register;
|
||||
std::copy(data, data + len, buffer + 1);
|
||||
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
|
||||
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
|
||||
uint8_t *buffer = buffer_alloc.get(len + 2);
|
||||
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
buffer[0] = a_register >> 8;
|
||||
buffer[1] = a_register;
|
||||
|
||||
@@ -11,22 +11,6 @@
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
|
||||
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
|
||||
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
|
||||
public:
|
||||
uint8_t *get(size_t size) {
|
||||
if (size <= STACK_SIZE) {
|
||||
return this->stack_buffer_;
|
||||
}
|
||||
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
|
||||
return this->heap_buffer_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t stack_buffer_[STACK_SIZE];
|
||||
std::unique_ptr<uint8_t[]> heap_buffer_;
|
||||
};
|
||||
|
||||
/// @brief Error codes returned by I2CBus and I2CDevice methods
|
||||
enum ErrorCode {
|
||||
NO_ERROR = 0, ///< No error found during execution of method
|
||||
@@ -92,8 +76,8 @@ class I2CBus {
|
||||
total_len += read_buffers[i].len;
|
||||
}
|
||||
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
|
||||
uint8_t *buffer = buffer_alloc.get(total_len);
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
|
||||
if (err != ERROR_OK)
|
||||
@@ -116,8 +100,8 @@ class I2CBus {
|
||||
total_len += write_buffers[i].len;
|
||||
}
|
||||
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
|
||||
uint8_t *buffer = buffer_alloc.get(total_len);
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; i != count; i++) {
|
||||
|
||||
@@ -396,12 +396,6 @@ void MQTTClientComponent::loop() {
|
||||
|
||||
this->last_connected_ = now;
|
||||
this->resubscribe_subscriptions_();
|
||||
|
||||
// Process pending resends for all MQTT components centrally
|
||||
// This is more efficient than each component polling in its own loop
|
||||
for (MQTTComponent *component : this->children_) {
|
||||
component->process_resend();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -307,12 +307,16 @@ void MQTTComponent::call_setup() {
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTComponent::process_resend() {
|
||||
// Called by MQTTClientComponent when connected to process pending resends
|
||||
// Note: is_internal() check not needed - internal components are never registered
|
||||
if (!this->resend_state_)
|
||||
void MQTTComponent::call_loop() {
|
||||
if (this->is_internal())
|
||||
return;
|
||||
|
||||
this->loop();
|
||||
|
||||
if (!this->resend_state_ || !this->is_connected_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->resend_state_ = false;
|
||||
if (this->is_discovery_enabled()) {
|
||||
if (!this->send_discovery_()) {
|
||||
|
||||
@@ -81,6 +81,8 @@ class MQTTComponent : public Component {
|
||||
/// Override setup_ so that we can call send_discovery() when needed.
|
||||
void call_setup() override;
|
||||
|
||||
void call_loop() override;
|
||||
|
||||
void call_dump_config() override;
|
||||
|
||||
/// Send discovery info the Home Assistant, override this.
|
||||
@@ -131,9 +133,6 @@ class MQTTComponent : public Component {
|
||||
/// Internal method for the MQTT client base to schedule a resend of the state on reconnect.
|
||||
void schedule_resend_state();
|
||||
|
||||
/// Process pending resend if needed (called by MQTTClientComponent)
|
||||
void process_resend();
|
||||
|
||||
/** Send a MQTT message.
|
||||
*
|
||||
* @param topic The topic.
|
||||
|
||||
@@ -107,9 +107,9 @@ std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol) {
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) {
|
||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) {
|
||||
#if USE_NETWORK_IPV6
|
||||
if (ip_address.find(':') != std::string::npos) {
|
||||
if (strchr(ip_address, ':') != nullptr) {
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
@@ -121,14 +121,14 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
|
||||
|
||||
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
// Use standard inet_pton for BSD sockets
|
||||
if (inet_pton(AF_INET6, ip_address.c_str(), &server->sin6_addr) != 1) {
|
||||
if (inet_pton(AF_INET6, ip_address, &server->sin6_addr) != 1) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
// Use LWIP-specific functions
|
||||
ip6_addr_t ip6;
|
||||
inet6_aton(ip_address.c_str(), &ip6);
|
||||
inet6_aton(ip_address, &ip6);
|
||||
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||
#endif
|
||||
return sizeof(sockaddr_in6);
|
||||
@@ -141,7 +141,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
|
||||
auto *server = reinterpret_cast<sockaddr_in *>(addr);
|
||||
memset(server, 0, sizeof(sockaddr_in));
|
||||
server->sin_family = AF_INET;
|
||||
server->sin_addr.s_addr = inet_addr(ip_address.c_str());
|
||||
server->sin_addr.s_addr = inet_addr(ip_address);
|
||||
server->sin_port = htons(port);
|
||||
return sizeof(sockaddr_in);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,17 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
|
||||
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol);
|
||||
|
||||
/// Set a sockaddr to the specified address and port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port);
|
||||
/// @param addr Destination sockaddr structure
|
||||
/// @param addrlen Size of the addr buffer
|
||||
/// @param ip_address Null-terminated IP address string (IPv4 or IPv6)
|
||||
/// @param port Port number in host byte order
|
||||
/// @return Size of the sockaddr structure used, or 0 on error
|
||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port);
|
||||
|
||||
/// Convenience overload for std::string (backward compatible).
|
||||
inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) {
|
||||
return set_sockaddr(addr, addrlen, ip_address.c_str(), port);
|
||||
}
|
||||
|
||||
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
|
||||
|
||||
@@ -7,14 +7,14 @@ DEPENDENCIES = ["network"]
|
||||
|
||||
status_ns = cg.esphome_ns.namespace("status")
|
||||
StatusBinarySensor = status_ns.class_(
|
||||
"StatusBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
"StatusBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
|
||||
StatusBinarySensor,
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace status {
|
||||
namespace esphome::status {
|
||||
|
||||
static const char *const TAG = "status";
|
||||
|
||||
void StatusBinarySensor::loop() {
|
||||
void StatusBinarySensor::update() {
|
||||
bool status = network::is_connected();
|
||||
#ifdef USE_MQTT
|
||||
if (mqtt::global_mqtt_client != nullptr) {
|
||||
@@ -33,5 +32,4 @@ void StatusBinarySensor::loop() {
|
||||
void StatusBinarySensor::setup() { this->publish_initial_state(false); }
|
||||
void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); }
|
||||
|
||||
} // namespace status
|
||||
} // namespace esphome
|
||||
} // namespace esphome::status
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace status {
|
||||
namespace esphome::status {
|
||||
|
||||
class StatusBinarySensor : public binary_sensor::BinarySensor, public Component {
|
||||
class StatusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent {
|
||||
public:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -16,5 +15,4 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component
|
||||
bool is_status_binary_sensor() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace status
|
||||
} // namespace esphome
|
||||
} // namespace esphome::status
|
||||
|
||||
@@ -366,6 +366,35 @@ template<typename T> class FixedVector {
|
||||
const T *end() const { return data_ + size_; }
|
||||
};
|
||||
|
||||
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
|
||||
/// This is useful when most operations need a small buffer but occasionally need larger ones.
|
||||
/// The stack buffer avoids heap allocation in the common case, while heap fallback handles edge cases.
|
||||
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
|
||||
public:
|
||||
explicit SmallBufferWithHeapFallback(size_t size) {
|
||||
if (size <= STACK_SIZE) {
|
||||
this->buffer_ = this->stack_buffer_;
|
||||
} else {
|
||||
this->heap_buffer_ = new uint8_t[size];
|
||||
this->buffer_ = this->heap_buffer_;
|
||||
}
|
||||
}
|
||||
~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; }
|
||||
|
||||
// Delete copy and move operations to prevent double-delete
|
||||
SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete;
|
||||
SmallBufferWithHeapFallback &operator=(const SmallBufferWithHeapFallback &) = delete;
|
||||
SmallBufferWithHeapFallback(SmallBufferWithHeapFallback &&) = delete;
|
||||
SmallBufferWithHeapFallback &operator=(SmallBufferWithHeapFallback &&) = delete;
|
||||
|
||||
uint8_t *get() { return this->buffer_; }
|
||||
|
||||
private:
|
||||
uint8_t stack_buffer_[STACK_SIZE];
|
||||
uint8_t *heap_buffer_{nullptr};
|
||||
uint8_t *buffer_;
|
||||
};
|
||||
|
||||
///@}
|
||||
|
||||
/// @name Mathematics
|
||||
|
||||
@@ -108,6 +108,25 @@ text_sensor:
|
||||
format: "HA Empty state updated: %s"
|
||||
args: ['x.c_str()']
|
||||
|
||||
# Test long attribute handling (>255 characters)
|
||||
# HA states are limited to 255 chars, but attributes are not
|
||||
- platform: homeassistant
|
||||
name: "HA Long Attribute"
|
||||
entity_id: sensor.long_data
|
||||
attribute: long_value
|
||||
id: ha_long_attribute
|
||||
on_value:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "HA Long attribute received, length: %d"
|
||||
args: ['x.size()']
|
||||
# Log the first 50 and last 50 chars to verify no truncation
|
||||
- lambda: |-
|
||||
if (x.size() >= 100) {
|
||||
ESP_LOGI("test", "Long attribute first 50 chars: %.50s", x.c_str());
|
||||
ESP_LOGI("test", "Long attribute last 50 chars: %s", x.c_str() + x.size() - 50);
|
||||
}
|
||||
|
||||
# Number component for testing HA number control
|
||||
number:
|
||||
- platform: template
|
||||
|
||||
@@ -40,6 +40,7 @@ async def test_api_homeassistant(
|
||||
humidity_update_future = loop.create_future()
|
||||
motion_update_future = loop.create_future()
|
||||
weather_update_future = loop.create_future()
|
||||
long_attr_future = loop.create_future()
|
||||
|
||||
# Number future
|
||||
ha_number_future = loop.create_future()
|
||||
@@ -58,6 +59,7 @@ async def test_api_homeassistant(
|
||||
humidity_update_pattern = re.compile(r"HA Humidity state updated: ([\d.]+)")
|
||||
motion_update_pattern = re.compile(r"HA Motion state changed: (ON|OFF)")
|
||||
weather_update_pattern = re.compile(r"HA Weather condition updated: (\w+)")
|
||||
long_attr_pattern = re.compile(r"HA Long attribute received, length: (\d+)")
|
||||
|
||||
# Number pattern
|
||||
ha_number_pattern = re.compile(r"Setting HA number to: ([\d.]+)")
|
||||
@@ -143,8 +145,14 @@ async def test_api_homeassistant(
|
||||
elif not weather_update_future.done() and weather_update_pattern.search(line):
|
||||
weather_update_future.set_result(line)
|
||||
|
||||
# Check number pattern
|
||||
elif not ha_number_future.done() and ha_number_pattern.search(line):
|
||||
# Check long attribute pattern - separate if since it can come at different times
|
||||
if not long_attr_future.done():
|
||||
match = long_attr_pattern.search(line)
|
||||
if match:
|
||||
long_attr_future.set_result(int(match.group(1)))
|
||||
|
||||
# Check number pattern - separate if since it can come at different times
|
||||
if not ha_number_future.done():
|
||||
match = ha_number_pattern.search(line)
|
||||
if match:
|
||||
ha_number_future.set_result(match.group(1))
|
||||
@@ -179,6 +187,14 @@ async def test_api_homeassistant(
|
||||
client.send_home_assistant_state("binary_sensor.external_motion", "", "ON")
|
||||
client.send_home_assistant_state("weather.home", "condition", "sunny")
|
||||
|
||||
# Send a long attribute (300 characters) to test that attributes aren't truncated
|
||||
# HA states are limited to 255 chars, but attributes are NOT limited
|
||||
# This tests the fix for the 256-byte buffer truncation bug
|
||||
long_attr_value = "X" * 300 # 300 chars - enough to expose truncation bug
|
||||
client.send_home_assistant_state(
|
||||
"sensor.long_data", "long_value", long_attr_value
|
||||
)
|
||||
|
||||
# Test edge cases for zero-copy implementation safety
|
||||
# Empty entity_id should be silently ignored (no crash)
|
||||
client.send_home_assistant_state("", "", "should_be_ignored")
|
||||
@@ -225,6 +241,13 @@ async def test_api_homeassistant(
|
||||
number_value = await asyncio.wait_for(ha_number_future, timeout=5.0)
|
||||
assert number_value == "42.5", f"Unexpected number value: {number_value}"
|
||||
|
||||
# Long attribute test - verify 300 chars weren't truncated to 255
|
||||
long_attr_len = await asyncio.wait_for(long_attr_future, timeout=5.0)
|
||||
assert long_attr_len == 300, (
|
||||
f"Long attribute was truncated! Expected 300 chars, got {long_attr_len}. "
|
||||
"This indicates the 256-byte truncation bug."
|
||||
)
|
||||
|
||||
# Wait for completion
|
||||
await asyncio.wait_for(tests_complete_future, timeout=5.0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user