Merge branch 'batch_ping_fallback' into integration

This commit is contained in:
J. Nick Koston
2025-06-26 10:05:06 +02:00
10 changed files with 231 additions and 263 deletions

View File

@@ -1,28 +1,11 @@
--- ---
name: Lock name: Lock closed issues and PRs
on: on:
schedule: schedule:
- cron: "30 0 * * *" - cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch: workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs: jobs:
lock: lock:
runs-on: ubuntu-latest uses: esphome/workflows/.github/workflows/lock.yml@main
steps:
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open

View File

@@ -168,6 +168,18 @@ void APIConnection::loop() {
ESP_LOGVV(TAG, "Failed to send ping directly, scheduling at front of batch"); ESP_LOGVV(TAG, "Failed to send ping directly, scheduling at front of batch");
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE);
this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings
// Also set up retry tracking from upstream
this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
this->ping_retries_++;
if (this->ping_retries_ >= MAX_PING_RETRIES) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
} else {
ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
}
} }
} }
@@ -267,6 +279,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
// Encode directly into buffer // Encode directly into buffer
msg.encode(buffer); msg.encode(buffer);
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message for VV debugging
conn->log_send_message_(msg.message_name(), msg.dump());
#endif
// Calculate actual encoded size (not including header that was already added) // Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode; size_t actual_payload_size = shared_buf.size() - size_before_encode;

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService {
template<typename T> bool send_message(const T &msg) { template<typename T> bool send_message(const T &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump()); this->log_send_message_(msg.message_name(), msg.dump());
#endif #endif
return this->send_message_(msg, T::MESSAGE_TYPE); return this->send_message_(msg, T::MESSAGE_TYPE);
} }

View File

@@ -335,6 +335,7 @@ class ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const; std::string dump() const;
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif #endif
protected: protected:

View File

@@ -4,7 +4,7 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
from esphome import git from esphome import yaml_util
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -23,7 +23,6 @@ from esphome.const import (
CONF_REFRESH, CONF_REFRESH,
CONF_SOURCE, CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
CONF_URL,
CONF_VARIANT, CONF_VARIANT,
CONF_VERSION, CONF_VERSION,
KEY_CORE, KEY_CORE,
@@ -32,14 +31,13 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_ESP32, PLATFORM_ESP32,
TYPE_GIT,
TYPE_LOCAL,
__version__, __version__,
) )
from esphome.core import CORE, HexInt, TimePeriod from esphome.core import CORE, HexInt, TimePeriod
from esphome.cpp_generator import RawExpression from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType
from .boards import BOARDS from .boards import BOARDS
from .const import ( # noqa from .const import ( # noqa
@@ -49,10 +47,8 @@ from .const import ( # noqa
KEY_EXTRA_BUILD_FILES, KEY_EXTRA_BUILD_FILES,
KEY_PATH, KEY_PATH,
KEY_REF, KEY_REF,
KEY_REFRESH,
KEY_REPO, KEY_REPO,
KEY_SDKCONFIG_OPTIONS, KEY_SDKCONFIG_OPTIONS,
KEY_SUBMODULES,
KEY_VARIANT, KEY_VARIANT,
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
@@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
def add_idf_component( def add_idf_component(
*, *,
name: str, name: str,
repo: str, repo: str = None,
ref: str = None, ref: str = None,
path: str = None, path: str = None,
refresh: TimePeriod = None, refresh: TimePeriod = None,
@@ -245,30 +241,27 @@ def add_idf_component(
"""Add an esp-idf component to the project.""" """Add an esp-idf component to the project."""
if not CORE.using_esp_idf: if not CORE.using_esp_idf:
raise ValueError("Not an esp-idf project") raise ValueError("Not an esp-idf project")
if components is None: if not repo and not ref and not path:
components = [] raise ValueError("Requires at least one of repo, ref or path")
if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: if refresh or submodules or components:
_LOGGER.warning(
"The refresh, components and submodules parameters in add_idf_component() are "
"deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report "
"an issue to the external_component author and ask them to update it."
)
if components:
for comp in components:
CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = {
KEY_REPO: repo,
KEY_REF: ref,
KEY_PATH: f"{path}/{comp}" if path else comp,
}
else:
CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = {
KEY_REPO: repo, KEY_REPO: repo,
KEY_REF: ref, KEY_REF: ref,
KEY_PATH: path, KEY_PATH: path,
KEY_REFRESH: refresh,
KEY_COMPONENTS: components,
KEY_SUBMODULES: submodules,
} }
else:
component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name]
if components is not None:
component_config[KEY_COMPONENTS] = list(
set(component_config[KEY_COMPONENTS] + components)
)
if submodules is not None:
if component_config[KEY_SUBMODULES] is None:
component_config[KEY_SUBMODULES] = submodules
else:
component_config[KEY_SUBMODULES] = list(
set(component_config[KEY_SUBMODULES] + submodules)
)
def add_extra_script(stage: str, filename: str, path: str): def add_extra_script(stage: str, filename: str, path: str):
@@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
def _validate_idf_component(config: ConfigType) -> ConfigType:
"""Validate IDF component config and warn about deprecated options."""
if CONF_REFRESH in config:
_LOGGER.warning(
"The 'refresh' option for IDF components is deprecated and has no effect. "
"It will be removed in ESPHome 2026.1. Please remove it from your configuration."
)
return config
ESP_IDF_FRAMEWORK_SCHEMA = cv.All( ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.Schema( cv.All(
{ cv.Schema(
cv.Required(CONF_NAME): cv.string_strict, {
cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_PATH): cv.string, cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REFRESH, default="1d"): cv.All( cv.Optional(CONF_REF): cv.string,
cv.string, cv.source_refresh cv.Optional(CONF_PATH): cv.string,
), cv.Optional(CONF_REFRESH): cv.All(
} cv.string, cv.source_refresh
),
}
),
_validate_idf_component,
) )
), ),
} }
@@ -814,18 +822,12 @@ async def to_code(config):
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
for component in conf[CONF_COMPONENTS]: for component in conf[CONF_COMPONENTS]:
source = component[CONF_SOURCE] add_idf_component(
if source[CONF_TYPE] == TYPE_GIT: name=component[CONF_NAME],
add_idf_component( repo=component.get(CONF_SOURCE),
name=component[CONF_NAME], ref=component.get(CONF_REF),
repo=source[CONF_URL], path=component.get(CONF_PATH),
ref=source.get(CONF_REF), )
path=component.get(CONF_PATH),
refresh=component[CONF_REFRESH],
)
elif source[CONF_TYPE] == TYPE_LOCAL:
_LOGGER.warning("Local components are not implemented yet.")
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
cg.add_platformio_option("framework", "arduino") cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
@@ -924,6 +926,26 @@ def _write_sdkconfig():
write_file_if_changed(sdk_path, contents) write_file_if_changed(sdk_path, contents)
def _write_idf_component_yml():
yml_path = Path(CORE.relative_build_path("src/idf_component.yml"))
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
dependencies = {}
for name, component in components.items():
dependency = {}
if component[KEY_REF]:
dependency["version"] = component[KEY_REF]
if component[KEY_REPO]:
dependency["git"] = component[KEY_REPO]
if component[KEY_PATH]:
dependency["path"] = component[KEY_PATH]
dependencies[name] = dependency
contents = yaml_util.dump({"dependencies": dependencies})
else:
contents = ""
write_file_if_changed(yml_path, contents)
# Called by writer.py # Called by writer.py
def copy_files(): def copy_files():
if CORE.using_arduino: if CORE.using_arduino:
@@ -936,6 +958,7 @@ def copy_files():
) )
if CORE.using_esp_idf: if CORE.using_esp_idf:
_write_sdkconfig() _write_sdkconfig()
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
@@ -952,55 +975,6 @@ def copy_files():
__version__, __version__,
) )
import shutil
shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True)
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
for name, component in components.items():
repo_dir, _ = git.clone_or_update(
url=component[KEY_REPO],
ref=component[KEY_REF],
refresh=component[KEY_REFRESH],
domain="idf_components",
submodules=component[KEY_SUBMODULES],
)
mkdir_p(CORE.relative_build_path("components"))
component_dir = repo_dir
if component[KEY_PATH] is not None:
component_dir = component_dir / component[KEY_PATH]
if component[KEY_COMPONENTS] == ["*"]:
shutil.copytree(
component_dir,
CORE.relative_build_path("components"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
elif len(component[KEY_COMPONENTS]) > 0:
for comp in component[KEY_COMPONENTS]:
shutil.copytree(
component_dir / comp,
CORE.relative_build_path(f"components/{comp}"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
else:
shutil.copytree(
component_dir,
CORE.relative_build_path(f"components/{name}"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items():
if file[KEY_PATH].startswith("http"): if file[KEY_PATH].startswith("http"):
import requests import requests

View File

@@ -17,8 +17,9 @@ namespace esphome {
namespace ld2450 { namespace ld2450 {
static const char *const TAG = "ld2450"; static const char *const TAG = "ld2450";
static const char *const NO_MAC("08:05:04:03:02:01"); static const char *const NO_MAC = "08:05:04:03:02:01";
static const char *const UNKNOWN_MAC("unknown"); static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
// LD2450 UART Serial Commands // LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0x00FF; static const uint8_t CMD_ENABLE_CONF = 0x00FF;
@@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) {
return STATIONARY; return STATIONARY;
} }
static inline std::string format_version(uint8_t *buffer) {
return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
buffer[14]);
}
LD2450Component::LD2450Component() {}
void LD2450Component::setup() { void LD2450Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup"); ESP_LOGCONFIG(TAG, "Running setup");
#ifdef USE_NUMBER #ifdef USE_NUMBER
@@ -189,7 +183,7 @@ void LD2450Component::dump_config() {
" Throttle: %ums\n" " Throttle: %ums\n"
" MAC Address: %s\n" " MAC Address: %s\n"
" Firmware version: %s", " Firmware version: %s",
this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
} }
void LD2450Component::loop() { void LD2450Component::loop() {
@@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
break; break;
case lowbyte(CMD_VERSION): case lowbyte(CMD_VERSION):
this->version_ = ld2450::format_version(buffer); this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) { if (this->version_text_sensor_ != nullptr) {
@@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) { if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
} }
#endif #endif
break; break;

View File

@@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
#endif #endif
public: public:
LD2450Component();
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
@@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice {
bool get_timeout_status_(uint32_t check_millis); bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint32_t last_periodic_millis_ = 0; uint32_t last_periodic_millis_ = 0;
uint32_t presence_millis_ = 0; uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0; uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0; uint16_t throttle_ = 0;
uint16_t timeout_ = 5; uint16_t timeout_ = 5;
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t zone_type_ = 0; uint8_t zone_type_ = 0;
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
std::string version_{}; std::string version_{};
std::string mac_{}; std::string mac_{};
#ifdef USE_NUMBER #ifdef USE_NUMBER

View File

@@ -886,7 +886,7 @@ def build_message_type(
public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
snake_name = camel_to_snake(desc.name) snake_name = camel_to_snake(desc.name)
public_content.append( public_content.append(
f'static constexpr const char *message_name() {{ return "{snake_name}"; }}' f'const char *message_name() const override {{ return "{snake_name}"; }}'
) )
public_content.append("#endif") public_content.append("#endif")
@@ -1356,7 +1356,7 @@ def main() -> None:
hpp += " template<typename T>\n" hpp += " template<typename T>\n"
hpp += " bool send_message(const T &msg) {\n" hpp += " bool send_message(const T &msg) {\n"
hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
hpp += " this->log_send_message_(T::message_name(), msg.dump());\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n"
hpp += "#endif\n" hpp += "#endif\n"
hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n" hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n"
hpp += " }\n\n" hpp += " }\n\n"

0
script/run-in-env.py Normal file → Executable file
View File