From 663a4304e01baf6d0d03caa2e105880f8f716116 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 18 Dec 2025 07:50:31 -0500 Subject: [PATCH 1/9] [libretiny] Fix millis() ambiguity on BK72XX (#12534) Co-authored-by: Claude --- esphome/core/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index 97157b6f92..507a39b401 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -382,10 +382,15 @@ def include_file(path: Path, basename: Path, is_c_header: bool = False): ARDUINO_GLUE_CODE = """\ +#undef yield #define yield() esphome::yield() +#undef millis #define millis() esphome::millis() +#undef micros #define micros() esphome::micros() +#undef delay #define delay(x) esphome::delay(x) +#undef delayMicroseconds #define delayMicroseconds(x) esphome::delayMicroseconds(x) """ @@ -536,7 +541,7 @@ async def to_code(config: ConfigType) -> None: if config[CONF_DEBUG_SCHEDULER]: cg.add_define("ESPHOME_DEBUG_SCHEDULER") - if CORE.using_arduino and not CORE.is_bk72xx: + if CORE.using_arduino: CORE.add_job(add_arduino_global_workaround) if config[CONF_INCLUDES]: From b47b7d43fd29d1837a2b1499900ba1424a6aa13d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 09:06:16 -0700 Subject: [PATCH 2/9] [api] Remove unused force parameter from encode_message (#12551) --- esphome/components/api/api_pb2.cpp | 24 ++++++++++++------------ esphome/components/api/proto.h | 4 ++-- script/api_protobuf/api_protobuf.py | 22 ++++++++++++++-------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4a89ee78e1..52f4b495e9 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -124,12 +124,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it, true); + buffer.encode_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it, true); + buffer.encode_message(21, it); } #endif #ifdef USE_AREAS @@ -878,13 +878,13 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->service_ref_); for (auto &it : this->data) { - buffer.encode_message(2, it, true); + buffer.encode_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it, true); + buffer.encode_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -1011,7 +1011,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name_ref_); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it); } buffer.encode_uint32(4, static_cast(this->supports_response)); } @@ -1867,7 +1867,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it, true); + buffer.encode_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -1987,7 +1987,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i], true); + buffer.encode_message(1, this->advertisements[i]); } } void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { @@ -2060,7 +2060,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it, true); + buffer.encode_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } @@ -2081,7 +2081,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } @@ -2097,7 +2097,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it, true); + buffer.encode_message(2, it); } } void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { @@ -2557,7 +2557,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it, true); + buffer.encode_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 83b6922be1..efdab9341c 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -334,7 +334,7 @@ class ProtoWriteBuffer { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { this->encode_uint64(field_id, encode_zigzag64(value), force); } - void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); + void encode_message(uint32_t field_id, const ProtoMessage &value); std::vector *get_buffer() const { return buffer_; } protected: @@ -795,7 +795,7 @@ class ProtoSize { }; // Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { this->encode_field_raw(field_id, 2); // type 2: Length-delimited message // Calculate the message size first diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3412fac5db..cb09ef7050 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1215,6 +1215,9 @@ class FixedArrayRepeatedType(TypeInfo): """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + # MessageType.encode_message doesn't have a force parameter + if isinstance(self._ti, MessageType): + return f"buffer.{self._ti.encode_func}({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property @@ -1536,6 +1539,15 @@ class RepeatedTypeInfo(TypeInfo): # std::vector is specialized for bool, reference does not work return isinstance(self._ti, BoolType) + def _encode_element_call(self, element: str) -> str: + """Helper to generate encode call for a single element.""" + if isinstance(self._ti, EnumType): + return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + # MessageType.encode_message doesn't have a force parameter + if isinstance(self._ti, MessageType): + return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + @property def encode_content(self) -> str: if self._use_pointer: @@ -1546,17 +1558,11 @@ class RepeatedTypeInfo(TypeInfo): o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" else: o = f"for (const auto &it : *this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" - else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += f" {self._encode_element_call('it')}\n" o += "}" return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" - else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += f" {self._encode_element_call('it')}\n" o += "}" return o From 2cf6ed2af729bed731326b2eac6342e19010b8c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 09:07:35 -0700 Subject: [PATCH 3/9] [socket] Refactor socket implementations for memory efficiency and code quality (#12550) --- .../components/socket/bsd_sockets_impl.cpp | 106 ++++++++++-------- .../components/socket/lwip_raw_tcp_impl.cpp | 6 +- .../components/socket/lwip_sockets_impl.cpp | 106 ++++++++++-------- esphome/components/socket/socket.cpp | 28 +---- esphome/components/socket/socket.h | 13 +-- 5 files changed, 122 insertions(+), 137 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index c7cca62027..09cd81752a 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -12,8 +12,7 @@ #include #endif -namespace esphome { -namespace socket { +namespace esphome::socket { std::string format_sockaddr(const struct sockaddr_storage &storage) { if (storage.ss_family == AF_INET) { @@ -44,11 +43,11 @@ class BSDSocketImpl : public Socket { BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT // Register new socket with the application for select() if monitoring requested - if (monitor_loop && fd_ >= 0) { + if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds - loop_monitored_ = App.register_socket_fd(fd_); + this->loop_monitored_ = App.register_socket_fd(this->fd_); } else { - loop_monitored_ = false; + this->loop_monitored_ = false; } #else // Without select support, ignore monitor_loop parameter @@ -56,70 +55,69 @@ class BSDSocketImpl : public Socket { #endif } ~BSDSocketImpl() override { - if (!closed_) { - close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) + if (!this->closed_) { + this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) } } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(fd_, addr, addrlen); } + int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); } std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, true); - } - - private: - std::unique_ptr accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) { - int fd = ::accept(fd_, addr, addrlen); + int fd = ::accept(this->fd_, addr, addrlen); if (fd == -1) return {}; - return make_unique(fd, loop_monitored); + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = ::accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); } - public: - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int close() override { - if (!closed_) { + if (!this->closed_) { #ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored - if (loop_monitored_) { - App.unregister_socket_fd(fd_); + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } #endif - int ret = ::close(fd_); - closed_ = true; + int ret = ::close(this->fd_); + this->closed_ = true; return ret; } return 0; } - int shutdown(int how) override { return ::shutdown(fd_, how); } + int shutdown(int how) override { return ::shutdown(this->fd_, how); } - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); } + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { + return ::getpeername(this->fd_, addr, addrlen); + } std::string getpeername() override { struct sockaddr_storage storage; socklen_t len = sizeof(storage); - int err = this->getpeername((struct sockaddr *) &storage, &len); - if (err != 0) + if (::getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) return {}; return format_sockaddr(storage); } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { + return ::getsockname(this->fd_, addr, addrlen); + } std::string getsockname() override { struct sockaddr_storage storage; socklen_t len = sizeof(storage); - int err = this->getsockname((struct sockaddr *) &storage, &len); - if (err != 0) + if (::getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0) return {}; return format_sockaddr(storage); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return ::getsockopt(fd_, level, optname, optval, optlen); + return ::getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return ::setsockopt(fd_, level, optname, optval, optlen); + return ::setsockopt(this->fd_, level, optname, optval, optlen); } - int listen(int backlog) override { return ::listen(fd_, backlog); } - ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } + int listen(int backlog) override { return ::listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) override { return ::read(this->fd_, buf, len); } ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { #if defined(USE_ESP32) || defined(USE_HOST) return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); @@ -129,41 +127,52 @@ class BSDSocketImpl : public Socket { } ssize_t readv(const struct iovec *iov, int iovcnt) override { #if defined(USE_ESP32) - return ::lwip_readv(fd_, iov, iovcnt); + return ::lwip_readv(this->fd_, iov, iovcnt); #else - return ::readv(fd_, iov, iovcnt); + return ::readv(this->fd_, iov, iovcnt); #endif } - ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return ::send(fd_, buf, len, flags); } + ssize_t write(const void *buf, size_t len) override { return ::write(this->fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } ssize_t writev(const struct iovec *iov, int iovcnt) override { #if defined(USE_ESP32) - return ::lwip_writev(fd_, iov, iovcnt); + return ::lwip_writev(this->fd_, iov, iovcnt); #else - return ::writev(fd_, iov, iovcnt); + return ::writev(this->fd_, iov, iovcnt); #endif } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) + return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) } int setblocking(bool blocking) override { - int fl = ::fcntl(fd_, F_GETFL, 0); + int fl = ::fcntl(this->fd_, F_GETFL, 0); if (blocking) { fl &= ~O_NONBLOCK; } else { fl |= O_NONBLOCK; } - ::fcntl(fd_, F_SETFL, fl); + ::fcntl(this->fd_, F_SETFL, fl); return 0; } - int get_fd() const override { return fd_; } + int get_fd() const override { return this->fd_; } + +#ifdef USE_SOCKET_SELECT_SUPPORT + bool ready() const override { + if (!this->loop_monitored_) + return true; + return App.is_socket_ready(this->fd_); + } +#endif protected: int fd_; - bool closed_ = false; + bool closed_{false}; +#ifdef USE_SOCKET_SELECT_SUPPORT + bool loop_monitored_{false}; +#endif }; // Helper to create a socket with optional monitoring @@ -182,7 +191,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 328df24bdd..cb5d17d5af 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -18,8 +18,7 @@ #include // For esp_schedule() #endif -namespace esphome { -namespace socket { +namespace esphome::socket { #ifdef USE_ESP8266 // Flag to signal socket activity - checked by socket_delay() to exit early @@ -711,7 +710,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return socket(domain, type, protocol); } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index d94c1fb2ff..23fb1a7f6f 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -7,8 +7,7 @@ #include #include "esphome/core/application.h" -namespace esphome { -namespace socket { +namespace esphome::socket { std::string format_sockaddr(const struct sockaddr_storage &storage) { if (storage.ss_family == AF_INET) { @@ -37,11 +36,11 @@ class LwIPSocketImpl : public Socket { LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT // Register new socket with the application for select() if monitoring requested - if (monitor_loop && fd_ >= 0) { + if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds - loop_monitored_ = App.register_socket_fd(fd_); + this->loop_monitored_ = App.register_socket_fd(this->fd_); } else { - loop_monitored_ = false; + this->loop_monitored_ = false; } #else // Without select support, ignore monitor_loop parameter @@ -49,96 +48,108 @@ class LwIPSocketImpl : public Socket { #endif } ~LwIPSocketImpl() override { - if (!closed_) { - close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) + if (!this->closed_) { + this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) } } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_connect(fd_, addr, addrlen); } + int connect(const struct sockaddr *addr, socklen_t addrlen) override { + return lwip_connect(this->fd_, addr, addrlen); + } std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, true); - } - - private: - std::unique_ptr accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) { - int fd = lwip_accept(fd_, addr, addrlen); + int fd = lwip_accept(this->fd_, addr, addrlen); if (fd == -1) return {}; - return make_unique(fd, loop_monitored); + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = lwip_accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); } - public: - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(fd_, addr, addrlen); } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int close() override { - if (!closed_) { + if (!this->closed_) { #ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored - if (loop_monitored_) { - App.unregister_socket_fd(fd_); + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } #endif - int ret = lwip_close(fd_); - closed_ = true; + int ret = lwip_close(this->fd_); + this->closed_ = true; return ret; } return 0; } - int shutdown(int how) override { return lwip_shutdown(fd_, how); } + int shutdown(int how) override { return lwip_shutdown(this->fd_, how); } - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getpeername(fd_, addr, addrlen); } + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { + return lwip_getpeername(this->fd_, addr, addrlen); + } std::string getpeername() override { struct sockaddr_storage storage; socklen_t len = sizeof(storage); - int err = this->getpeername((struct sockaddr *) &storage, &len); - if (err != 0) + if (lwip_getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) return {}; return format_sockaddr(storage); } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getsockname(fd_, addr, addrlen); } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { + return lwip_getsockname(this->fd_, addr, addrlen); + } std::string getsockname() override { struct sockaddr_storage storage; socklen_t len = sizeof(storage); - int err = this->getsockname((struct sockaddr *) &storage, &len); - if (err != 0) + if (lwip_getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0) return {}; return format_sockaddr(storage); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return lwip_getsockopt(fd_, level, optname, optval, optlen); + return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return lwip_setsockopt(fd_, level, optname, optval, optlen); + return lwip_setsockopt(this->fd_, level, optname, optval, optlen); } - int listen(int backlog) override { return lwip_listen(fd_, backlog); } - ssize_t read(void *buf, size_t len) override { return lwip_read(fd_, buf, len); } + int listen(int backlog) override { return lwip_listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); } ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { - return lwip_recvfrom(fd_, buf, len, 0, addr, addr_len); + return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); } - ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(fd_, iov, iovcnt); } - ssize_t write(const void *buf, size_t len) override { return lwip_write(fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return lwip_send(fd_, buf, len, flags); } - ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(fd_, iov, iovcnt); } + ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); } + ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } + ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return lwip_sendto(fd_, buf, len, flags, to, tolen); + return lwip_sendto(this->fd_, buf, len, flags, to, tolen); } int setblocking(bool blocking) override { - int fl = lwip_fcntl(fd_, F_GETFL, 0); + int fl = lwip_fcntl(this->fd_, F_GETFL, 0); if (blocking) { fl &= ~O_NONBLOCK; } else { fl |= O_NONBLOCK; } - lwip_fcntl(fd_, F_SETFL, fl); + lwip_fcntl(this->fd_, F_SETFL, fl); return 0; } - int get_fd() const override { return fd_; } + int get_fd() const override { return this->fd_; } + +#ifdef USE_SOCKET_SELECT_SUPPORT + bool ready() const override { + if (!this->loop_monitored_) + return true; + return App.is_socket_ready(this->fd_); + } +#endif protected: int fd_; - bool closed_ = false; + bool closed_{false}; +#ifdef USE_SOCKET_SELECT_SUPPORT + bool loop_monitored_{false}; +#endif }; // Helper to create a socket with optional monitoring @@ -157,7 +168,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index cc9232d21a..ffe0233abc 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -6,33 +6,10 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace socket { +namespace esphome::socket { Socket::~Socket() {} -bool Socket::ready() const { -#ifdef USE_SOCKET_SELECT_SUPPORT - if (!loop_monitored_) { - // Non-monitored sockets always return true (assume data may be available) - return true; - } - - // For loop-monitored sockets, check with the Application's select() results - int fd = this->get_fd(); - if (fd < 0) { - // No valid file descriptor, assume ready (fallback behavior) - return true; - } - - return App.is_socket_ready(fd); -#else - // Without select() support, we can't monitor sockets in the loop - // Always return true (assume data may be available) - return true; -#endif -} - std::unique_ptr socket_ip(int type, int protocol) { #if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); @@ -113,6 +90,5 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po return sizeof(sockaddr_in); #endif /* USE_NETWORK_IPV6 */ } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 8936b2cd10..75eb07de4a 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -6,8 +6,7 @@ #include "headers.h" #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) -namespace esphome { -namespace socket { +namespace esphome::socket { class Socket { public: @@ -54,12 +53,7 @@ class Socket { /// Check if socket has data ready to read /// For loop-monitored sockets, checks with the Application's select() results /// For non-monitored sockets, always returns true (assumes data may be available) - bool ready() const; - - protected: -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; ///< Whether this socket is monitored by the event loop -#endif + virtual bool ready() const { return true; } }; /// Create a socket of the given domain, type and protocol. @@ -91,6 +85,5 @@ void socket_delay(uint32_t ms); void socket_wake(); #endif -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif From cd93468225ccbf6fef8c1e9272e123416c24ad41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 06:41:23 -1000 Subject: [PATCH 4/9] [core] Move comment to PROGMEM on ESP8266 --- esphome/components/web_server/web_server.cpp | 4 +- esphome/core/application.h | 25 ++++++---- esphome/core/build_info_data.h | 2 + esphome/core/config.py | 3 +- esphome/writer.py | 28 +++++++++--- tests/unit_tests/test_writer.py | 48 ++++++++++++++++++-- 6 files changed, 88 insertions(+), 22 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0c22c2f08d..1508fce89d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -287,7 +287,9 @@ std::string WebServer::get_config_json() { JsonObject root = builder.root(); root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); - root[ESPHOME_F("comment")] = App.get_comment_ref(); + char comment_buffer[ESPHOME_COMMENT_SIZE]; + App.get_comment_string(comment_buffer, sizeof(comment_buffer)); + root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else diff --git a/esphome/core/application.h b/esphome/core/application.h index f462553a81..712748be9d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -12,6 +12,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/progmem.h" #include "esphome/core/scheduler.h" #include "esphome/core/string_ref.h" #include "esphome/core/version.h" @@ -104,8 +105,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, - bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -124,7 +124,6 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->comment_ = comment; } #ifdef USE_DEVICES @@ -257,10 +256,21 @@ class Application { return ""; } - /// Get the comment of this Application set by pre_setup(). - std::string get_comment() const { return this->comment_; } - /// Get the comment as StringRef (avoids allocation) - StringRef get_comment_ref() const { return StringRef(this->comment_); } + /// Copy the comment string into the provided buffer + /// Buffer must be ESPHOME_COMMENT_SIZE bytes + void get_comment_string(char *buffer, size_t size) { + ESPHOME_strncpy_P(buffer, ESPHOME_COMMENT_STR, size); + buffer[size - 1] = '\0'; + } + + /// Get the comment of this Application (deprecated, use get_comment_string() instead) + // Remove before 2026.7.0 + ESPDEPRECATED("Use get_comment_string() instead. Removed in 2026.7.0", "2026.1.0") + std::string get_comment() { + char buffer[ESPHOME_COMMENT_SIZE]; + this->get_comment_string(buffer, sizeof(buffer)); + return std::string(buffer); + } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } @@ -501,7 +511,6 @@ class Application { // Pointer-sized members first Component *current_component_{nullptr}; - const char *comment_{nullptr}; // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components diff --git a/esphome/core/build_info_data.h b/esphome/core/build_info_data.h index 5e424ffaca..02bb465e44 100644 --- a/esphome/core/build_info_data.h +++ b/esphome/core/build_info_data.h @@ -7,4 +7,6 @@ #define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT #define ESPHOME_BUILD_TIME 1700000000 // NOLINT +#define ESPHOME_COMMENT_SIZE 1 // NOLINT static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000"; +static const char ESPHOME_COMMENT_STR[] = ""; diff --git a/esphome/core/config.py b/esphome/core/config.py index 507a39b401..5e32b9380d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -209,7 +209,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), cv.Optional(CONF_AREA): validate_area_config, - cv.Optional(CONF_COMMENT): cv.string, + cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( { @@ -505,7 +505,6 @@ async def to_code(config: ConfigType) -> None: cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], - config.get(CONF_COMMENT, ""), config[CONF_NAME_ADD_MAC_SUFFIX], ) ) diff --git a/esphome/writer.py b/esphome/writer.py index 183fff8730..8de5791a5a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -271,7 +271,7 @@ def copy_src_tree(): "esphome", "core", "build_info_data.h" ) build_info_json_path = CORE.relative_build_path("build_info.json") - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() # Defensively force a rebuild if the build_info files don't exist, or if # there was a config change which didn't actually cause a source change @@ -292,7 +292,9 @@ def copy_src_tree(): if sources_changed: write_file( build_info_data_h_path, - generate_build_info_data_h(config_hash, build_time, build_time_str), + generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ), ) write_file( build_info_json_path, @@ -332,31 +334,43 @@ def generate_version_h(): ) -def get_build_info() -> tuple[int, int, str]: +def get_build_info() -> tuple[int, int, str, str]: """Calculate build_info values from current config. Returns: - Tuple of (config_hash, build_time, build_time_str) + Tuple of (config_hash, build_time, build_time_str, comment) """ config_hash = CORE.config_hash build_time = int(time.time()) build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time)) - return config_hash, build_time, build_time_str + comment = CORE.comment or "" + return config_hash, build_time, build_time_str, comment + + +def _escape_c_string(s: str) -> str: + """Escape a string for use in a C string literal.""" + return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") def generate_build_info_data_h( - config_hash: int, build_time: int, build_time_str: str + config_hash: int, build_time: int, build_time_str: str, comment: str ) -> str: - """Generate build_info_data.h header with config hash and build time.""" + """Generate build_info_data.h header with config hash, build time, and comment.""" + escaped_comment = _escape_c_string(comment) + # +1 for null terminator + comment_size = len(comment) + 1 return f"""#pragma once // Auto-generated build_info data #define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT #define ESPHOME_BUILD_TIME {build_time} // NOLINT +#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT #ifdef USE_ESP8266 #include static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}"; #else static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}"; #endif """ diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 06a7d5dbdf..86d7cb106b 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1357,36 +1357,67 @@ def test_generate_build_info_data_h_format() -> None: config_hash = 0x12345678 build_time = 1700000000 build_time_str = "2023-11-14 22:13:20 +0000" + comment = "Test comment" - result = generate_build_info_data_h(config_hash, build_time, build_time_str) + result = generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ) assert "#pragma once" in result assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result assert "#define ESPHOME_BUILD_TIME 1700000000" in result + assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1 assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result + assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result def test_generate_build_info_data_h_esp8266_progmem() -> None: """Test generate_build_info_data_h includes PROGMEM for ESP8266.""" - result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test") + result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment") # Should have ESP8266 PROGMEM conditional assert "#ifdef USE_ESP8266" in result assert "#include " in result assert "PROGMEM" in result + # Both build time and comment should have PROGMEM versions + assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result + assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result def test_generate_build_info_data_h_hash_formatting() -> None: """Test generate_build_info_data_h formats hash with leading zeros.""" # Test with small hash value that needs leading zeros - result = generate_build_info_data_h(0x00000001, 0, "test") + result = generate_build_info_data_h(0x00000001, 0, "test", "") assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result # Test with larger hash value - result = generate_build_info_data_h(0xFFFFFFFF, 0, "test") + result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "") assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result +def test_generate_build_info_data_h_comment_escaping() -> None: + """Test generate_build_info_data_h properly escapes special characters in comment.""" + # Test backslash escaping + result = generate_build_info_data_h(0, 0, "test", "backslash\\here") + assert 'ESPHOME_COMMENT_STR[] = "backslash\\\\here"' in result + + # Test quote escaping + result = generate_build_info_data_h(0, 0, "test", 'has "quotes"') + assert 'ESPHOME_COMMENT_STR[] = "has \\"quotes\\""' in result + + # Test newline escaping + result = generate_build_info_data_h(0, 0, "test", "line1\nline2") + assert 'ESPHOME_COMMENT_STR[] = "line1\\nline2"' in result + + +def test_generate_build_info_data_h_empty_comment() -> None: + """Test generate_build_info_data_h handles empty comment.""" + result = generate_build_info_data_h(0, 0, "test", "") + + assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator + assert 'ESPHOME_COMMENT_STR[] = ""' in result + + @patch("esphome.writer.CORE") @patch("esphome.writer.iter_components") @patch("esphome.writer.walk_files") @@ -1445,6 +1476,7 @@ def test_copy_src_tree_writes_build_info_files( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "Test comment" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [("core", mock_component)] @@ -1466,6 +1498,8 @@ def test_copy_src_tree_writes_build_info_files( assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content assert "#define ESPHOME_BUILD_TIME" in build_info_h_content assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content + assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content + assert "ESPHOME_COMMENT_STR" in build_info_h_content # Verify build_info.json was written build_info_json_path = build_path / "build_info.json" @@ -1517,6 +1551,7 @@ def test_copy_src_tree_detects_config_hash_change( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF # Different from existing + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1578,6 +1613,7 @@ def test_copy_src_tree_detects_version_change( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1627,6 +1663,7 @@ def test_copy_src_tree_handles_invalid_build_info_json( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1700,6 +1737,7 @@ def test_copy_src_tree_build_info_timestamp_behavior( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [("test", mock_component)] @@ -1794,6 +1832,7 @@ def test_copy_src_tree_detects_removed_source_file( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] # No components = file should be removed @@ -1855,6 +1894,7 @@ def test_copy_src_tree_ignores_removed_generated_file( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] From 23ee8bdcaf9c939941e96acc02341cf691f25c13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 06:48:23 -1000 Subject: [PATCH 5/9] [core] Move comment to PROGMEM on ESP8266 --- tests/unit_tests/test_writer.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 86d7cb106b..293b7781c0 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1186,8 +1186,9 @@ def test_get_build_info_new_build( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "Test comment" - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1195,6 +1196,7 @@ def test_get_build_info_new_build( assert isinstance(build_time_str, str) # Verify build_time_str format matches expected pattern assert len(build_time_str) >= 19 # e.g., "2025-12-15 16:27:44 +0000" + assert comment == "Test comment" @patch("esphome.writer.CORE") @@ -1206,6 +1208,7 @@ def test_get_build_info_always_returns_current_time( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create existing build_info.json with matching config_hash and version existing_build_time = 1700000000 @@ -1222,7 +1225,7 @@ def test_get_build_info_always_returns_current_time( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 # get_build_info now always returns current time @@ -1240,6 +1243,7 @@ def test_get_build_info_config_changed( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0xABCDEF00 # Different from existing + mock_core.comment = "" # Create existing build_info.json with different config_hash existing_build_time = 1700000000 @@ -1255,7 +1259,7 @@ def test_get_build_info_config_changed( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0xABCDEF00 assert build_time != existing_build_time # New time generated @@ -1271,6 +1275,7 @@ def test_get_build_info_version_changed( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create existing build_info.json with different version existing_build_time = 1700000000 @@ -1286,7 +1291,7 @@ def test_get_build_info_version_changed( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): # New version - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert build_time != existing_build_time # New time generated @@ -1302,11 +1307,12 @@ def test_get_build_info_invalid_json( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create invalid JSON file build_info_path.write_text("not valid json {{{") - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1322,12 +1328,13 @@ def test_get_build_info_missing_keys( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create JSON with missing keys build_info_path.write_text(json.dumps({"config_hash": 0x12345678})) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1343,8 +1350,9 @@ def test_get_build_info_build_time_str_format( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() # Verify the format matches "%Y-%m-%d %H:%M:%S %z" # e.g., "2025-12-15 16:27:44 +0000" From 455091a03f1981f2cf85d02014f2d6459003bebf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 06:55:02 -1000 Subject: [PATCH 6/9] tweaks --- esphome/writer.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 8de5791a5a..9ae40e417a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -21,6 +21,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, get_str_env, is_ha_addon, read_file, @@ -347,16 +348,12 @@ def get_build_info() -> tuple[int, int, str, str]: return config_hash, build_time, build_time_str, comment -def _escape_c_string(s: str) -> str: - """Escape a string for use in a C string literal.""" - return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") - - def generate_build_info_data_h( config_hash: int, build_time: int, build_time_str: str, comment: str ) -> str: """Generate build_info_data.h header with config hash, build time, and comment.""" - escaped_comment = _escape_c_string(comment) + # cpp_string_escape returns '"escaped"', slice off the quotes since template has them + escaped_comment = cpp_string_escape(comment)[1:-1] # +1 for null terminator comment_size = len(comment) + 1 return f"""#pragma once From 12d8e2ada2df6044ede2d4f95d6e532375c86c11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 06:56:12 -1000 Subject: [PATCH 7/9] tweaks --- tests/unit_tests/test_writer.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 293b7781c0..f354d71bb7 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1404,18 +1404,24 @@ def test_generate_build_info_data_h_hash_formatting() -> None: def test_generate_build_info_data_h_comment_escaping() -> None: - """Test generate_build_info_data_h properly escapes special characters in comment.""" - # Test backslash escaping + r"""Test generate_build_info_data_h properly escapes special characters in comment. + + Uses cpp_string_escape which outputs octal escapes for special characters: + - backslash (ASCII 92) -> \134 + - double quote (ASCII 34) -> \042 + - newline (ASCII 10) -> \012 + """ + # Test backslash escaping (ASCII 92 = octal 134) result = generate_build_info_data_h(0, 0, "test", "backslash\\here") - assert 'ESPHOME_COMMENT_STR[] = "backslash\\\\here"' in result + assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result - # Test quote escaping + # Test quote escaping (ASCII 34 = octal 042) result = generate_build_info_data_h(0, 0, "test", 'has "quotes"') - assert 'ESPHOME_COMMENT_STR[] = "has \\"quotes\\""' in result + assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result - # Test newline escaping + # Test newline escaping (ASCII 10 = octal 012) result = generate_build_info_data_h(0, 0, "test", "line1\nline2") - assert 'ESPHOME_COMMENT_STR[] = "line1\\nline2"' in result + assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result def test_generate_build_info_data_h_empty_comment() -> None: From 5547f9f5d624ec0212884a8d4fc93ea8f569cf2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 06:58:32 -1000 Subject: [PATCH 8/9] span --- esphome/components/web_server/web_server.cpp | 2 +- esphome/core/application.h | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1508fce89d..f1f1dbd6fb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -288,7 +288,7 @@ std::string WebServer::get_config_json() { root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); char comment_buffer[ESPHOME_COMMENT_SIZE]; - App.get_comment_string(comment_buffer, sizeof(comment_buffer)); + App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal diff --git a/esphome/core/application.h b/esphome/core/application.h index 712748be9d..d2addd01d6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -257,10 +257,10 @@ class Application { } /// Copy the comment string into the provided buffer - /// Buffer must be ESPHOME_COMMENT_SIZE bytes - void get_comment_string(char *buffer, size_t size) { - ESPHOME_strncpy_P(buffer, ESPHOME_COMMENT_STR, size); - buffer[size - 1] = '\0'; + /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) + void get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); + buffer[buffer.size() - 1] = '\0'; } /// Get the comment of this Application (deprecated, use get_comment_string() instead) @@ -268,7 +268,7 @@ class Application { ESPDEPRECATED("Use get_comment_string() instead. Removed in 2026.7.0", "2026.1.0") std::string get_comment() { char buffer[ESPHOME_COMMENT_SIZE]; - this->get_comment_string(buffer, sizeof(buffer)); + this->get_comment_string(buffer); return std::string(buffer); } From 3a69cb9c135de4d3291c715648bd3907504d7396 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Dec 2025 07:53:48 -1000 Subject: [PATCH 9/9] tidy --- tests/dummy_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 5849f4eb95..e6fe733807 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "comment", false); + App.pre_setup("livingroom", "LivingRoom", false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0);