mirror of
https://github.com/esphome/esphome.git
synced 2025-11-19 00:05:43 +00:00
Compare commits
3 Commits
document_t
...
claude/cre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00c7bf0a19 | ||
|
|
23624aff09 | ||
|
|
755357b7c6 |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -70,9 +70,6 @@ void BME68xBSEC2Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_,
|
||||
this->bme68x_status_);
|
||||
if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) {
|
||||
ESP_LOGE(TAG, "No sensors, add at least one sensor to the config");
|
||||
}
|
||||
}
|
||||
|
||||
if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) {
|
||||
|
||||
@@ -72,16 +72,6 @@ def _final_validate(config: ConfigType) -> ConfigType:
|
||||
"Add 'ap:' to your WiFi configuration to enable the captive portal."
|
||||
)
|
||||
|
||||
# Register socket needs for DNS server and additional HTTP connections
|
||||
# - 1 UDP socket for DNS server
|
||||
# - 3 additional TCP sockets for captive portal detection probes + configuration requests
|
||||
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
|
||||
# Need headroom for actual user configuration requests.
|
||||
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(4, "captive_portal")(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
ESP_LOGI(TAG, "Requested WiFi Settings Change:");
|
||||
ESP_LOGI(TAG, " SSID='%s'", ssid.c_str());
|
||||
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
||||
wifi::global_wifi_component->start_scanning();
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
}
|
||||
|
||||
@@ -63,12 +63,6 @@ void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler(this);
|
||||
#ifdef USE_ESP32
|
||||
// Enable LRU socket purging to handle captive portal detection probe bursts
|
||||
// OS captive portal detection makes many simultaneous HTTP requests which can
|
||||
// exhaust sockets. LRU purging automatically closes oldest idle connections.
|
||||
this->base_->get_server()->set_lru_purge_enable(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
@@ -40,10 +40,6 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
void end() {
|
||||
this->active_ = false;
|
||||
this->disable_loop(); // Stop processing DNS requests
|
||||
#ifdef USE_ESP32
|
||||
// Disable LRU socket purging now that captive portal is done
|
||||
this->base_->get_server()->set_lru_purge_enable(false);
|
||||
#endif
|
||||
this->base_->deinit();
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->stop();
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace esphome {
|
||||
namespace dashboard_import {
|
||||
|
||||
static const char *g_package_import_url = ""; // NOLINT
|
||||
static std::string g_package_import_url; // NOLINT
|
||||
|
||||
const char *get_package_import_url() { return g_package_import_url; }
|
||||
void set_package_import_url(const char *url) { g_package_import_url = url; }
|
||||
const std::string &get_package_import_url() { return g_package_import_url; }
|
||||
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
|
||||
|
||||
} // namespace dashboard_import
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace dashboard_import {
|
||||
|
||||
const char *get_package_import_url();
|
||||
void set_package_import_url(const char *url);
|
||||
const std::string &get_package_import_url();
|
||||
void set_package_import_url(std::string url);
|
||||
|
||||
} // namespace dashboard_import
|
||||
} // namespace esphome
|
||||
|
||||
@@ -931,12 +931,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||
|
||||
@@ -20,10 +20,6 @@ CONF_ON_STOP = "on_stop"
|
||||
CONF_STATUS_INDICATOR = "status_indicator"
|
||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
||||
|
||||
# Default WiFi timeout - aligned with WiFi component ap_timeout
|
||||
# Allows sufficient time to try all BSSIDs before starting provisioning mode
|
||||
DEFAULT_WIFI_TIMEOUT = "90s"
|
||||
|
||||
|
||||
improv_ns = cg.esphome_ns.namespace("improv")
|
||||
Error = improv_ns.enum("Error")
|
||||
@@ -63,7 +59,7 @@ CONFIG_SCHEMA = (
|
||||
CONF_AUTHORIZED_DURATION, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT
|
||||
CONF_WIFI_TIMEOUT, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
|
||||
@@ -127,7 +127,6 @@ void ESP32ImprovComponent::loop() {
|
||||
// Set initial state based on whether we have an authorizer
|
||||
this->set_state_(this->get_initial_state_(), false);
|
||||
this->set_error_(improv::ERROR_NONE);
|
||||
this->should_start_ = false; // Clear flag after starting
|
||||
ESP_LOGD(TAG, "Service started!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||
void start();
|
||||
void stop();
|
||||
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
|
||||
bool should_start() const { return this->should_start_; }
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace esphome {
|
||||
namespace ld2410 {
|
||||
|
||||
static const char *const TAG = "ld2410";
|
||||
static const char *const UNKNOWN_MAC = "unknown";
|
||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
|
||||
|
||||
enum BaudRate : uint8_t {
|
||||
BAUD_RATE_9600 = 1,
|
||||
@@ -179,15 +181,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
|
||||
}
|
||||
|
||||
void LD2410Component::dump_config() {
|
||||
char mac_s[18];
|
||||
char version_s[20];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
std::string mac_str =
|
||||
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
|
||||
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
|
||||
this->version_[4], this->version_[3], this->version_[2]);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LD2410:\n"
|
||||
" Firmware version: %s\n"
|
||||
" MAC address: %s",
|
||||
version_s, mac_str);
|
||||
version.c_str(), mac_str.c_str());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
ESP_LOGCONFIG(TAG, "Binary Sensors:");
|
||||
LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
|
||||
@@ -446,12 +448,12 @@ bool LD2410Component::handle_ack_data_() {
|
||||
|
||||
case CMD_QUERY_VERSION: {
|
||||
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
|
||||
char version_s[20];
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
ESP_LOGV(TAG, "Firmware version: %s", version_s);
|
||||
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
|
||||
this->version_[4], this->version_[3], this->version_[2]);
|
||||
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
this->version_text_sensor_->publish_state(version_s);
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -504,9 +506,9 @@ bool LD2410Component::handle_ack_data_() {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
|
||||
char mac_s[18];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
ESP_LOGV(TAG, "MAC address: %s", mac_str);
|
||||
std::string mac_str =
|
||||
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
|
||||
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->mac_text_sensor_ != nullptr) {
|
||||
this->mac_text_sensor_->publish_state(mac_str);
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace esphome {
|
||||
namespace ld2412 {
|
||||
|
||||
static const char *const TAG = "ld2412";
|
||||
static const char *const UNKNOWN_MAC = "unknown";
|
||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
|
||||
|
||||
enum BaudRate : uint8_t {
|
||||
BAUD_RATE_9600 = 1,
|
||||
@@ -198,15 +200,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
|
||||
}
|
||||
|
||||
void LD2412Component::dump_config() {
|
||||
char mac_s[18];
|
||||
char version_s[20];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
std::string mac_str =
|
||||
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
|
||||
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
|
||||
this->version_[4], this->version_[3], this->version_[2]);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LD2412:\n"
|
||||
" Firmware version: %s\n"
|
||||
" MAC address: %s",
|
||||
version_s, mac_str);
|
||||
version.c_str(), mac_str.c_str());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
ESP_LOGCONFIG(TAG, "Binary Sensors:");
|
||||
LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus",
|
||||
@@ -490,12 +492,12 @@ bool LD2412Component::handle_ack_data_() {
|
||||
|
||||
case CMD_QUERY_VERSION: {
|
||||
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
|
||||
char version_s[20];
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
ESP_LOGV(TAG, "Firmware version: %s", version_s);
|
||||
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
|
||||
this->version_[4], this->version_[3], this->version_[2]);
|
||||
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
this->version_text_sensor_->publish_state(version_s);
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -542,9 +544,9 @@ bool LD2412Component::handle_ack_data_() {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
|
||||
char mac_s[18];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
ESP_LOGV(TAG, "MAC address: %s", mac_str);
|
||||
std::string mac_str =
|
||||
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
|
||||
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->mac_text_sensor_ != nullptr) {
|
||||
this->mac_text_sensor_->publish_state(mac_str);
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace esphome {
|
||||
namespace ld2450 {
|
||||
|
||||
static const char *const TAG = "ld2450";
|
||||
static const char *const UNKNOWN_MAC = "unknown";
|
||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
|
||||
|
||||
enum BaudRate : uint8_t {
|
||||
BAUD_RATE_9600 = 1,
|
||||
@@ -190,15 +192,15 @@ void LD2450Component::setup() {
|
||||
}
|
||||
|
||||
void LD2450Component::dump_config() {
|
||||
char mac_s[18];
|
||||
char version_s[20];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
std::string mac_str =
|
||||
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
|
||||
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
|
||||
this->version_[4], this->version_[3], this->version_[2]);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LD2450:\n"
|
||||
" Firmware version: %s\n"
|
||||
" MAC address: %s",
|
||||
version_s, mac_str);
|
||||
version.c_str(), mac_str.c_str());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
ESP_LOGCONFIG(TAG, "Binary Sensors:");
|
||||
LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
|
||||
@@ -640,12 +642,12 @@ bool LD2450Component::handle_ack_data_() {
|
||||
|
||||
case CMD_QUERY_VERSION: {
|
||||
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
|
||||
char version_s[20];
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
ESP_LOGV(TAG, "Firmware version: %s", version_s);
|
||||
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
|
||||
this->version_[4], this->version_[3], this->version_[2]);
|
||||
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
this->version_text_sensor_->publish_state(version_s);
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -661,9 +663,9 @@ bool LD2450Component::handle_ack_data_() {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
|
||||
char mac_s[18];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
ESP_LOGV(TAG, "MAC address: %s", mac_str);
|
||||
std::string mac_str =
|
||||
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
|
||||
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->mac_text_sensor_ != nullptr) {
|
||||
this->mac_text_sensor_->publish_state(mac_str);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \
|
||||
@@ -40,27 +39,6 @@
|
||||
namespace esphome {
|
||||
namespace ld24xx {
|
||||
|
||||
static const char *const UNKNOWN_MAC = "unknown";
|
||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
|
||||
|
||||
// Helper function to format MAC address with stack allocation
|
||||
// Returns pointer to UNKNOWN_MAC constant or formatted buffer
|
||||
// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator)
|
||||
inline const char *format_mac_str(const uint8_t *mac_address, std::span<char, 18> buffer) {
|
||||
if (mac_address_is_valid(mac_address)) {
|
||||
format_mac_addr_upper(mac_address, buffer.data());
|
||||
return buffer.data();
|
||||
}
|
||||
return UNKNOWN_MAC;
|
||||
}
|
||||
|
||||
// Helper function to format firmware version with stack allocation
|
||||
// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety)
|
||||
inline void format_version_str(const uint8_t *version, std::span<char, 20> buffer) {
|
||||
snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3],
|
||||
version[2]);
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Helper class to store a sensor with a deduplicator & publish state only when the value changes
|
||||
template<typename T> class SensorWithDedup {
|
||||
|
||||
@@ -261,10 +261,6 @@ async def component_to_code(config):
|
||||
cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}")
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
|
||||
# LibreTiny uses MULTI_NO_ATOMICS because platforms like BK7231N (ARM968E-S) lack
|
||||
# exclusive load/store (no LDREX/STREX). std::atomic RMW operations require libatomic,
|
||||
# which is not linked to save flash (4-8KB). Even if linked, libatomic would use locks
|
||||
# (ATOMIC_INT_LOCK_FREE=1), so explicit FreeRTOS mutexes are simpler and equivalent.
|
||||
cg.add_define(ThreadModel.MULTI_NO_ATOMICS)
|
||||
|
||||
# force using arduino framework
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -115,9 +114,7 @@ class SpinboxType(WidgetType):
|
||||
w.obj, digits, digits - config[CONF_DECIMAL_PLACES]
|
||||
)
|
||||
if (value := config.get(CONF_VALUE)) is not None:
|
||||
lv.spinbox_set_value(
|
||||
w.obj, MockObj(await lv_float.process(value)) * w.get_scale()
|
||||
)
|
||||
lv.spinbox_set_value(w.obj, await lv_float.process(value))
|
||||
|
||||
def get_scale(self, config):
|
||||
return 10 ** config[CONF_DECIMAL_PLACES]
|
||||
|
||||
@@ -135,7 +135,8 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
|
||||
#ifdef USE_DASHBOARD_IMPORT
|
||||
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
|
||||
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url())});
|
||||
txt_records.push_back(
|
||||
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
|
||||
#endif
|
||||
}
|
||||
#endif // USE_API
|
||||
|
||||
@@ -350,7 +350,6 @@ void MipiRgb::dump_config() {
|
||||
"\n Width: %u"
|
||||
"\n Height: %u"
|
||||
"\n Rotation: %d degrees"
|
||||
"\n PCLK Inverted: %s"
|
||||
"\n HSync Pulse Width: %u"
|
||||
"\n HSync Back Porch: %u"
|
||||
"\n HSync Front Porch: %u"
|
||||
@@ -358,18 +357,18 @@ void MipiRgb::dump_config() {
|
||||
"\n VSync Back Porch: %u"
|
||||
"\n VSync Front Porch: %u"
|
||||
"\n Invert Colors: %s"
|
||||
"\n Pixel Clock: %uMHz"
|
||||
"\n Pixel Clock: %dMHz"
|
||||
"\n Reset Pin: %s"
|
||||
"\n DE Pin: %s"
|
||||
"\n PCLK Pin: %s"
|
||||
"\n HSYNC Pin: %s"
|
||||
"\n VSYNC Pin: %s",
|
||||
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
|
||||
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
|
||||
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
|
||||
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
|
||||
this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_,
|
||||
this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_,
|
||||
this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000,
|
||||
get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(),
|
||||
get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(),
|
||||
get_pin_name(this->vsync_pin_).c_str());
|
||||
|
||||
if (this->madctl_ & MADCTL_BGR) {
|
||||
this->dump_pins_(8, 13, "Blue", 0);
|
||||
|
||||
@@ -11,7 +11,6 @@ st7701s.extend(
|
||||
vsync_pin=17,
|
||||
pclk_pin=21,
|
||||
pclk_frequency="12MHz",
|
||||
pclk_inverted=False,
|
||||
pixel_mode="18bit",
|
||||
mirror_x=True,
|
||||
mirror_y=True,
|
||||
|
||||
87
esphome/components/motion_map/__init__.py
Normal file
87
esphome/components/motion_map/__init__.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Motion Map Component for ESPHome.
|
||||
|
||||
This component uses Wi-Fi Channel State Information (CSI) to detect motion
|
||||
without cameras or microphones, providing privacy-preserving presence detection.
|
||||
"""
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_sdkconfig_option,
|
||||
only_on_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["esp32", "wifi"]
|
||||
AUTO_LOAD = []
|
||||
|
||||
motion_map_ns = cg.esphome_ns.namespace("motion_map")
|
||||
MotionMapComponent = motion_map_ns.class_("MotionMapComponent", cg.Component)
|
||||
|
||||
# For sub-components to reference the parent
|
||||
CONF_MOTION_MAP_ID = "motion_map_id"
|
||||
|
||||
# Configuration keys
|
||||
CONF_MOTION_THRESHOLD = "motion_threshold"
|
||||
CONF_IDLE_THRESHOLD = "idle_threshold"
|
||||
CONF_WINDOW_SIZE = "window_size"
|
||||
CONF_MAC_ADDRESS = "mac_address"
|
||||
CONF_SENSITIVITY = "sensitivity"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MotionMapComponent),
|
||||
cv.Optional(CONF_MOTION_THRESHOLD, default=0.5): cv.float_range(
|
||||
min=0.0, max=1.0
|
||||
),
|
||||
cv.Optional(CONF_IDLE_THRESHOLD, default=0.2): cv.float_range(
|
||||
min=0.0, max=1.0
|
||||
),
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=100): cv.int_range(
|
||||
min=10, max=500
|
||||
),
|
||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_SENSITIVITY, default=1.0): cv.float_range(
|
||||
min=0.1, max=5.0
|
||||
),
|
||||
}
|
||||
),
|
||||
only_on_variant(supported=[VARIANT_ESP32S3]),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate C++ code for the motion map component."""
|
||||
# Enable CSI in ESP-IDF SDK config
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLE_CSI", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CSI_ENABLED", True)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_motion_threshold(config[CONF_MOTION_THRESHOLD]))
|
||||
cg.add(var.set_idle_threshold(config[CONF_IDLE_THRESHOLD]))
|
||||
cg.add(var.set_window_size(config[CONF_WINDOW_SIZE]))
|
||||
cg.add(var.set_sensitivity(config[CONF_SENSITIVITY]))
|
||||
|
||||
if CONF_MAC_ADDRESS in config:
|
||||
mac_address = config[CONF_MAC_ADDRESS].parts
|
||||
cg.add(
|
||||
var.set_mac_address(
|
||||
[
|
||||
mac_address[0],
|
||||
mac_address[1],
|
||||
mac_address[2],
|
||||
mac_address[3],
|
||||
mac_address[4],
|
||||
mac_address[5],
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Add ESP-IDF component dependencies
|
||||
if CORE.using_esp_idf:
|
||||
cg.add_library("esp_wifi", None)
|
||||
35
esphome/components/motion_map/binary_sensor.py
Normal file
35
esphome/components/motion_map/binary_sensor.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Binary sensor platform for Motion Map component."""
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_MOTION,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
from . import CONF_MOTION_MAP_ID, MotionMapComponent, motion_map_ns
|
||||
|
||||
DEPENDENCIES = ["motion_map"]
|
||||
|
||||
MotionMapBinarySensor = motion_map_ns.class_(
|
||||
"MotionMapBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
|
||||
MotionMapBinarySensor,
|
||||
device_class=DEVICE_CLASS_MOTION,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_MOTION_MAP_ID): cv.use_id(MotionMapComponent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the motion binary sensor."""
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[CONF_MOTION_MAP_ID])
|
||||
cg.add(parent.set_motion_binary_sensor(var))
|
||||
305
esphome/components/motion_map/motion_map.cpp
Normal file
305
esphome/components/motion_map/motion_map.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
#include "motion_map.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace motion_map {
|
||||
|
||||
static const char *const TAG = "motion_map";
|
||||
|
||||
void MotionMapComponent::setup() {
|
||||
// Reserve space for variance window
|
||||
this->variance_window_.reserve(this->window_size_);
|
||||
|
||||
// Initialize CSI capture
|
||||
this->init_csi_();
|
||||
}
|
||||
|
||||
void MotionMapComponent::loop() {
|
||||
// Process new CSI data if available
|
||||
if (this->new_csi_data_) {
|
||||
this->new_csi_data_ = false;
|
||||
this->process_csi_data_();
|
||||
}
|
||||
|
||||
// Periodic sensor publishing (every 1 second)
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_update_time_ >= 1000) {
|
||||
this->publish_sensors_();
|
||||
this->last_update_time_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMapComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Motion Map:");
|
||||
ESP_LOGCONFIG(TAG, " Motion Threshold: %.2f\n Idle Threshold: %.2f\n Window Size: %u\n Sensitivity: %.2f",
|
||||
this->motion_threshold_, this->idle_threshold_, this->window_size_, this->sensitivity_);
|
||||
if (this->mac_address_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " MAC Filter: %02X:%02X:%02X:%02X:%02X:%02X", (*this->mac_address_)[0],
|
||||
(*this->mac_address_)[1], (*this->mac_address_)[2], (*this->mac_address_)[3],
|
||||
(*this->mac_address_)[4], (*this->mac_address_)[5]);
|
||||
}
|
||||
if (!this->csi_initialized_) {
|
||||
ESP_LOGW(TAG, "CSI not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMapComponent::init_csi_() {
|
||||
// Configure CSI
|
||||
wifi_csi_config_t csi_config = {};
|
||||
csi_config.lltf_en = true; // Enable Long Training Field
|
||||
csi_config.htltf_en = true; // Enable HT Long Training Field
|
||||
csi_config.stbc_htltf2_en = false; // Disable STBC HT-LTF2
|
||||
csi_config.ltf_merge_en = true; // Merge LTF
|
||||
csi_config.channel_filter_en = true; // Enable channel filter
|
||||
csi_config.manu_scale = false; // Auto scale
|
||||
csi_config.shift = 0; // No shift
|
||||
|
||||
esp_err_t err = esp_wifi_set_csi_config(&csi_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set CSI config: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Register CSI callback
|
||||
err = esp_wifi_set_csi_rx_cb(MotionMapComponent::csi_callback_, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set CSI RX callback: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable CSI
|
||||
err = esp_wifi_set_csi(true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to enable CSI: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
this->csi_initialized_ = true;
|
||||
ESP_LOGD(TAG, "CSI initialized");
|
||||
}
|
||||
|
||||
void MotionMapComponent::csi_callback_(void *ctx, wifi_csi_info_t *info) {
|
||||
auto *component = static_cast<MotionMapComponent *>(ctx);
|
||||
if (component == nullptr || info == nullptr || info->buf == nullptr || info->len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy CSI data to buffer for processing in main loop
|
||||
// This callback runs in WiFi task context
|
||||
size_t len = std::min(static_cast<size_t>(info->len), MAX_CSI_LEN);
|
||||
memcpy(component->csi_buffer_.data.data(), info->buf, len);
|
||||
component->csi_buffer_.len = len;
|
||||
memcpy(component->csi_buffer_.mac.data(), info->mac, 6);
|
||||
component->csi_buffer_.valid = true;
|
||||
component->new_csi_data_ = true;
|
||||
}
|
||||
|
||||
void MotionMapComponent::process_csi_data_() {
|
||||
if (!this->csi_buffer_.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter by MAC address if configured
|
||||
if (this->mac_address_.has_value()) {
|
||||
bool mac_match = true;
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
if (this->csi_buffer_.mac[i] != (*this->mac_address_)[i]) {
|
||||
mac_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mac_match) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CSI data from buffer
|
||||
const int8_t *csi_data = this->csi_buffer_.data.data();
|
||||
size_t csi_len = this->csi_buffer_.len;
|
||||
|
||||
// Calculate variance and amplitude
|
||||
float variance = this->calculate_variance_(csi_data, csi_len);
|
||||
float amplitude = this->calculate_amplitude_(csi_data, csi_len);
|
||||
|
||||
// Apply sensitivity scaling
|
||||
variance *= this->sensitivity_;
|
||||
|
||||
// Update moving window
|
||||
this->variance_window_.push_back(variance);
|
||||
if (this->variance_window_.size() > this->window_size_) {
|
||||
this->variance_window_.erase(this->variance_window_.begin());
|
||||
}
|
||||
|
||||
// Store current values
|
||||
this->current_variance_ = variance;
|
||||
this->current_amplitude_ = amplitude;
|
||||
|
||||
// Update motion state
|
||||
this->update_motion_state_(variance);
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_variance_(const int8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return 0.0f;
|
||||
|
||||
// Calculate mean
|
||||
float sum = 0.0f;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum += static_cast<float>(data[i]);
|
||||
}
|
||||
float mean = sum / static_cast<float>(len);
|
||||
|
||||
// Calculate variance
|
||||
float variance = 0.0f;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
float diff = static_cast<float>(data[i]) - mean;
|
||||
variance += diff * diff;
|
||||
}
|
||||
variance /= static_cast<float>(len);
|
||||
|
||||
return variance;
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_amplitude_(const int8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return 0.0f;
|
||||
|
||||
// Calculate RMS amplitude
|
||||
float sum_sq = 0.0f;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
float val = static_cast<float>(data[i]);
|
||||
sum_sq += val * val;
|
||||
}
|
||||
return sqrtf(sum_sq / static_cast<float>(len));
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_entropy_() {
|
||||
if (this->variance_window_.empty())
|
||||
return 0.0f;
|
||||
|
||||
// Simple entropy calculation using histogram
|
||||
const int num_bins = 10;
|
||||
std::array<int, num_bins> histogram = {};
|
||||
|
||||
// Find min/max for binning
|
||||
float min_val = this->variance_window_[0];
|
||||
float max_val = this->variance_window_[0];
|
||||
for (float val : this->variance_window_) {
|
||||
min_val = std::min(min_val, val);
|
||||
max_val = std::max(max_val, val);
|
||||
}
|
||||
|
||||
float range = max_val - min_val;
|
||||
if (range < 0.0001f)
|
||||
return 0.0f;
|
||||
|
||||
// Build histogram
|
||||
for (float val : this->variance_window_) {
|
||||
int bin = static_cast<int>(((val - min_val) / range) * (num_bins - 1));
|
||||
bin = std::max(0, std::min(num_bins - 1, bin));
|
||||
histogram[bin]++;
|
||||
}
|
||||
|
||||
// Calculate entropy
|
||||
float entropy = 0.0f;
|
||||
float total = static_cast<float>(this->variance_window_.size());
|
||||
for (int count : histogram) {
|
||||
if (count > 0) {
|
||||
float p = static_cast<float>(count) / total;
|
||||
entropy -= p * logf(p);
|
||||
}
|
||||
}
|
||||
|
||||
return entropy;
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_skewness_() {
|
||||
if (this->variance_window_.size() < 3)
|
||||
return 0.0f;
|
||||
|
||||
// Calculate mean
|
||||
float sum = 0.0f;
|
||||
for (float val : this->variance_window_) {
|
||||
sum += val;
|
||||
}
|
||||
float mean = sum / static_cast<float>(this->variance_window_.size());
|
||||
|
||||
// Calculate standard deviation and skewness
|
||||
float variance = 0.0f;
|
||||
float skewness_sum = 0.0f;
|
||||
for (float val : this->variance_window_) {
|
||||
float diff = val - mean;
|
||||
variance += diff * diff;
|
||||
skewness_sum += diff * diff * diff;
|
||||
}
|
||||
|
||||
float n = static_cast<float>(this->variance_window_.size());
|
||||
variance /= n;
|
||||
float std_dev = sqrtf(variance);
|
||||
|
||||
if (std_dev < 0.0001f)
|
||||
return 0.0f;
|
||||
|
||||
float skewness = (skewness_sum / n) / (std_dev * std_dev * std_dev);
|
||||
return skewness;
|
||||
}
|
||||
|
||||
void MotionMapComponent::update_motion_state_(float variance) {
|
||||
MotionState new_state = this->current_state_;
|
||||
|
||||
// Simple threshold-based state machine
|
||||
if (this->current_state_ == MotionState::IDLE) {
|
||||
if (variance > this->motion_threshold_) {
|
||||
new_state = MotionState::MOTION;
|
||||
}
|
||||
} else { // MOTION
|
||||
if (variance < this->idle_threshold_) {
|
||||
new_state = MotionState::IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
// Update state if changed
|
||||
if (new_state != this->current_state_) {
|
||||
this->current_state_ = new_state;
|
||||
ESP_LOGV(TAG, "State: %s", new_state == MotionState::MOTION ? "MOTION" : "IDLE");
|
||||
|
||||
// Publish binary sensor immediately on state change
|
||||
if (this->motion_binary_sensor_ != nullptr) {
|
||||
this->motion_binary_sensor_->publish_state(new_state == MotionState::MOTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMapComponent::publish_sensors_() {
|
||||
// Publish variance sensor
|
||||
if (this->variance_sensor_ != nullptr) {
|
||||
this->variance_sensor_->publish_state(this->current_variance_);
|
||||
}
|
||||
|
||||
// Publish amplitude sensor
|
||||
if (this->amplitude_sensor_ != nullptr) {
|
||||
this->amplitude_sensor_->publish_state(this->current_amplitude_);
|
||||
}
|
||||
|
||||
// Publish entropy sensor
|
||||
if (this->entropy_sensor_ != nullptr && !this->variance_window_.empty()) {
|
||||
float entropy = this->calculate_entropy_();
|
||||
this->entropy_sensor_->publish_state(entropy);
|
||||
}
|
||||
|
||||
// Publish skewness sensor
|
||||
if (this->skewness_sensor_ != nullptr && this->variance_window_.size() >= 3) {
|
||||
float skewness = this->calculate_skewness_();
|
||||
this->skewness_sensor_->publish_state(skewness);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace motion_map
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
128
esphome/components/motion_map/motion_map.h
Normal file
128
esphome/components/motion_map/motion_map.h
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_wifi_types.h"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Forward declarations
|
||||
namespace binary_sensor {
|
||||
class BinarySensor;
|
||||
}
|
||||
namespace sensor {
|
||||
class Sensor;
|
||||
}
|
||||
|
||||
namespace motion_map {
|
||||
|
||||
/// Motion state enumeration
|
||||
enum class MotionState : uint8_t {
|
||||
IDLE = 0,
|
||||
MOTION = 1,
|
||||
};
|
||||
|
||||
/// Maximum CSI buffer size for ESP32-S3
|
||||
static constexpr size_t MAX_CSI_LEN = 384;
|
||||
|
||||
/// CSI data buffer for cross-task communication
|
||||
struct CSIDataBuffer {
|
||||
std::array<int8_t, MAX_CSI_LEN> data;
|
||||
size_t len{0};
|
||||
std::array<uint8_t, 6> mac;
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Motion Map Component using Wi-Fi CSI for motion detection
|
||||
*
|
||||
* This component captures Channel State Information (CSI) from Wi-Fi packets
|
||||
* and analyzes signal variations to detect motion without cameras or microphones.
|
||||
*/
|
||||
class MotionMapComponent : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
// Configuration setters
|
||||
void set_motion_threshold(float threshold) { this->motion_threshold_ = threshold; }
|
||||
void set_idle_threshold(float threshold) { this->idle_threshold_ = threshold; }
|
||||
void set_window_size(uint32_t size) { this->window_size_ = size; }
|
||||
void set_sensitivity(float sensitivity) { this->sensitivity_ = sensitivity; }
|
||||
void set_mac_address(const std::array<uint8_t, 6> &mac) { this->mac_address_ = mac; }
|
||||
|
||||
// Sensor setters
|
||||
void set_motion_binary_sensor(binary_sensor::BinarySensor *sensor) { this->motion_binary_sensor_ = sensor; }
|
||||
void set_variance_sensor(sensor::Sensor *sensor) { this->variance_sensor_ = sensor; }
|
||||
void set_amplitude_sensor(sensor::Sensor *sensor) { this->amplitude_sensor_ = sensor; }
|
||||
void set_entropy_sensor(sensor::Sensor *sensor) { this->entropy_sensor_ = sensor; }
|
||||
void set_skewness_sensor(sensor::Sensor *sensor) { this->skewness_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
/// Initialize CSI capture
|
||||
void init_csi_();
|
||||
|
||||
/// CSI callback (static wrapper for ESP-IDF) - runs in WiFi task
|
||||
static void csi_callback_(void *ctx, wifi_csi_info_t *info);
|
||||
|
||||
/// Process CSI data in main loop
|
||||
void process_csi_data_();
|
||||
|
||||
/// Calculate variance from CSI data
|
||||
float calculate_variance_(const int8_t *data, size_t len);
|
||||
|
||||
/// Calculate amplitude from CSI data
|
||||
float calculate_amplitude_(const int8_t *data, size_t len);
|
||||
|
||||
/// Calculate entropy from variance window
|
||||
float calculate_entropy_();
|
||||
|
||||
/// Calculate skewness from variance window
|
||||
float calculate_skewness_();
|
||||
|
||||
/// Update motion state based on current variance
|
||||
void update_motion_state_(float variance);
|
||||
|
||||
/// Publish sensor values
|
||||
void publish_sensors_();
|
||||
|
||||
// Configuration parameters
|
||||
float motion_threshold_{0.5f};
|
||||
float idle_threshold_{0.2f};
|
||||
uint32_t window_size_{100};
|
||||
float sensitivity_{1.0f};
|
||||
optional<std::array<uint8_t, 6>> mac_address_;
|
||||
|
||||
// Sensors
|
||||
binary_sensor::BinarySensor *motion_binary_sensor_{nullptr};
|
||||
sensor::Sensor *variance_sensor_{nullptr};
|
||||
sensor::Sensor *amplitude_sensor_{nullptr};
|
||||
sensor::Sensor *entropy_sensor_{nullptr};
|
||||
sensor::Sensor *skewness_sensor_{nullptr};
|
||||
|
||||
// Runtime state
|
||||
MotionState current_state_{MotionState::IDLE};
|
||||
std::vector<float> variance_window_;
|
||||
float current_variance_{0.0f};
|
||||
float current_amplitude_{0.0f};
|
||||
uint32_t last_update_time_{0};
|
||||
bool csi_initialized_{false};
|
||||
|
||||
// CSI data buffer (written by WiFi task, read by main loop)
|
||||
CSIDataBuffer csi_buffer_;
|
||||
volatile bool new_csi_data_{false};
|
||||
};
|
||||
|
||||
} // namespace motion_map
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
70
esphome/components/motion_map/sensor.py
Normal file
70
esphome/components/motion_map/sensor.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Sensor platform for Motion Map component - CSI feature sensors."""
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
from . import CONF_MOTION_MAP_ID, MotionMapComponent, motion_map_ns
|
||||
|
||||
DEPENDENCIES = ["motion_map"]
|
||||
|
||||
# Sensor types for CSI features
|
||||
CONF_VARIANCE = "variance"
|
||||
CONF_AMPLITUDE = "amplitude"
|
||||
CONF_ENTROPY = "entropy"
|
||||
CONF_SKEWNESS = "skewness"
|
||||
|
||||
MotionMapSensor = motion_map_ns.class_(
|
||||
"MotionMapSensor", sensor.Sensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_MOTION_MAP_ID): cv.use_id(MotionMapComponent),
|
||||
cv.Optional(CONF_VARIANCE): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_AMPLITUDE): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_ENTROPY): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SKEWNESS): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the motion map sensors."""
|
||||
parent = await cg.get_variable(config[CONF_MOTION_MAP_ID])
|
||||
|
||||
if variance_config := config.get(CONF_VARIANCE):
|
||||
sens = await sensor.new_sensor(variance_config)
|
||||
cg.add(parent.set_variance_sensor(sens))
|
||||
|
||||
if amplitude_config := config.get(CONF_AMPLITUDE):
|
||||
sens = await sensor.new_sensor(amplitude_config)
|
||||
cg.add(parent.set_amplitude_sensor(sens))
|
||||
|
||||
if entropy_config := config.get(CONF_ENTROPY):
|
||||
sens = await sensor.new_sensor(entropy_config)
|
||||
cg.add(parent.set_entropy_sensor(sens))
|
||||
|
||||
if skewness_config := config.get(CONF_SKEWNESS):
|
||||
sens = await sensor.new_sensor(skewness_config)
|
||||
cg.add(parent.set_skewness_sensor(sens))
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number.automation";
|
||||
|
||||
@@ -51,4 +52,5 @@ void ValueRangeTrigger::on_state_(float state) {
|
||||
this->rtc_.save(&in_range);
|
||||
}
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class NumberStateTrigger : public Trigger<float> {
|
||||
public:
|
||||
@@ -90,4 +91,5 @@ template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...>
|
||||
float max_{NAN};
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
@@ -42,4 +43,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include "number_call.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class Number;
|
||||
void log_number(const char *tag, const char *prefix, const char *type, Number *obj);
|
||||
@@ -52,4 +53,5 @@ class Number : public EntityBase {
|
||||
CallbackManager<void(float)> state_callback_;
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
#include "number.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
@@ -124,4 +125,5 @@ void NumberCall::perform() {
|
||||
this->parent_->control(target_value);
|
||||
}
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class Number;
|
||||
|
||||
@@ -43,4 +44,5 @@ class NumberCall {
|
||||
bool cycle_;
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::number {
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_AUTO = 0,
|
||||
@@ -34,4 +35,5 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas
|
||||
NumberMode mode_{NUMBER_MODE_AUTO};
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
||||
@@ -73,17 +73,17 @@ void SFA30Component::update() {
|
||||
}
|
||||
|
||||
if (this->formaldehyde_sensor_ != nullptr) {
|
||||
const float formaldehyde = static_cast<int16_t>(raw_data[0]) / 5.0f;
|
||||
const float formaldehyde = raw_data[0] / 5.0f;
|
||||
this->formaldehyde_sensor_->publish_state(formaldehyde);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
const float humidity = static_cast<int16_t>(raw_data[1]) / 100.0f;
|
||||
const float humidity = raw_data[1] / 100.0f;
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
const float temperature = static_cast<int16_t>(raw_data[2]) / 200.0f;
|
||||
const float temperature = raw_data[2] / 200.0f;
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,18 +94,6 @@ void AsyncWebServer::end() {
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServer::set_lru_purge_enable(bool enable) {
|
||||
if (this->lru_purge_enable_ == enable) {
|
||||
return; // No change needed
|
||||
}
|
||||
this->lru_purge_enable_ = enable;
|
||||
// If server is already running, restart it with new config
|
||||
if (this->server_) {
|
||||
this->end();
|
||||
this->begin();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServer::begin() {
|
||||
if (this->server_) {
|
||||
this->end();
|
||||
@@ -113,8 +101,6 @@ void AsyncWebServer::begin() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = this->port_;
|
||||
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
|
||||
// Enable LRU purging if requested (e.g., by captive portal to handle probe bursts)
|
||||
config.lru_purge_enable = this->lru_purge_enable_;
|
||||
if (httpd_start(&this->server_, &config) == ESP_OK) {
|
||||
const httpd_uri_t handler_get = {
|
||||
.uri = "",
|
||||
@@ -256,7 +242,6 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char
|
||||
void AsyncWebServerRequest::redirect(const std::string &url) {
|
||||
httpd_resp_set_status(*this, "302 Found");
|
||||
httpd_resp_set_hdr(*this, "Location", url.c_str());
|
||||
httpd_resp_set_hdr(*this, "Connection", "close");
|
||||
httpd_resp_send(*this, nullptr, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -199,13 +199,9 @@ class AsyncWebServer {
|
||||
return *handler;
|
||||
}
|
||||
|
||||
void set_lru_purge_enable(bool enable);
|
||||
httpd_handle_t get_server() { return this->server_; }
|
||||
|
||||
protected:
|
||||
uint16_t port_{};
|
||||
httpd_handle_t server_{};
|
||||
bool lru_purge_enable_{false};
|
||||
static esp_err_t request_handler(httpd_req_t *r);
|
||||
static esp_err_t request_post_handler(httpd_req_t *r);
|
||||
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
||||
|
||||
@@ -69,12 +69,6 @@ CONF_MIN_AUTH_MODE = "min_auth_mode"
|
||||
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
||||
MAX_WIFI_NETWORKS = 127
|
||||
|
||||
# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection
|
||||
# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only
|
||||
# get best-effort connection attempts. Longer timeout ensures we exhaust all options
|
||||
# before falling back to AP mode. Aligned with improv wifi_timeout default.
|
||||
DEFAULT_AP_TIMEOUT = "90s"
|
||||
|
||||
wifi_ns = cg.esphome_ns.namespace("wifi")
|
||||
EAPAuth = wifi_ns.struct("EAPAuth")
|
||||
ManualIP = wifi_ns.struct("ManualIP")
|
||||
@@ -183,7 +177,7 @@ CONF_AP_TIMEOUT = "ap_timeout"
|
||||
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT
|
||||
CONF_AP_TIMEOUT, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
@@ -485,14 +479,11 @@ async def to_code(config):
|
||||
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
|
||||
if config[CONF_FAST_CONNECT]:
|
||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||
# passive_scan defaults to false in C++ - only set if true
|
||||
if config[CONF_PASSIVE_SCAN]:
|
||||
cg.add(var.set_passive_scan(True))
|
||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||
if CONF_OUTPUT_POWER in config:
|
||||
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
|
||||
# enable_on_boot defaults to true in C++ - only set if false
|
||||
if not config[CONF_ENABLE_ON_BOOT]:
|
||||
cg.add(var.set_enable_on_boot(False))
|
||||
|
||||
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
|
||||
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
|
||||
@@ -199,12 +199,7 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
|
||||
|
||||
/// Cooldown duration in milliseconds after adapter restart or repeated failures
|
||||
/// Allows WiFi hardware to stabilize before next connection attempt
|
||||
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
|
||||
|
||||
/// Cooldown duration when fallback AP is active and captive portal may be running
|
||||
/// Longer interval gives users time to configure WiFi without constant connection attempts
|
||||
/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown
|
||||
static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
|
||||
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000;
|
||||
|
||||
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
@@ -280,9 +275,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't scan this cycle, treat all networks as potentially hidden
|
||||
// Otherwise, only retry networks that weren't seen in the scan
|
||||
if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) {
|
||||
if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) {
|
||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
|
||||
return static_cast<int8_t>(i);
|
||||
}
|
||||
@@ -424,6 +417,10 @@ void WiFiComponent::start() {
|
||||
void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
// Enter cooldown state to allow WiFi hardware to stabilize after restart
|
||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
@@ -444,16 +441,7 @@ void WiFiComponent::loop() {
|
||||
switch (this->state_) {
|
||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||
// Skip cooldown if new credentials were provided while connecting
|
||||
if (this->skip_cooldown_next_cycle_) {
|
||||
this->skip_cooldown_next_cycle_ = false;
|
||||
this->check_connecting_finished();
|
||||
break;
|
||||
}
|
||||
// Use longer cooldown when captive portal/improv is active to avoid disrupting user config
|
||||
bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
|
||||
uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
|
||||
if (now - this->action_started_ > cooldown_duration) {
|
||||
if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) {
|
||||
// After cooldown we either restarted the adapter because of
|
||||
// a failure, or something tried to connect over and over
|
||||
// so we entered cooldown. In both cases we call
|
||||
@@ -507,8 +495,7 @@ void WiFiComponent::loop() {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
#ifdef USE_IMPROV
|
||||
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() &&
|
||||
!esp32_improv::global_improv_component->should_start()) {
|
||||
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) {
|
||||
if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
|
||||
if (this->wifi_mode_(true, {}))
|
||||
esp32_improv::global_improv_component->start();
|
||||
@@ -618,8 +605,6 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
|
||||
this->init_sta(1);
|
||||
this->add_sta(ap);
|
||||
this->selected_sta_index_ = 0;
|
||||
// When new credentials are set (e.g., from improv), skip cooldown to retry immediately
|
||||
this->skip_cooldown_next_cycle_ = true;
|
||||
}
|
||||
|
||||
WiFiAP WiFiComponent::build_params_for_current_phase_() {
|
||||
@@ -681,17 +666,6 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
||||
sta.set_ssid(ssid);
|
||||
sta.set_password(password);
|
||||
this->set_sta(sta);
|
||||
|
||||
// Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
|
||||
this->connect_soon_();
|
||||
}
|
||||
|
||||
void WiFiComponent::connect_soon_() {
|
||||
// Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) {
|
||||
ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
|
||||
this->retry_connect();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
@@ -989,7 +963,6 @@ void WiFiComponent::check_scanning_finished() {
|
||||
return;
|
||||
}
|
||||
this->scan_done_ = false;
|
||||
this->did_scan_this_cycle_ = true;
|
||||
|
||||
if (this->scan_result_.empty()) {
|
||||
ESP_LOGW(TAG, "No networks found");
|
||||
@@ -1256,16 +1229,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
// After restart, go back to explicit hidden if we went through it initially
|
||||
if (this->went_through_explicit_hidden_phase_()) {
|
||||
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
}
|
||||
// Skip scanning when captive portal/improv is active to avoid disrupting AP
|
||||
// Even passive scans can cause brief AP disconnections on ESP32
|
||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
// After restart, go back to explicit hidden if we went through it initially, otherwise scan
|
||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
|
||||
// Should never reach here
|
||||
@@ -1353,12 +1319,6 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||
this->restart_adapter();
|
||||
}
|
||||
// Clear scan flag - we're starting a new retry cycle
|
||||
this->did_scan_this_cycle_ = false;
|
||||
// Always enter cooldown after restart (or skip-restart) to allow stabilization
|
||||
// Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
||||
return true;
|
||||
|
||||
|
||||
@@ -291,7 +291,6 @@ class WiFiComponent : public Component {
|
||||
void set_passive_scan(bool passive);
|
||||
|
||||
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
/// Setup WiFi interface.
|
||||
@@ -425,8 +424,6 @@ class WiFiComponent : public Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
void connect_soon_();
|
||||
|
||||
void wifi_loop_();
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
bool wifi_sta_pre_setup_();
|
||||
@@ -529,11 +526,9 @@ class WiFiComponent : public Component {
|
||||
bool btm_{false};
|
||||
bool rrm_{false};
|
||||
#endif
|
||||
bool enable_on_boot_{true};
|
||||
bool enable_on_boot_;
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
bool did_scan_this_cycle_{false};
|
||||
bool skip_cooldown_next_cycle_{false};
|
||||
|
||||
// Pointers at the end (naturally aligned)
|
||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||
|
||||
@@ -36,30 +36,7 @@ class Framework(StrEnum):
|
||||
|
||||
|
||||
class ThreadModel(StrEnum):
|
||||
"""Threading model identifiers for ESPHome scheduler.
|
||||
|
||||
ESPHome currently uses three threading models based on platform capabilities:
|
||||
|
||||
SINGLE:
|
||||
- Single-threaded platforms (ESP8266, RP2040)
|
||||
- No RTOS task switching
|
||||
- No concurrent access to scheduler data structures
|
||||
- No atomics or locks required
|
||||
- Minimal overhead
|
||||
|
||||
MULTI_NO_ATOMICS:
|
||||
- Multi-threaded platforms without hardware atomic RMW support (e.g. LibreTiny BK7231N)
|
||||
- Uses FreeRTOS or another RTOS with multiple tasks
|
||||
- CPU lacks exclusive load/store instructions (ARM968E-S has no LDREX/STREX)
|
||||
- std::atomic cannot provide lock-free RMW; libatomic is avoided to save flash (4–8 KB)
|
||||
- Scheduler uses explicit FreeRTOS mutexes for synchronization
|
||||
|
||||
MULTI_ATOMICS:
|
||||
- Multi-threaded platforms with hardware atomic RMW support (ESP32, Cortex-M, Host)
|
||||
- CPU provides native atomic instructions (ESP32 S32C1I, ARM LDREX/STREX)
|
||||
- std::atomic is used for lock-free synchronization
|
||||
- Reduced contention and better performance
|
||||
"""
|
||||
"""Threading model identifiers for ESPHome scheduler."""
|
||||
|
||||
SINGLE = "ESPHOME_THREAD_SINGLE"
|
||||
MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS"
|
||||
|
||||
@@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
|
||||
(has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) ||
|
||||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) {
|
||||
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) ||
|
||||
has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) {
|
||||
// Skip scheduling - the retry was cancelled
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr);
|
||||
@@ -556,8 +556,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Mark items in defer queue as cancelled (they'll be skipped when processed)
|
||||
if (type == SchedulerItem::TIMEOUT) {
|
||||
total_cancelled +=
|
||||
this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry);
|
||||
total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry);
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
@@ -566,20 +565,19 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
// (removing the last element doesn't break heap structure)
|
||||
if (!this->items_.empty()) {
|
||||
auto &last_item = this->items_.back();
|
||||
if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) {
|
||||
if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) {
|
||||
this->recycle_item_(std::move(this->items_.back()));
|
||||
this->items_.pop_back();
|
||||
total_cancelled++;
|
||||
}
|
||||
// For other items in heap, we can only mark for removal (can't remove from middle of heap)
|
||||
size_t heap_cancelled =
|
||||
this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry);
|
||||
size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry);
|
||||
total_cancelled += heap_cancelled;
|
||||
this->to_remove_ += heap_cancelled; // Track removals for heap items
|
||||
}
|
||||
|
||||
// Cancel items in to_add_
|
||||
total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry);
|
||||
total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry);
|
||||
|
||||
return total_cancelled > 0;
|
||||
}
|
||||
|
||||
@@ -243,18 +243,8 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Helper function to check if item matches criteria for cancellation
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
inline bool HOT matches_item_locked_(const std::unique_ptr<SchedulerItem> &item, Component *component,
|
||||
const char *name_cstr, SchedulerItem::Type type, bool match_retry,
|
||||
bool skip_removed = true) const {
|
||||
// THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded
|
||||
// platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries.
|
||||
// PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and
|
||||
// has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper
|
||||
// functions should be safe regardless of caller behavior.
|
||||
// Fixes: https://github.com/esphome/esphome/issues/11940
|
||||
if (!item)
|
||||
return false;
|
||||
inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
|
||||
if (item->component != component || item->type != type || (skip_removed && item->remove) ||
|
||||
(match_retry && !item->is_retry)) {
|
||||
return false;
|
||||
@@ -314,8 +304,8 @@ class Scheduler {
|
||||
// SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_.
|
||||
// This is intentional and safe because:
|
||||
// 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_
|
||||
// and has_cancelled_timeout_in_container_locked_ in scheduler.h)
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_
|
||||
// and has_cancelled_timeout_in_container_ in scheduler.h)
|
||||
// 3. The lock protects concurrent access, but the nullptr remains until cleanup
|
||||
item = std::move(this->defer_queue_[this->defer_queue_front_]);
|
||||
this->defer_queue_front_++;
|
||||
@@ -403,10 +393,10 @@ class Scheduler {
|
||||
|
||||
// Helper to mark matching items in a container as removed
|
||||
// Returns the number of items marked for removal
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
template<typename Container>
|
||||
size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry) {
|
||||
size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry) {
|
||||
size_t count = 0;
|
||||
for (auto &item : container) {
|
||||
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
|
||||
@@ -415,7 +405,7 @@ class Scheduler {
|
||||
// the vector can still contain nullptr items from the processing loop. This check prevents crashes.
|
||||
if (!item)
|
||||
continue;
|
||||
if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) {
|
||||
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
|
||||
// Mark item for removal (platform-specific)
|
||||
this->set_item_removed_(item.get(), true);
|
||||
count++;
|
||||
@@ -425,10 +415,9 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Template helper to check if any item in a container matches our criteria
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
template<typename Container>
|
||||
bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component,
|
||||
const char *name_cstr, bool match_retry) const {
|
||||
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
|
||||
bool match_retry) const {
|
||||
for (const auto &item : container) {
|
||||
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
|
||||
// The defer_queue_ uses index-based processing: items are std::moved out but left in the
|
||||
@@ -437,8 +426,8 @@ class Scheduler {
|
||||
if (!item)
|
||||
continue;
|
||||
if (is_item_removed_(item.get()) &&
|
||||
this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
|
||||
/* skip_removed= */ false)) {
|
||||
this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
|
||||
/* skip_removed= */ false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ def update_storage_json() -> None:
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("Core config or version changed, cleaning build files...")
|
||||
clean_build(clear_pio_cache=False)
|
||||
clean_build()
|
||||
elif storage_should_update_cmake_cache(old, new):
|
||||
_LOGGER.info("Integrations changed, cleaning cmake cache...")
|
||||
clean_cmake_cache()
|
||||
@@ -301,7 +301,7 @@ def clean_cmake_cache():
|
||||
pioenvs_cmake_path.unlink()
|
||||
|
||||
|
||||
def clean_build(clear_pio_cache: bool = True):
|
||||
def clean_build():
|
||||
import shutil
|
||||
|
||||
# Allow skipping cache cleaning for integration tests
|
||||
@@ -322,9 +322,6 @@ def clean_build(clear_pio_cache: bool = True):
|
||||
_LOGGER.info("Deleting %s", dependencies_lock)
|
||||
dependencies_lock.unlink()
|
||||
|
||||
if not clear_pio_cache:
|
||||
return
|
||||
|
||||
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
||||
# This helps when toolchain paths change or get corrupted
|
||||
try:
|
||||
|
||||
@@ -703,9 +703,7 @@ lvgl:
|
||||
on_value:
|
||||
- lvgl.spinbox.update:
|
||||
id: spinbox_id
|
||||
value: !lambda |-
|
||||
static float yyy = 83.0;
|
||||
return yyy + .8;
|
||||
value: !lambda return x;
|
||||
- button:
|
||||
styles: spin_button
|
||||
id: spin_up
|
||||
|
||||
27
tests/components/motion_map/common.yaml
Normal file
27
tests/components/motion_map/common.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
motion_map:
|
||||
id: motion_map_component
|
||||
motion_threshold: 0.6
|
||||
idle_threshold: 0.2
|
||||
window_size: 100
|
||||
sensitivity: 1.5
|
||||
|
||||
binary_sensor:
|
||||
- platform: motion_map
|
||||
motion_map_id: motion_map_component
|
||||
name: "Motion Detected"
|
||||
|
||||
sensor:
|
||||
- platform: motion_map
|
||||
motion_map_id: motion_map_component
|
||||
variance:
|
||||
name: "CSI Variance"
|
||||
amplitude:
|
||||
name: "CSI Amplitude"
|
||||
entropy:
|
||||
name: "CSI Entropy"
|
||||
skewness:
|
||||
name: "CSI Skewness"
|
||||
1
tests/components/motion_map/test.esp32-s3-idf.yaml
Normal file
1
tests/components/motion_map/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user