mirror of
https://github.com/esphome/esphome.git
synced 2025-09-14 17:22:20 +01:00
Merge branch 'dev' into proxy_memory
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.11.10
|
rev: v0.12.0
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
@@ -520,6 +520,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
|||||||
esphome/components/xiaomi_mhoc303/* @drug123
|
esphome/components/xiaomi_mhoc303/* @drug123
|
||||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||||
|
esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
|
||||||
esphome/components/xl9535/* @mreditor97
|
esphome/components/xl9535/* @mreditor97
|
||||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||||
esphome/components/xxtea/* @clydebarrow
|
esphome/components/xxtea/* @clydebarrow
|
||||||
|
@@ -17,7 +17,11 @@ void Anova::setup() {
|
|||||||
this->current_request_ = 0;
|
this->current_request_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Anova::loop() {}
|
void Anova::loop() {
|
||||||
|
// Parent BLEClientNode has a loop() method, but this component uses
|
||||||
|
// polling via update() and BLE callbacks so loop isn't needed
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void Anova::control(const ClimateCall &call) {
|
void Anova::control(const ClimateCall &call) {
|
||||||
if (call.get_mode().has_value()) {
|
if (call.get_mode().has_value()) {
|
||||||
|
@@ -1643,6 +1643,7 @@ enum VoiceAssistantEvent {
|
|||||||
VOICE_ASSISTANT_STT_VAD_END = 12;
|
VOICE_ASSISTANT_STT_VAD_END = 12;
|
||||||
VOICE_ASSISTANT_TTS_STREAM_START = 98;
|
VOICE_ASSISTANT_TTS_STREAM_START = 98;
|
||||||
VOICE_ASSISTANT_TTS_STREAM_END = 99;
|
VOICE_ASSISTANT_TTS_STREAM_END = 99;
|
||||||
|
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VoiceAssistantEventData {
|
message VoiceAssistantEventData {
|
||||||
|
@@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
|
|||||||
return "VOICE_ASSISTANT_TTS_STREAM_START";
|
return "VOICE_ASSISTANT_TTS_STREAM_START";
|
||||||
case enums::VOICE_ASSISTANT_TTS_STREAM_END:
|
case enums::VOICE_ASSISTANT_TTS_STREAM_END:
|
||||||
return "VOICE_ASSISTANT_TTS_STREAM_END";
|
return "VOICE_ASSISTANT_TTS_STREAM_END";
|
||||||
|
case enums::VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||||
|
return "VOICE_ASSISTANT_INTENT_PROGRESS";
|
||||||
default:
|
default:
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
@@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t {
|
|||||||
VOICE_ASSISTANT_STT_VAD_END = 12,
|
VOICE_ASSISTANT_STT_VAD_END = 12,
|
||||||
VOICE_ASSISTANT_TTS_STREAM_START = 98,
|
VOICE_ASSISTANT_TTS_STREAM_START = 98,
|
||||||
VOICE_ASSISTANT_TTS_STREAM_END = 99,
|
VOICE_ASSISTANT_TTS_STREAM_END = 99,
|
||||||
|
VOICE_ASSISTANT_INTENT_PROGRESS = 100,
|
||||||
};
|
};
|
||||||
enum VoiceAssistantTimerEvent : uint32_t {
|
enum VoiceAssistantTimerEvent : uint32_t {
|
||||||
VOICE_ASSISTANT_TIMER_STARTED = 0,
|
VOICE_ASSISTANT_TIMER_STARTED = 0,
|
||||||
|
@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
@coroutine_with_priority(200.0)
|
@coroutine_with_priority(200.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if CORE.is_esp32 or CORE.is_libretiny:
|
if CORE.is_esp32 or CORE.is_libretiny:
|
||||||
# https://github.com/esphome/AsyncTCP/blob/master/library.json
|
# https://github.com/ESP32Async/AsyncTCP
|
||||||
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4")
|
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
|
||||||
elif CORE.is_esp8266:
|
elif CORE.is_esp8266:
|
||||||
# https://github.com/esphome/ESPAsyncTCP
|
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||||
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")
|
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||||
|
@@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
|
|||||||
|
|
||||||
/* Internal */
|
/* Internal */
|
||||||
|
|
||||||
void BedJetHub::loop() {}
|
void BedJetHub::loop() {
|
||||||
|
// Parent BLEClientNode has a loop() method, but this component uses
|
||||||
|
// polling via update() and BLE callbacks so loop isn't needed
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
void BedJetHub::update() { this->dispatch_status_(); }
|
void BedJetHub::update() { this->dispatch_status_(); }
|
||||||
|
|
||||||
void BedJetHub::dump_config() {
|
void BedJetHub::dump_config() {
|
||||||
|
@@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() {
|
|||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BedJetClimate::loop() {}
|
void BedJetClimate::loop() {
|
||||||
|
// This component is controlled via the parent BedJetHub
|
||||||
|
// Empty loop not needed, disable to save CPU cycles
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void BedJetClimate::control(const ClimateCall &call) {
|
void BedJetClimate::control(const ClimateCall &call) {
|
||||||
ESP_LOGD(TAG, "Received BedJetClimate::control");
|
ESP_LOGD(TAG, "Received BedJetClimate::control");
|
||||||
|
@@ -11,7 +11,11 @@ namespace ble_client {
|
|||||||
|
|
||||||
static const char *const TAG = "ble_rssi_sensor";
|
static const char *const TAG = "ble_rssi_sensor";
|
||||||
|
|
||||||
void BLEClientRSSISensor::loop() {}
|
void BLEClientRSSISensor::loop() {
|
||||||
|
// Parent BLEClientNode has a loop() method, but this component uses
|
||||||
|
// polling via update() and BLE GAP callbacks so loop isn't needed
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void BLEClientRSSISensor::dump_config() {
|
void BLEClientRSSISensor::dump_config() {
|
||||||
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
|
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
|
||||||
|
@@ -11,7 +11,11 @@ namespace ble_client {
|
|||||||
|
|
||||||
static const char *const TAG = "ble_sensor";
|
static const char *const TAG = "ble_sensor";
|
||||||
|
|
||||||
void BLESensor::loop() {}
|
void BLESensor::loop() {
|
||||||
|
// Parent BLEClientNode has a loop() method, but this component uses
|
||||||
|
// polling via update() and BLE callbacks so loop isn't needed
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void BLESensor::dump_config() {
|
void BLESensor::dump_config() {
|
||||||
LOG_SENSOR("", "BLE Sensor", this);
|
LOG_SENSOR("", "BLE Sensor", this);
|
||||||
|
@@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor";
|
|||||||
|
|
||||||
static const std::string EMPTY = "";
|
static const std::string EMPTY = "";
|
||||||
|
|
||||||
void BLETextSensor::loop() {}
|
void BLETextSensor::loop() {
|
||||||
|
// Parent BLEClientNode has a loop() method, but this component uses
|
||||||
|
// polling via update() and BLE callbacks so loop isn't needed
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void BLETextSensor::dump_config() {
|
void BLETextSensor::dump_config() {
|
||||||
LOG_TEXT_SENSOR("", "BLE Text Sensor", this);
|
LOG_TEXT_SENSOR("", "BLE Text Sensor", this);
|
||||||
|
@@ -12,8 +12,8 @@ from esphome.const import (
|
|||||||
CONF_OVERSAMPLING,
|
CONF_OVERSAMPLING,
|
||||||
CONF_PRESSURE,
|
CONF_PRESSURE,
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
DEVICE_CLASS_HUMIDITY,
|
|
||||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
ICON_GAS_CYLINDER,
|
ICON_GAS_CYLINDER,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
@@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
|||||||
request->redirect("/?save");
|
request->redirect("/?save");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CaptivePortal::setup() {}
|
void CaptivePortal::setup() {
|
||||||
|
#ifndef USE_ARDUINO
|
||||||
|
// No DNS server needed for non-Arduino frameworks
|
||||||
|
this->disable_loop();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
void CaptivePortal::start() {
|
void CaptivePortal::start() {
|
||||||
this->base_->init();
|
this->base_->init();
|
||||||
if (!this->initialized_) {
|
if (!this->initialized_) {
|
||||||
@@ -50,6 +55,8 @@ void CaptivePortal::start() {
|
|||||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||||
this->dns_server_->start(53, "*", ip);
|
this->dns_server_->start(53, "*", ip);
|
||||||
|
// Re-enable loop() when DNS server is started
|
||||||
|
this->enable_loop();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
|
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
|
||||||
@@ -68,7 +75,11 @@ void CaptivePortal::start() {
|
|||||||
|
|
||||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||||
if (req->url() == "/") {
|
if (req->url() == "/") {
|
||||||
|
#ifndef USE_ESP8266
|
||||||
|
auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
||||||
|
#else
|
||||||
auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
||||||
|
#endif
|
||||||
response->addHeader("Content-Encoding", "gzip");
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
req->send(response);
|
req->send(response);
|
||||||
return;
|
return;
|
||||||
|
@@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
void loop() override {
|
void loop() override {
|
||||||
if (this->dns_server_ != nullptr)
|
if (this->dns_server_ != nullptr) {
|
||||||
this->dns_server_->processNextRequest();
|
this->dns_server_->processNextRequest();
|
||||||
|
} else {
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
@@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canHandle(AsyncWebServerRequest *request) override {
|
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||||
if (!this->active_)
|
if (!this->active_)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
#include "ble.h"
|
#include "ble.h"
|
||||||
|
#include "ble_event_pool.h"
|
||||||
|
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -23,9 +24,6 @@ namespace esp32_ble {
|
|||||||
|
|
||||||
static const char *const TAG = "esp32_ble";
|
static const char *const TAG = "esp32_ble";
|
||||||
|
|
||||||
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
|
|
||||||
|
|
||||||
void ESP32BLE::setup() {
|
void ESP32BLE::setup() {
|
||||||
global_ble = this;
|
global_ble = this;
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
ESP_LOGCONFIG(TAG, "Running setup");
|
||||||
@@ -349,9 +347,8 @@ void ESP32BLE::loop() {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Destructor will clean up external allocations for GATTC/GATTS
|
// Return the event to the pool
|
||||||
ble_event->~BLEEvent();
|
this->ble_event_pool_.release(ble_event);
|
||||||
EVENT_ALLOCATOR.deallocate(ble_event, 1);
|
|
||||||
ble_event = this->ble_events_.pop();
|
ble_event = this->ble_events_.pop();
|
||||||
}
|
}
|
||||||
if (this->advertising_ != nullptr) {
|
if (this->advertising_ != nullptr) {
|
||||||
@@ -359,37 +356,41 @@ void ESP32BLE::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log dropped events periodically
|
// Log dropped events periodically
|
||||||
size_t dropped = this->ble_events_.get_and_reset_dropped_count();
|
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
|
||||||
if (dropped > 0) {
|
if (dropped > 0) {
|
||||||
ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped);
|
ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to load new event data based on type
|
||||||
|
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||||
|
event->load_gap_event(e, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
|
||||||
|
event->load_gattc_event(e, i, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
|
||||||
|
event->load_gatts_event(e, i, p);
|
||||||
|
}
|
||||||
|
|
||||||
template<typename... Args> void enqueue_ble_event(Args... args) {
|
template<typename... Args> void enqueue_ble_event(Args... args) {
|
||||||
// Check if queue is full before allocating
|
// Allocate an event from the pool
|
||||||
if (global_ble->ble_events_.full()) {
|
BLEEvent *event = global_ble->ble_event_pool_.allocate();
|
||||||
// Queue is full, drop the event
|
if (event == nullptr) {
|
||||||
|
// No events available - queue is full or we're out of memory
|
||||||
global_ble->ble_events_.increment_dropped_count();
|
global_ble->ble_events_.increment_dropped_count();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
|
// Load new event data (replaces previous event)
|
||||||
if (new_event == nullptr) {
|
load_ble_event(event, args...);
|
||||||
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
|
|
||||||
global_ble->ble_events_.increment_dropped_count();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
new (new_event) BLEEvent(args...);
|
|
||||||
|
|
||||||
// Push the event - since we're the only producer and we checked full() above,
|
// Push the event to the queue
|
||||||
// this should always succeed unless we have a bug
|
global_ble->ble_events_.push(event);
|
||||||
if (!global_ble->ble_events_.push(new_event)) {
|
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
|
||||||
// This should not happen in SPSC queue with single producer
|
}
|
||||||
ESP_LOGE(TAG, "BLE queue push failed unexpectedly");
|
|
||||||
new_event->~BLEEvent();
|
|
||||||
EVENT_ALLOCATOR.deallocate(new_event, 1);
|
|
||||||
}
|
|
||||||
} // NOLINT(clang-analyzer-unix.Malloc)
|
|
||||||
|
|
||||||
// Explicit template instantiations for the friend function
|
// Explicit template instantiations for the friend function
|
||||||
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
|
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include "ble_event.h"
|
#include "ble_event.h"
|
||||||
|
#include "ble_event_pool.h"
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
@@ -148,6 +149,7 @@ class ESP32BLE : public Component {
|
|||||||
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
|
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
|
||||||
|
|
||||||
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
||||||
|
BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
|
||||||
BLEAdvertising *advertising_{};
|
BLEAdvertising *advertising_{};
|
||||||
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
|
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
|
||||||
uint32_t advertising_cycle_time_{};
|
uint32_t advertising_cycle_time_{};
|
||||||
|
@@ -51,6 +51,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
|
|||||||
// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
|
// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
|
||||||
// the data remains valid even after the BLE callback returns. The original
|
// the data remains valid even after the BLE callback returns. The original
|
||||||
// param pointer from ESP-IDF is only valid during the callback.
|
// param pointer from ESP-IDF is only valid during the callback.
|
||||||
|
//
|
||||||
|
// CRITICAL DESIGN NOTE:
|
||||||
|
// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety.
|
||||||
|
// DO NOT attempt to optimize by removing these allocations or storing pointers
|
||||||
|
// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime
|
||||||
|
// than our event processing, and accessing it after the callback returns would
|
||||||
|
// result in use-after-free bugs and crashes.
|
||||||
class BLEEvent {
|
class BLEEvent {
|
||||||
public:
|
public:
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
@@ -63,125 +70,74 @@ class BLEEvent {
|
|||||||
// Constructor for GAP events - no external allocations needed
|
// Constructor for GAP events - no external allocations needed
|
||||||
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||||
this->type_ = GAP;
|
this->type_ = GAP;
|
||||||
this->event_.gap.gap_event = e;
|
this->init_gap_data_(e, p);
|
||||||
|
|
||||||
if (p == nullptr) {
|
|
||||||
return; // Invalid event, but we can't log in header file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only copy the data we actually use for each GAP event type
|
|
||||||
switch (e) {
|
|
||||||
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
|
||||||
// Copy only the fields we use from scan results
|
|
||||||
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
|
|
||||||
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
|
|
||||||
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
|
|
||||||
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
|
|
||||||
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
|
|
||||||
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
|
|
||||||
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
|
|
||||||
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
|
||||||
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
|
||||||
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
|
||||||
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// We only handle 4 GAP event types, others are dropped
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor for GATTC events - uses heap allocation
|
// Constructor for GATTC events - uses heap allocation
|
||||||
// Creates a copy of the param struct since the original is only valid during the callback
|
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
|
||||||
|
// The param pointer from ESP-IDF is only valid during the callback execution.
|
||||||
|
// Since BLE events are processed asynchronously in the main loop, we must create
|
||||||
|
// our own copy to ensure the data remains valid until the event is processed.
|
||||||
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
|
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
|
||||||
this->type_ = GATTC;
|
this->type_ = GATTC;
|
||||||
this->event_.gattc.gattc_event = e;
|
this->init_gattc_data_(e, i, p);
|
||||||
this->event_.gattc.gattc_if = i;
|
|
||||||
|
|
||||||
if (p == nullptr) {
|
|
||||||
this->event_.gattc.gattc_param = nullptr;
|
|
||||||
this->event_.gattc.data = nullptr;
|
|
||||||
return; // Invalid event, but we can't log in header file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heap-allocate param and data
|
|
||||||
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
|
|
||||||
// while GAP events (99%) are stored inline to minimize memory usage
|
|
||||||
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
|
|
||||||
|
|
||||||
// Copy data for events that need it
|
|
||||||
switch (e) {
|
|
||||||
case ESP_GATTC_NOTIFY_EVT:
|
|
||||||
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
|
|
||||||
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
|
|
||||||
break;
|
|
||||||
case ESP_GATTC_READ_CHAR_EVT:
|
|
||||||
case ESP_GATTC_READ_DESCR_EVT:
|
|
||||||
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
|
|
||||||
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this->event_.gattc.data = nullptr;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor for GATTS events - uses heap allocation
|
// Constructor for GATTS events - uses heap allocation
|
||||||
// Creates a copy of the param struct since the original is only valid during the callback
|
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
|
||||||
|
// The param pointer from ESP-IDF is only valid during the callback execution.
|
||||||
|
// Since BLE events are processed asynchronously in the main loop, we must create
|
||||||
|
// our own copy to ensure the data remains valid until the event is processed.
|
||||||
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
|
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
|
||||||
this->type_ = GATTS;
|
this->type_ = GATTS;
|
||||||
this->event_.gatts.gatts_event = e;
|
this->init_gatts_data_(e, i, p);
|
||||||
this->event_.gatts.gatts_if = i;
|
|
||||||
|
|
||||||
if (p == nullptr) {
|
|
||||||
this->event_.gatts.gatts_param = nullptr;
|
|
||||||
this->event_.gatts.data = nullptr;
|
|
||||||
return; // Invalid event, but we can't log in header file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heap-allocate param and data
|
|
||||||
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
|
|
||||||
// while GAP events (99%) are stored inline to minimize memory usage
|
|
||||||
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
|
|
||||||
|
|
||||||
// Copy data for events that need it
|
|
||||||
switch (e) {
|
|
||||||
case ESP_GATTS_WRITE_EVT:
|
|
||||||
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
|
|
||||||
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this->event_.gatts.data = nullptr;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destructor to clean up heap allocations
|
// Destructor to clean up heap allocations
|
||||||
~BLEEvent() {
|
~BLEEvent() { this->cleanup_heap_data(); }
|
||||||
switch (this->type_) {
|
|
||||||
case GATTC:
|
// Default constructor for pre-allocation in pool
|
||||||
|
BLEEvent() : type_(GAP) {}
|
||||||
|
|
||||||
|
// Clean up any heap-allocated data
|
||||||
|
void cleanup_heap_data() {
|
||||||
|
if (this->type_ == GAP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->type_ == GATTC) {
|
||||||
delete this->event_.gattc.gattc_param;
|
delete this->event_.gattc.gattc_param;
|
||||||
delete this->event_.gattc.data;
|
delete this->event_.gattc.data;
|
||||||
break;
|
this->event_.gattc.gattc_param = nullptr;
|
||||||
case GATTS:
|
this->event_.gattc.data = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->type_ == GATTS) {
|
||||||
delete this->event_.gatts.gatts_param;
|
delete this->event_.gatts.gatts_param;
|
||||||
delete this->event_.gatts.data;
|
delete this->event_.gatts.data;
|
||||||
break;
|
this->event_.gatts.gatts_param = nullptr;
|
||||||
default:
|
this->event_.gatts.data = nullptr;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load new event data for reuse (replaces previous event data)
|
||||||
|
void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||||
|
this->cleanup_heap_data();
|
||||||
|
this->type_ = GAP;
|
||||||
|
this->init_gap_data_(e, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
|
||||||
|
this->cleanup_heap_data();
|
||||||
|
this->type_ = GATTC;
|
||||||
|
this->init_gattc_data_(e, i, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
|
||||||
|
this->cleanup_heap_data();
|
||||||
|
this->type_ = GATTS;
|
||||||
|
this->init_gatts_data_(e, i, p);
|
||||||
|
}
|
||||||
|
|
||||||
// Disable copy to prevent double-delete
|
// Disable copy to prevent double-delete
|
||||||
BLEEvent(const BLEEvent &) = delete;
|
BLEEvent(const BLEEvent &) = delete;
|
||||||
BLEEvent &operator=(const BLEEvent &) = delete;
|
BLEEvent &operator=(const BLEEvent &) = delete;
|
||||||
@@ -224,6 +180,119 @@ class BLEEvent {
|
|||||||
esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
|
esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
|
||||||
const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
|
const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
|
||||||
esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
|
esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Initialize GAP event data
|
||||||
|
void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||||
|
this->event_.gap.gap_event = e;
|
||||||
|
|
||||||
|
if (p == nullptr) {
|
||||||
|
return; // Invalid event, but we can't log in header file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy data based on event type
|
||||||
|
switch (e) {
|
||||||
|
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
||||||
|
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
|
||||||
|
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
|
||||||
|
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
|
||||||
|
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
|
||||||
|
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
|
||||||
|
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
|
||||||
|
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
|
||||||
|
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
||||||
|
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
||||||
|
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
||||||
|
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We only handle 4 GAP event types, others are dropped
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize GATTC event data
|
||||||
|
void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
|
||||||
|
this->event_.gattc.gattc_event = e;
|
||||||
|
this->event_.gattc.gattc_if = i;
|
||||||
|
|
||||||
|
if (p == nullptr) {
|
||||||
|
this->event_.gattc.gattc_param = nullptr;
|
||||||
|
this->event_.gattc.data = nullptr;
|
||||||
|
return; // Invalid event, but we can't log in header file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate param and data
|
||||||
|
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
|
||||||
|
// while GAP events (99%) are stored inline to minimize memory usage
|
||||||
|
// IMPORTANT: This heap allocation provides clear ownership semantics:
|
||||||
|
// - The BLEEvent owns the allocated memory for its lifetime
|
||||||
|
// - The data remains valid from the BLE callback context until processed in the main loop
|
||||||
|
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
|
||||||
|
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
|
||||||
|
|
||||||
|
// Copy data for events that need it
|
||||||
|
// The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
|
||||||
|
// We must copy this data to ensure it remains valid when the event is processed later.
|
||||||
|
switch (e) {
|
||||||
|
case ESP_GATTC_NOTIFY_EVT:
|
||||||
|
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
|
||||||
|
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
|
||||||
|
break;
|
||||||
|
case ESP_GATTC_READ_CHAR_EVT:
|
||||||
|
case ESP_GATTC_READ_DESCR_EVT:
|
||||||
|
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
|
||||||
|
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->event_.gattc.data = nullptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize GATTS event data
|
||||||
|
void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
|
||||||
|
this->event_.gatts.gatts_event = e;
|
||||||
|
this->event_.gatts.gatts_if = i;
|
||||||
|
|
||||||
|
if (p == nullptr) {
|
||||||
|
this->event_.gatts.gatts_param = nullptr;
|
||||||
|
this->event_.gatts.data = nullptr;
|
||||||
|
return; // Invalid event, but we can't log in header file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate param and data
|
||||||
|
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
|
||||||
|
// while GAP events (99%) are stored inline to minimize memory usage
|
||||||
|
// IMPORTANT: This heap allocation provides clear ownership semantics:
|
||||||
|
// - The BLEEvent owns the allocated memory for its lifetime
|
||||||
|
// - The data remains valid from the BLE callback context until processed in the main loop
|
||||||
|
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
|
||||||
|
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
|
||||||
|
|
||||||
|
// Copy data for events that need it
|
||||||
|
// The param struct contains pointers (e.g., write.value) that point to temporary buffers.
|
||||||
|
// We must copy this data to ensure it remains valid when the event is processed later.
|
||||||
|
switch (e) {
|
||||||
|
case ESP_GATTS_WRITE_EVT:
|
||||||
|
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
|
||||||
|
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->event_.gatts.data = nullptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
|
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
|
||||||
|
72
esphome/components/esp32_ble/ble_event_pool.h
Normal file
72
esphome/components/esp32_ble/ble_event_pool.h
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstddef>
|
||||||
|
#include "ble_event.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace esp32_ble {
|
||||||
|
|
||||||
|
// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation
|
||||||
|
// Events are allocated on first use and reused thereafter, growing to peak usage
|
||||||
|
template<uint8_t SIZE> class BLEEventPool {
|
||||||
|
public:
|
||||||
|
BLEEventPool() : total_created_(0) {}
|
||||||
|
|
||||||
|
~BLEEventPool() {
|
||||||
|
// Clean up any remaining events in the free list
|
||||||
|
BLEEvent *event;
|
||||||
|
while ((event = this->free_list_.pop()) != nullptr) {
|
||||||
|
delete event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate an event from the pool
|
||||||
|
// Returns nullptr if pool is full
|
||||||
|
BLEEvent *allocate() {
|
||||||
|
// Try to get from free list first
|
||||||
|
BLEEvent *event = this->free_list_.pop();
|
||||||
|
if (event != nullptr)
|
||||||
|
return event;
|
||||||
|
|
||||||
|
// Need to create a new event
|
||||||
|
if (this->total_created_ >= SIZE) {
|
||||||
|
// Pool is at capacity
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use internal RAM for better performance
|
||||||
|
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
|
||||||
|
event = allocator.allocate(1);
|
||||||
|
|
||||||
|
if (event == nullptr) {
|
||||||
|
// Memory allocation failed
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placement new to construct the object
|
||||||
|
new (event) BLEEvent();
|
||||||
|
this->total_created_++;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an event to the pool for reuse
|
||||||
|
void release(BLEEvent *event) {
|
||||||
|
if (event != nullptr) {
|
||||||
|
this->free_list_.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LockFreeQueue<BLEEvent, SIZE> free_list_; // Free events ready for reuse
|
||||||
|
uint8_t total_created_; // Total events created (high water mark)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esp32_ble
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
@@ -18,7 +18,7 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace esp32_ble {
|
namespace esp32_ble {
|
||||||
|
|
||||||
template<class T, size_t SIZE> class LockFreeQueue {
|
template<class T, uint8_t SIZE> class LockFreeQueue {
|
||||||
public:
|
public:
|
||||||
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
|
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ template<class T, size_t SIZE> class LockFreeQueue {
|
|||||||
if (element == nullptr)
|
if (element == nullptr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
size_t current_tail = tail_.load(std::memory_order_relaxed);
|
uint8_t current_tail = tail_.load(std::memory_order_relaxed);
|
||||||
size_t next_tail = (current_tail + 1) % SIZE;
|
uint8_t next_tail = (current_tail + 1) % SIZE;
|
||||||
|
|
||||||
if (next_tail == head_.load(std::memory_order_acquire)) {
|
if (next_tail == head_.load(std::memory_order_acquire)) {
|
||||||
// Buffer full
|
// Buffer full
|
||||||
@@ -41,7 +41,7 @@ template<class T, size_t SIZE> class LockFreeQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
T *pop() {
|
T *pop() {
|
||||||
size_t current_head = head_.load(std::memory_order_relaxed);
|
uint8_t current_head = head_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
if (current_head == tail_.load(std::memory_order_acquire)) {
|
if (current_head == tail_.load(std::memory_order_acquire)) {
|
||||||
return nullptr; // Empty
|
return nullptr; // Empty
|
||||||
@@ -53,27 +53,30 @@ template<class T, size_t SIZE> class LockFreeQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t size() const {
|
size_t size() const {
|
||||||
size_t tail = tail_.load(std::memory_order_acquire);
|
uint8_t tail = tail_.load(std::memory_order_acquire);
|
||||||
size_t head = head_.load(std::memory_order_acquire);
|
uint8_t head = head_.load(std::memory_order_acquire);
|
||||||
return (tail - head + SIZE) % SIZE;
|
return (tail - head + SIZE) % SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
|
uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
|
||||||
|
|
||||||
void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
|
void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
|
||||||
|
|
||||||
bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
|
bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
bool full() const {
|
bool full() const {
|
||||||
size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
|
uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
|
||||||
return next_tail == head_.load(std::memory_order_acquire);
|
return next_tail == head_.load(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
T *buffer_[SIZE];
|
T *buffer_[SIZE];
|
||||||
std::atomic<size_t> head_;
|
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
|
||||||
std::atomic<size_t> tail_;
|
std::atomic<uint16_t> dropped_count_; // 65535 max - more than enough for drop tracking
|
||||||
std::atomic<size_t> dropped_count_;
|
// Atomic: written by consumer (pop), read by producer (push) to check if full
|
||||||
|
std::atomic<uint8_t> head_;
|
||||||
|
// Atomic: written by producer (push), read by consumer (pop) to check if empty
|
||||||
|
std::atomic<uint8_t> tail_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp32_ble
|
} // namespace esp32_ble
|
||||||
|
@@ -22,6 +22,16 @@ void BLEClientBase::setup() {
|
|||||||
this->connection_index_ = connection_index++;
|
this->connection_index_ = connection_index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BLEClientBase::set_state(espbt::ClientState st) {
|
||||||
|
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
|
||||||
|
ESPBTClient::set_state(st);
|
||||||
|
|
||||||
|
if (st == espbt::ClientState::READY_TO_CONNECT) {
|
||||||
|
// Enable loop when we need to connect
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BLEClientBase::loop() {
|
void BLEClientBase::loop() {
|
||||||
if (!esp32_ble::global_ble->is_active()) {
|
if (!esp32_ble::global_ble->is_active()) {
|
||||||
this->set_state(espbt::ClientState::INIT);
|
this->set_state(espbt::ClientState::INIT);
|
||||||
@@ -37,9 +47,14 @@ void BLEClientBase::loop() {
|
|||||||
}
|
}
|
||||||
// READY_TO_CONNECT means we have discovered the device
|
// READY_TO_CONNECT means we have discovered the device
|
||||||
// and the scanner has been stopped by the tracker.
|
// and the scanner has been stopped by the tracker.
|
||||||
if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
|
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
|
||||||
this->connect();
|
this->connect();
|
||||||
}
|
}
|
||||||
|
// If its idle, we can disable the loop as set_state
|
||||||
|
// will enable it again when we need to connect.
|
||||||
|
else if (this->state_ == espbt::ClientState::IDLE) {
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||||
|
@@ -93,6 +93,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
|
|
||||||
bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
|
bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
|
||||||
|
|
||||||
|
void set_state(espbt::ClientState st) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Memory optimized layout for 32-bit systems
|
// Memory optimized layout for 32-bit systems
|
||||||
// Group 1: 8-byte types
|
// Group 1: 8-byte types
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
#ifdef USE_ESP32
|
|
||||||
#include "esp32_hall.h"
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include "esphome/core/hal.h"
|
|
||||||
#include <driver/adc.h>
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace esp32_hall {
|
|
||||||
|
|
||||||
static const char *const TAG = "esp32_hall";
|
|
||||||
|
|
||||||
void ESP32HallSensor::update() {
|
|
||||||
adc1_config_width(ADC_WIDTH_BIT_12);
|
|
||||||
int value_int = hall_sensor_read();
|
|
||||||
float value = (value_int / 4095.0f) * 10000.0f;
|
|
||||||
ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value);
|
|
||||||
this->publish_state(value);
|
|
||||||
}
|
|
||||||
std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; }
|
|
||||||
void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); }
|
|
||||||
|
|
||||||
} // namespace esp32_hall
|
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif
|
|
@@ -1,23 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/components/sensor/sensor.h"
|
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace esp32_hall {
|
|
||||||
|
|
||||||
class ESP32HallSensor : public sensor::Sensor, public PollingComponent {
|
|
||||||
public:
|
|
||||||
void dump_config() override;
|
|
||||||
|
|
||||||
void update() override;
|
|
||||||
|
|
||||||
std::string unique_id() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace esp32_hall
|
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif
|
|
@@ -1,24 +0,0 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
from esphome.components import sensor
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA
|
|
||||||
|
|
||||||
DEPENDENCIES = ["esp32"]
|
|
||||||
|
|
||||||
esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall")
|
|
||||||
ESP32HallSensor = esp32_hall_ns.class_(
|
|
||||||
"ESP32HallSensor", sensor.Sensor, cg.PollingComponent
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
|
||||||
ESP32HallSensor,
|
|
||||||
unit_of_measurement=UNIT_MICROTESLA,
|
|
||||||
icon=ICON_MAGNET,
|
|
||||||
accuracy_decimals=1,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
).extend(cv.polling_component_schema("60s"))
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
|
||||||
var = await sensor.new_sensor(config)
|
|
||||||
await cg.register_component(var, config)
|
|
@@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() {
|
|||||||
case improv::STATE_PROVISIONED: {
|
case improv::STATE_PROVISIONED: {
|
||||||
this->incoming_data_.clear();
|
this->incoming_data_.clear();
|
||||||
this->set_status_indicator_state_(false);
|
this->set_status_indicator_state_(false);
|
||||||
|
// Provisioning complete, no further loop execution needed
|
||||||
|
this->disable_loop();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() {
|
|||||||
|
|
||||||
ESP_LOGD(TAG, "Setting Improv to start");
|
ESP_LOGD(TAG, "Setting Improv to start");
|
||||||
this->should_start_ = true;
|
this->should_start_ = true;
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32ImprovComponent::stop() {
|
void ESP32ImprovComponent::stop() {
|
||||||
|
@@ -125,6 +125,6 @@ async def to_code(config):
|
|||||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||||
|
|
||||||
cg.add_library("tonia/HeatpumpIR", "1.0.32")
|
cg.add_library("tonia/HeatpumpIR", "1.0.35")
|
||||||
if CORE.is_libretiny:
|
if CORE.is_libretiny:
|
||||||
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
|
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
|
||||||
|
@@ -116,5 +116,5 @@ async def to_code(config):
|
|||||||
|
|
||||||
cg.add_library("WiFiClientSecure", None)
|
cg.add_library("WiFiClientSecure", None)
|
||||||
cg.add_library("HTTPClient", None)
|
cg.add_library("HTTPClient", None)
|
||||||
cg.add_library("esphome/ESP32-audioI2S", "2.2.0")
|
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
|
||||||
cg.add_build_flag("-DAUDIO_NO_SD_FS")
|
cg.add_build_flag("-DAUDIO_NO_SD_FS")
|
||||||
|
@@ -17,7 +17,7 @@ namespace light {
|
|||||||
|
|
||||||
class LightOutput;
|
class LightOutput;
|
||||||
|
|
||||||
enum LightRestoreMode {
|
enum LightRestoreMode : uint8_t {
|
||||||
LIGHT_RESTORE_DEFAULT_OFF,
|
LIGHT_RESTORE_DEFAULT_OFF,
|
||||||
LIGHT_RESTORE_DEFAULT_ON,
|
LIGHT_RESTORE_DEFAULT_ON,
|
||||||
LIGHT_ALWAYS_OFF,
|
LIGHT_ALWAYS_OFF,
|
||||||
@@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component {
|
|||||||
|
|
||||||
/// Store the output to allow effects to have more access.
|
/// Store the output to allow effects to have more access.
|
||||||
LightOutput *output_;
|
LightOutput *output_;
|
||||||
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
|
||||||
uint32_t active_effect_index_{};
|
|
||||||
/// The currently active transformer for this light (transition/flash).
|
/// The currently active transformer for this light (transition/flash).
|
||||||
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
||||||
/// Whether the light value should be written in the next cycle.
|
/// List of effects for this light.
|
||||||
bool next_write_{true};
|
std::vector<LightEffect *> effects_;
|
||||||
|
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
||||||
|
uint32_t active_effect_index_{};
|
||||||
|
/// Default transition length for all transitions in ms.
|
||||||
|
uint32_t default_transition_length_{};
|
||||||
|
/// Transition length to use for flash transitions.
|
||||||
|
uint32_t flash_transition_length_{};
|
||||||
|
/// Gamma correction factor for the light.
|
||||||
|
float gamma_correct_{};
|
||||||
|
|
||||||
/// Object used to store the persisted values of the light.
|
/// Object used to store the persisted values of the light.
|
||||||
ESPPreferenceObject rtc_;
|
ESPPreferenceObject rtc_;
|
||||||
@@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component {
|
|||||||
*/
|
*/
|
||||||
CallbackManager<void()> target_state_reached_callback_{};
|
CallbackManager<void()> target_state_reached_callback_{};
|
||||||
|
|
||||||
/// Default transition length for all transitions in ms.
|
|
||||||
uint32_t default_transition_length_{};
|
|
||||||
/// Transition length to use for flash transitions.
|
|
||||||
uint32_t flash_transition_length_{};
|
|
||||||
/// Gamma correction factor for the light.
|
|
||||||
float gamma_correct_{};
|
|
||||||
/// Restore mode of the light.
|
|
||||||
LightRestoreMode restore_mode_;
|
|
||||||
/// Initial state of the light.
|
/// Initial state of the light.
|
||||||
optional<LightStateRTCState> initial_state_{};
|
optional<LightStateRTCState> initial_state_{};
|
||||||
/// List of effects for this light.
|
|
||||||
std::vector<LightEffect *> effects_;
|
|
||||||
|
|
||||||
|
/// Restore mode of the light.
|
||||||
|
LightRestoreMode restore_mode_;
|
||||||
|
/// Whether the light value should be written in the next cycle.
|
||||||
|
bool next_write_{true};
|
||||||
// for effects, true if a transformer (transition) is active.
|
// for effects, true if a transformer (transition) is active.
|
||||||
bool is_transformer_active_ = false;
|
bool is_transformer_active_ = false;
|
||||||
};
|
};
|
||||||
|
@@ -178,18 +178,21 @@ void OnlineImage::update() {
|
|||||||
if (this->format_ == ImageFormat::BMP) {
|
if (this->format_ == ImageFormat::BMP) {
|
||||||
ESP_LOGD(TAG, "Allocating BMP decoder");
|
ESP_LOGD(TAG, "Allocating BMP decoder");
|
||||||
this->decoder_ = make_unique<BmpDecoder>(this);
|
this->decoder_ = make_unique<BmpDecoder>(this);
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||||
if (this->format_ == ImageFormat::JPEG) {
|
if (this->format_ == ImageFormat::JPEG) {
|
||||||
ESP_LOGD(TAG, "Allocating JPEG decoder");
|
ESP_LOGD(TAG, "Allocating JPEG decoder");
|
||||||
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
|
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
if (this->format_ == ImageFormat::PNG) {
|
if (this->format_ == ImageFormat::PNG) {
|
||||||
ESP_LOGD(TAG, "Allocating PNG decoder");
|
ESP_LOGD(TAG, "Allocating PNG decoder");
|
||||||
this->decoder_ = make_unique<PngDecoder>(this);
|
this->decoder_ = make_unique<PngDecoder>(this);
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
|
|
||||||
@@ -212,6 +215,7 @@ void OnlineImage::update() {
|
|||||||
void OnlineImage::loop() {
|
void OnlineImage::loop() {
|
||||||
if (!this->decoder_) {
|
if (!this->decoder_) {
|
||||||
// Not decoding at the moment => nothing to do.
|
// Not decoding at the moment => nothing to do.
|
||||||
|
this->disable_loop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||||
|
@@ -12,6 +12,8 @@ class IntervalSyncer : public Component {
|
|||||||
void setup() override {
|
void setup() override {
|
||||||
if (this->write_interval_ != 0) {
|
if (this->write_interval_ != 0) {
|
||||||
set_interval(this->write_interval_, []() { global_preferences->sync(); });
|
set_interval(this->write_interval_, []() { global_preferences->sync(); });
|
||||||
|
// When using interval-based syncing, we don't need the loop
|
||||||
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void loop() override {
|
void loop() override {
|
||||||
|
@@ -40,7 +40,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
|
|||||||
*/
|
*/
|
||||||
void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); }
|
void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); }
|
||||||
|
|
||||||
bool canHandle(AsyncWebServerRequest *request) override {
|
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||||
if (request->method() == HTTP_GET) {
|
if (request->method() == HTTP_GET) {
|
||||||
if (request->url() == "/metrics")
|
if (request->url() == "/metrics")
|
||||||
return true;
|
return true;
|
||||||
|
@@ -142,8 +142,10 @@ void Rtttl::stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Rtttl::loop() {
|
void Rtttl::loop() {
|
||||||
if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED)
|
if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) {
|
||||||
|
this->disable_loop();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_SPEAKER
|
#ifdef USE_SPEAKER
|
||||||
if (this->speaker_ != nullptr) {
|
if (this->speaker_ != nullptr) {
|
||||||
@@ -391,6 +393,11 @@ void Rtttl::set_state_(State state) {
|
|||||||
this->state_ = state;
|
this->state_ = state;
|
||||||
ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
|
ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
|
||||||
LOG_STR_ARG(state_to_string(state)));
|
LOG_STR_ARG(state_to_string(state)));
|
||||||
|
|
||||||
|
// Clear loop_done when transitioning from STOPPED to any other state
|
||||||
|
if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) {
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace rtttl
|
} // namespace rtttl
|
||||||
|
@@ -42,6 +42,8 @@ void SafeModeComponent::loop() {
|
|||||||
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
|
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
|
||||||
this->clean_rtc();
|
this->clean_rtc();
|
||||||
this->boot_successful_ = true;
|
this->boot_successful_ = true;
|
||||||
|
// Disable loop since we no longer need to check
|
||||||
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -67,6 +67,12 @@ void SNTPComponent::loop() {
|
|||||||
time.minute, time.second);
|
time.minute, time.second);
|
||||||
this->time_sync_callback_.call();
|
this->time_sync_callback_.call();
|
||||||
this->has_time_ = true;
|
this->has_time_ = true;
|
||||||
|
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
// On ESP-IDF, time sync is permanent and update() doesn't force resync
|
||||||
|
// Time is now synchronized, no need to check anymore
|
||||||
|
this->disable_loop();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sntp
|
} // namespace sntp
|
||||||
|
@@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02;
|
|||||||
const int RESTORE_MODE_INVERTED_MASK = 0x04;
|
const int RESTORE_MODE_INVERTED_MASK = 0x04;
|
||||||
const int RESTORE_MODE_DISABLED_MASK = 0x08;
|
const int RESTORE_MODE_DISABLED_MASK = 0x08;
|
||||||
|
|
||||||
enum SwitchRestoreMode {
|
enum SwitchRestoreMode : uint8_t {
|
||||||
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
|
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
|
||||||
SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK,
|
SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK,
|
||||||
SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK,
|
SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK,
|
||||||
@@ -49,12 +49,12 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
|
|||||||
*/
|
*/
|
||||||
void publish_state(bool state);
|
void publish_state(bool state);
|
||||||
|
|
||||||
/// The current reported state of the binary sensor.
|
|
||||||
bool state;
|
|
||||||
|
|
||||||
/// Indicates whether or not state is to be retrieved from flash and how
|
/// Indicates whether or not state is to be retrieved from flash and how
|
||||||
SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF};
|
SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF};
|
||||||
|
|
||||||
|
/// The current reported state of the binary sensor.
|
||||||
|
bool state;
|
||||||
|
|
||||||
/** Turn this switch on. This is called by the front-end.
|
/** Turn this switch on. This is called by the front-end.
|
||||||
*
|
*
|
||||||
* For implementing switches, please override write_state.
|
* For implementing switches, please override write_state.
|
||||||
@@ -123,10 +123,16 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
|
|||||||
*/
|
*/
|
||||||
virtual void write_state(bool state) = 0;
|
virtual void write_state(bool state) = 0;
|
||||||
|
|
||||||
CallbackManager<void(bool)> state_callback_{};
|
// Pointer first (4 bytes)
|
||||||
bool inverted_{false};
|
|
||||||
Deduplicator<bool> publish_dedup_;
|
|
||||||
ESPPreferenceObject rtc_;
|
ESPPreferenceObject rtc_;
|
||||||
|
|
||||||
|
// CallbackManager (12 bytes on 32-bit - contains vector)
|
||||||
|
CallbackManager<void(bool)> state_callback_{};
|
||||||
|
|
||||||
|
// Small types grouped together
|
||||||
|
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
|
||||||
|
bool inverted_{false}; // 1 byte
|
||||||
|
// Total: 3 bytes, 1 byte padding
|
||||||
};
|
};
|
||||||
|
|
||||||
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))
|
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))
|
||||||
|
@@ -24,8 +24,10 @@ void TLC5971::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TLC5971::loop() {
|
void TLC5971::loop() {
|
||||||
if (!this->update_)
|
if (!this->update_) {
|
||||||
|
this->disable_loop();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t command;
|
uint32_t command;
|
||||||
|
|
||||||
@@ -93,6 +95,7 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) {
|
|||||||
return;
|
return;
|
||||||
if (this->pwm_amounts_[channel] != value) {
|
if (this->pwm_amounts_[channel] != value) {
|
||||||
this->update_ = true;
|
this->update_ = true;
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
this->pwm_amounts_[channel] = value;
|
this->pwm_amounts_[channel] = value;
|
||||||
}
|
}
|
||||||
|
@@ -91,7 +91,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
|
|||||||
while (!deferred_queue_.empty()) {
|
while (!deferred_queue_.empty()) {
|
||||||
DeferredEvent &de = deferred_queue_.front();
|
DeferredEvent &de = deferred_queue_.front();
|
||||||
std::string message = de.message_generator_(web_server_, de.source_);
|
std::string message = de.message_generator_(web_server_, de.source_);
|
||||||
if (this->try_send(message.c_str(), "state")) {
|
if (this->send(message.c_str(), "state") != DISCARDED) {
|
||||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||||
deferred_queue_.erase(deferred_queue_.begin());
|
deferred_queue_.erase(deferred_queue_.begin());
|
||||||
} else {
|
} else {
|
||||||
@@ -131,7 +131,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
|
|||||||
deq_push_back_with_dedup_(source, message_generator);
|
deq_push_back_with_dedup_(source, message_generator);
|
||||||
} else {
|
} else {
|
||||||
std::string message = message_generator(web_server_, source);
|
std::string message = message_generator(web_server_, source);
|
||||||
if (!this->try_send(message.c_str(), "state")) {
|
if (this->send(message.c_str(), "state") == DISCARDED) {
|
||||||
deq_push_back_with_dedup_(source, message_generator);
|
deq_push_back_with_dedup_(source, message_generator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,8 +171,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer
|
|||||||
ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); });
|
ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); });
|
||||||
});
|
});
|
||||||
|
|
||||||
es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) {
|
es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) {
|
||||||
ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); });
|
ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); });
|
||||||
});
|
});
|
||||||
|
|
||||||
es->handleRequest(request);
|
es->handleRequest(request);
|
||||||
@@ -291,14 +291,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f
|
|||||||
|
|
||||||
#ifdef USE_WEBSERVER_LOCAL
|
#ifdef USE_WEBSERVER_LOCAL
|
||||||
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||||
|
#ifndef USE_ESP8266
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
||||||
|
#else
|
||||||
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
||||||
|
#endif
|
||||||
response->addHeader("Content-Encoding", "gzip");
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
#elif USE_WEBSERVER_VERSION >= 2
|
#elif USE_WEBSERVER_VERSION >= 2
|
||||||
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||||
|
#ifndef USE_ESP8266
|
||||||
|
AsyncWebServerResponse *response =
|
||||||
|
request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
|
||||||
|
#else
|
||||||
AsyncWebServerResponse *response =
|
AsyncWebServerResponse *response =
|
||||||
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
|
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
|
||||||
|
#endif
|
||||||
// No gzip header here because the HTML file is so small
|
// No gzip header here because the HTML file is so small
|
||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
@@ -317,8 +326,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||||
void WebServer::handle_css_request(AsyncWebServerRequest *request) {
|
void WebServer::handle_css_request(AsyncWebServerRequest *request) {
|
||||||
|
#ifndef USE_ESP8266
|
||||||
|
AsyncWebServerResponse *response =
|
||||||
|
request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
|
||||||
|
#else
|
||||||
AsyncWebServerResponse *response =
|
AsyncWebServerResponse *response =
|
||||||
request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
|
request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
|
||||||
|
#endif
|
||||||
response->addHeader("Content-Encoding", "gzip");
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
@@ -326,8 +340,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||||
void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
||||||
|
#ifndef USE_ESP8266
|
||||||
|
AsyncWebServerResponse *response =
|
||||||
|
request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
|
||||||
|
#else
|
||||||
AsyncWebServerResponse *response =
|
AsyncWebServerResponse *response =
|
||||||
request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
|
request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
|
||||||
|
#endif
|
||||||
response->addHeader("Content-Encoding", "gzip");
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
@@ -1837,7 +1856,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||||
if (request->url() == "/")
|
if (request->url() == "/")
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -1859,12 +1878,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
||||||
if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) {
|
if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) {
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
// Header needs to be added to interesting header list for it to not be
|
|
||||||
// nuked by the time we handle the request later.
|
|
||||||
// Only required in Arduino framework.
|
|
||||||
request->addInterestingHeader(HEADER_CORS_REQ_PNA);
|
|
||||||
#endif
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -2145,7 +2158,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebServer::isRequestHandlerTrivial() { return false; }
|
bool WebServer::isRequestHandlerTrivial() const { return false; }
|
||||||
|
|
||||||
void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) {
|
void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) {
|
||||||
this->sorting_entitys_[entity] = SortingComponents{weight, group};
|
this->sorting_entitys_[entity] = SortingComponents{weight, group};
|
||||||
|
@@ -99,7 +99,7 @@ class DeferredUpdateEventSource : public AsyncEventSource {
|
|||||||
protected:
|
protected:
|
||||||
// surface a couple methods from the base class
|
// surface a couple methods from the base class
|
||||||
using AsyncEventSource::handleRequest;
|
using AsyncEventSource::handleRequest;
|
||||||
using AsyncEventSource::try_send;
|
using AsyncEventSource::send;
|
||||||
|
|
||||||
ListEntitiesIterator entities_iterator_;
|
ListEntitiesIterator entities_iterator_;
|
||||||
// vector is used very specifically for its zero memory overhead even though items are popped from the front (memory
|
// vector is used very specifically for its zero memory overhead even though items are popped from the front (memory
|
||||||
@@ -468,11 +468,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Override the web handler's canHandle method.
|
/// Override the web handler's canHandle method.
|
||||||
bool canHandle(AsyncWebServerRequest *request) override;
|
bool canHandle(AsyncWebServerRequest *request) const override;
|
||||||
/// Override the web handler's handleRequest method.
|
/// Override the web handler's handleRequest method.
|
||||||
void handleRequest(AsyncWebServerRequest *request) override;
|
void handleRequest(AsyncWebServerRequest *request) override;
|
||||||
/// This web handle is not trivial.
|
/// This web handle is not trivial.
|
||||||
bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming)
|
bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming)
|
||||||
|
|
||||||
void add_entity_config(EntityBase *entity, float weight, uint64_t group);
|
void add_entity_config(EntityBase *entity, float weight, uint64_t group);
|
||||||
void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight);
|
void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight);
|
||||||
|
@@ -36,5 +36,7 @@ async def to_code(config):
|
|||||||
cg.add_library("WiFi", None)
|
cg.add_library("WiFi", None)
|
||||||
cg.add_library("FS", None)
|
cg.add_library("FS", None)
|
||||||
cg.add_library("Update", None)
|
cg.add_library("Update", None)
|
||||||
# https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json
|
if CORE.is_esp8266:
|
||||||
cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0")
|
cg.add_library("ESP8266WiFi", None)
|
||||||
|
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||||
|
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
|
||||||
|
@@ -23,7 +23,7 @@ class MiddlewareHandler : public AsyncWebHandler {
|
|||||||
public:
|
public:
|
||||||
MiddlewareHandler(AsyncWebHandler *next) : next_(next) {}
|
MiddlewareHandler(AsyncWebHandler *next) : next_(next) {}
|
||||||
|
|
||||||
bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); }
|
bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); }
|
||||||
void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); }
|
void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); }
|
||||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||||
bool final) override {
|
bool final) override {
|
||||||
@@ -32,7 +32,7 @@ class MiddlewareHandler : public AsyncWebHandler {
|
|||||||
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override {
|
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override {
|
||||||
next_->handleBody(request, data, len, index, total);
|
next_->handleBody(request, data, len, index, total);
|
||||||
}
|
}
|
||||||
bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); }
|
bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AsyncWebHandler *next_;
|
AsyncWebHandler *next_;
|
||||||
@@ -131,12 +131,12 @@ class OTARequestHandler : public AsyncWebHandler {
|
|||||||
void handleRequest(AsyncWebServerRequest *request) override;
|
void handleRequest(AsyncWebServerRequest *request) override;
|
||||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||||
bool final) override;
|
bool final) override;
|
||||||
bool canHandle(AsyncWebServerRequest *request) override {
|
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||||
return request->url() == "/update" && request->method() == HTTP_POST;
|
return request->url() == "/update" && request->method() == HTTP_POST;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
bool isRequestHandlerTrivial() override { return false; }
|
bool isRequestHandlerTrivial() const override { return false; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t last_ota_progress_{0};
|
uint32_t last_ota_progress_{0};
|
||||||
|
@@ -135,7 +135,7 @@ class AsyncWebServerRequest {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data,
|
AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data,
|
||||||
const size_t data_size) {
|
const size_t data_size) {
|
||||||
auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory)
|
auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
this->init_response_(res, code, content_type);
|
this->init_response_(res, code, content_type);
|
||||||
@@ -211,7 +211,7 @@ class AsyncWebHandler {
|
|||||||
public:
|
public:
|
||||||
virtual ~AsyncWebHandler() {}
|
virtual ~AsyncWebHandler() {}
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
virtual bool canHandle(AsyncWebServerRequest *request) { return false; }
|
virtual bool canHandle(AsyncWebServerRequest *request) const { return false; }
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
virtual void handleRequest(AsyncWebServerRequest *request) {}
|
virtual void handleRequest(AsyncWebServerRequest *request) {}
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
@@ -220,7 +220,7 @@ class AsyncWebHandler {
|
|||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {}
|
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {}
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
virtual bool isRequestHandlerTrivial() { return true; }
|
virtual bool isRequestHandlerTrivial() const { return true; }
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
@@ -290,7 +290,7 @@ class AsyncEventSource : public AsyncWebHandler {
|
|||||||
~AsyncEventSource() override;
|
~AsyncEventSource() override;
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
bool canHandle(AsyncWebServerRequest *request) override {
|
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||||
return request->method() == HTTP_GET && request->url() == this->url_;
|
return request->method() == HTTP_GET && request->url() == this->url_;
|
||||||
}
|
}
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
|
@@ -91,6 +91,13 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_
|
|||||||
// MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 %
|
// MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 %
|
||||||
else if ((value_type == 0x4C02) && (value_length == 1)) {
|
else if ((value_type == 0x4C02) && (value_length == 1)) {
|
||||||
result.humidity = data[0];
|
result.humidity = data[0];
|
||||||
|
}
|
||||||
|
// XMWSDJ04MMC humidity, 4 bytes, float, 0.1 °C
|
||||||
|
else if ((value_type == 0x4C08) && (value_length == 4)) {
|
||||||
|
const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]);
|
||||||
|
float humidity;
|
||||||
|
std::memcpy(&humidity, &int_number, sizeof(humidity));
|
||||||
|
result.humidity = humidity;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -219,6 +226,11 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
|
|||||||
} else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted
|
} else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted
|
||||||
result.type = XiaomiParseResult::TYPE_LYWSD03MMC;
|
result.type = XiaomiParseResult::TYPE_LYWSD03MMC;
|
||||||
result.name = "LYWSD03MMC";
|
result.name = "LYWSD03MMC";
|
||||||
|
} else if (device_uuid == 0x1203) { // small square body, e-ink display, encrypted
|
||||||
|
result.type = XiaomiParseResult::TYPE_XMWSDJ04MMC;
|
||||||
|
result.name = "XMWSDJ04MMC";
|
||||||
|
if (raw.size() == 19)
|
||||||
|
result.raw_offset -= 6;
|
||||||
} else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight
|
} else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight
|
||||||
result.type = XiaomiParseResult::TYPE_MJYD02YLA;
|
result.type = XiaomiParseResult::TYPE_MJYD02YLA;
|
||||||
result.name = "MJYD02YLA";
|
result.name = "MJYD02YLA";
|
||||||
|
@@ -20,6 +20,7 @@ struct XiaomiParseResult {
|
|||||||
TYPE_LYWSD02MMC,
|
TYPE_LYWSD02MMC,
|
||||||
TYPE_CGG1,
|
TYPE_CGG1,
|
||||||
TYPE_LYWSD03MMC,
|
TYPE_LYWSD03MMC,
|
||||||
|
TYPE_XMWSDJ04MMC,
|
||||||
TYPE_CGD1,
|
TYPE_CGD1,
|
||||||
TYPE_CGDK2,
|
TYPE_CGDK2,
|
||||||
TYPE_JQJCY01YM,
|
TYPE_JQJCY01YM,
|
||||||
|
77
esphome/components/xiaomi_xmwsdj04mmc/sensor.py
Normal file
77
esphome/components/xiaomi_xmwsdj04mmc/sensor.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import esp32_ble_tracker, sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BATTERY_LEVEL,
|
||||||
|
CONF_BINDKEY,
|
||||||
|
CONF_HUMIDITY,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_MAC_ADDRESS,
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_BATTERY,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTO_LOAD = ["xiaomi_ble"]
|
||||||
|
CODEOWNERS = ["@medusalix"]
|
||||||
|
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||||
|
|
||||||
|
xiaomi_xmwsdj04mmc_ns = cg.esphome_ns.namespace("xiaomi_xmwsdj04mmc")
|
||||||
|
XiaomiXMWSDJ04MMC = xiaomi_xmwsdj04mmc_ns.class_(
|
||||||
|
"XiaomiXMWSDJ04MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(XiaomiXMWSDJ04MMC),
|
||||||
|
cv.Required(CONF_BINDKEY): cv.bind_key,
|
||||||
|
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||||
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_HUMIDITY,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_BATTERY,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await esp32_ble_tracker.register_ble_device(var, config)
|
||||||
|
|
||||||
|
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||||
|
cg.add(var.set_bindkey(config[CONF_BINDKEY]))
|
||||||
|
|
||||||
|
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||||
|
sens = await sensor.new_sensor(temperature_config)
|
||||||
|
cg.add(var.set_temperature(sens))
|
||||||
|
if humidity_config := config.get(CONF_HUMIDITY):
|
||||||
|
sens = await sensor.new_sensor(humidity_config)
|
||||||
|
cg.add(var.set_humidity(sens))
|
||||||
|
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
|
||||||
|
sens = await sensor.new_sensor(battery_level_config)
|
||||||
|
cg.add(var.set_battery_level(sens))
|
77
esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
Normal file
77
esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#include "xiaomi_xmwsdj04mmc.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi_xmwsdj04mmc {
|
||||||
|
|
||||||
|
static const char *const TAG = "xiaomi_xmwsdj04mmc";
|
||||||
|
|
||||||
|
void XiaomiXMWSDJ04MMC::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC");
|
||||||
|
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
|
||||||
|
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||||
|
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||||
|
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||||
|
if (device.address_uint64() != this->address_) {
|
||||||
|
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
for (auto &service_data : device.get_service_datas()) {
|
||||||
|
auto res = xiaomi_ble::parse_xiaomi_header(service_data);
|
||||||
|
if (!res.has_value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res->is_duplicate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res->has_encryption &&
|
||||||
|
(!(xiaomi_ble::decrypt_xiaomi_payload(const_cast<std::vector<uint8_t> &>(service_data.data), this->bindkey_,
|
||||||
|
this->address_)))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res->humidity.has_value() && this->humidity_ != nullptr) {
|
||||||
|
// see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254
|
||||||
|
*res->humidity = trunc(*res->humidity);
|
||||||
|
}
|
||||||
|
if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res->temperature.has_value() && this->temperature_ != nullptr)
|
||||||
|
this->temperature_->publish_state(*res->temperature);
|
||||||
|
if (res->humidity.has_value() && this->humidity_ != nullptr)
|
||||||
|
this->humidity_->publish_state(*res->humidity);
|
||||||
|
if (res->battery_level.has_value() && this->battery_level_ != nullptr)
|
||||||
|
this->battery_level_->publish_state(*res->battery_level);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) {
|
||||||
|
memset(this->bindkey_, 0, 16);
|
||||||
|
if (bindkey.size() != 32) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char temp[3] = {0};
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
|
||||||
|
this->bindkey_[i] = std::strtoul(temp, nullptr, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xiaomi_xmwsdj04mmc
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
37
esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
Normal file
37
esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||||
|
#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi_xmwsdj04mmc {
|
||||||
|
|
||||||
|
class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
|
||||||
|
public:
|
||||||
|
void set_address(uint64_t address) { this->address_ = address; }
|
||||||
|
void set_bindkey(const std::string &bindkey);
|
||||||
|
|
||||||
|
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||||
|
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||||
|
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint64_t address_;
|
||||||
|
uint8_t bindkey_[16];
|
||||||
|
sensor::Sensor *temperature_{nullptr};
|
||||||
|
sensor::Sensor *humidity_{nullptr};
|
||||||
|
sensor::Sensor *battery_level_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace xiaomi_xmwsdj04mmc
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
@@ -97,7 +97,13 @@ void Application::loop() {
|
|||||||
// Feed WDT with time
|
// Feed WDT with time
|
||||||
this->feed_wdt(last_op_end_time);
|
this->feed_wdt(last_op_end_time);
|
||||||
|
|
||||||
for (Component *component : this->looping_components_) {
|
// Mark that we're in the loop for safe reentrant modifications
|
||||||
|
this->in_loop_ = true;
|
||||||
|
|
||||||
|
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
|
||||||
|
this->current_loop_index_++) {
|
||||||
|
Component *component = this->looping_components_[this->current_loop_index_];
|
||||||
|
|
||||||
// Update the cached time before each component runs
|
// Update the cached time before each component runs
|
||||||
this->loop_component_start_time_ = last_op_end_time;
|
this->loop_component_start_time_ = last_op_end_time;
|
||||||
|
|
||||||
@@ -112,6 +118,8 @@ void Application::loop() {
|
|||||||
this->app_state_ |= new_app_state;
|
this->app_state_ |= new_app_state;
|
||||||
this->feed_wdt(last_op_end_time);
|
this->feed_wdt(last_op_end_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->in_loop_ = false;
|
||||||
this->app_state_ = new_app_state;
|
this->app_state_ = new_app_state;
|
||||||
|
|
||||||
// Use the last component's end time instead of calling millis() again
|
// Use the last component's end time instead of calling millis() again
|
||||||
@@ -235,10 +243,67 @@ void Application::teardown_components(uint32_t timeout_ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::calculate_looping_components_() {
|
void Application::calculate_looping_components_() {
|
||||||
|
// First add all active components
|
||||||
for (auto *obj : this->components_) {
|
for (auto *obj : this->components_) {
|
||||||
if (obj->has_overridden_loop())
|
if (obj->has_overridden_loop() &&
|
||||||
|
(obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) {
|
||||||
this->looping_components_.push_back(obj);
|
this->looping_components_.push_back(obj);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->looping_components_active_end_ = this->looping_components_.size();
|
||||||
|
|
||||||
|
// Then add all inactive (LOOP_DONE) components
|
||||||
|
// This handles components that called disable_loop() during setup, before this method runs
|
||||||
|
for (auto *obj : this->components_) {
|
||||||
|
if (obj->has_overridden_loop() &&
|
||||||
|
(obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
|
||||||
|
this->looping_components_.push_back(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::disable_component_loop_(Component *component) {
|
||||||
|
// This method must be reentrant - components can disable themselves during their own loop() call
|
||||||
|
// Linear search to find component in active section
|
||||||
|
// Most configs have 10-30 looping components (30 is on the high end)
|
||||||
|
// O(n) is acceptable here as we optimize for memory, not complexity
|
||||||
|
for (uint16_t i = 0; i < this->looping_components_active_end_; i++) {
|
||||||
|
if (this->looping_components_[i] == component) {
|
||||||
|
// Move last active component to this position
|
||||||
|
this->looping_components_active_end_--;
|
||||||
|
if (i != this->looping_components_active_end_) {
|
||||||
|
std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]);
|
||||||
|
|
||||||
|
// If we're currently iterating and just swapped the current position
|
||||||
|
if (this->in_loop_ && i == this->current_loop_index_) {
|
||||||
|
// Decrement so we'll process the swapped component next
|
||||||
|
this->current_loop_index_--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::enable_component_loop_(Component *component) {
|
||||||
|
// This method must be reentrant - components can re-enable themselves during their own loop() call
|
||||||
|
// Single pass through all components to find and move if needed
|
||||||
|
// With typical 10-30 components, O(n) is faster than maintaining a map
|
||||||
|
const uint16_t size = this->looping_components_.size();
|
||||||
|
for (uint16_t i = 0; i < size; i++) {
|
||||||
|
if (this->looping_components_[i] == component) {
|
||||||
|
if (i < this->looping_components_active_end_) {
|
||||||
|
return; // Already active
|
||||||
|
}
|
||||||
|
// Found in inactive section - move to active
|
||||||
|
if (i != this->looping_components_active_end_) {
|
||||||
|
std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]);
|
||||||
|
}
|
||||||
|
this->looping_components_active_end_++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
@@ -572,13 +572,41 @@ class Application {
|
|||||||
|
|
||||||
void calculate_looping_components_();
|
void calculate_looping_components_();
|
||||||
|
|
||||||
|
// These methods are called by Component::disable_loop() and Component::enable_loop()
|
||||||
|
// Components should not call these directly - use this->disable_loop() or this->enable_loop()
|
||||||
|
// to ensure component state is properly updated along with the loop partition
|
||||||
|
void disable_component_loop_(Component *component);
|
||||||
|
void enable_component_loop_(Component *component);
|
||||||
|
|
||||||
void feed_wdt_arch_();
|
void feed_wdt_arch_();
|
||||||
|
|
||||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||||
void yield_with_select_(uint32_t delay_ms);
|
void yield_with_select_(uint32_t delay_ms);
|
||||||
|
|
||||||
std::vector<Component *> components_{};
|
std::vector<Component *> components_{};
|
||||||
|
|
||||||
|
// Partitioned vector design for looping components
|
||||||
|
// =================================================
|
||||||
|
// Components are partitioned into [active | inactive] sections:
|
||||||
|
//
|
||||||
|
// looping_components_: [A, B, C, D | E, F]
|
||||||
|
// ^
|
||||||
|
// looping_components_active_end_ (4)
|
||||||
|
//
|
||||||
|
// - Components A,B,C,D are active and will be called in loop()
|
||||||
|
// - Components E,F are inactive (disabled/failed) and won't be called
|
||||||
|
// - No flag checking needed during iteration - just loop 0 to active_end_
|
||||||
|
// - When a component is disabled, it's swapped with the last active component
|
||||||
|
// and active_end_ is decremented
|
||||||
|
// - When a component is enabled, it's swapped with the first inactive component
|
||||||
|
// and active_end_ is incremented
|
||||||
|
// - This eliminates branch mispredictions from flag checking in the hot loop
|
||||||
std::vector<Component *> looping_components_{};
|
std::vector<Component *> looping_components_{};
|
||||||
|
uint16_t looping_components_active_end_{0};
|
||||||
|
|
||||||
|
// For safe reentrant modifications during iteration
|
||||||
|
uint16_t current_loop_index_{0};
|
||||||
|
bool in_loop_{false};
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
std::vector<binary_sensor::BinarySensor *> binary_sensors_{};
|
std::vector<binary_sensor::BinarySensor *> binary_sensors_{};
|
||||||
|
@@ -30,17 +30,18 @@ const float LATE = -100.0f;
|
|||||||
|
|
||||||
} // namespace setup_priority
|
} // namespace setup_priority
|
||||||
|
|
||||||
// Component state uses bits 0-1 (4 states)
|
// Component state uses bits 0-2 (8 states, 5 used)
|
||||||
const uint8_t COMPONENT_STATE_MASK = 0x03;
|
const uint8_t COMPONENT_STATE_MASK = 0x07;
|
||||||
const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
|
const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
|
||||||
const uint8_t COMPONENT_STATE_SETUP = 0x01;
|
const uint8_t COMPONENT_STATE_SETUP = 0x01;
|
||||||
const uint8_t COMPONENT_STATE_LOOP = 0x02;
|
const uint8_t COMPONENT_STATE_LOOP = 0x02;
|
||||||
const uint8_t COMPONENT_STATE_FAILED = 0x03;
|
const uint8_t COMPONENT_STATE_FAILED = 0x03;
|
||||||
// Status LED uses bits 2-3
|
const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04;
|
||||||
const uint8_t STATUS_LED_MASK = 0x0C;
|
// Status LED uses bits 3-4
|
||||||
|
const uint8_t STATUS_LED_MASK = 0x18;
|
||||||
const uint8_t STATUS_LED_OK = 0x00;
|
const uint8_t STATUS_LED_OK = 0x00;
|
||||||
const uint8_t STATUS_LED_WARNING = 0x04; // Bit 2
|
const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3
|
||||||
const uint8_t STATUS_LED_ERROR = 0x08; // Bit 3
|
const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4
|
||||||
|
|
||||||
const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning
|
const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning
|
||||||
const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again
|
const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again
|
||||||
@@ -113,6 +114,9 @@ void Component::call() {
|
|||||||
case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone)
|
case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone)
|
||||||
// State failed: Do nothing
|
// State failed: Do nothing
|
||||||
break;
|
break;
|
||||||
|
case COMPONENT_STATE_LOOP_DONE: // NOLINT(bugprone-branch-clone)
|
||||||
|
// State loop done: Do nothing, component has finished its work
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -136,14 +140,30 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void Component::mark_failed() {
|
void Component::mark_failed() {
|
||||||
ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source());
|
ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source());
|
||||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
this->component_state_ |= COMPONENT_STATE_FAILED;
|
this->component_state_ |= COMPONENT_STATE_FAILED;
|
||||||
this->status_set_error();
|
this->status_set_error();
|
||||||
|
// Also remove from loop since failed components shouldn't loop
|
||||||
|
App.disable_component_loop_(this);
|
||||||
|
}
|
||||||
|
void Component::disable_loop() {
|
||||||
|
ESP_LOGD(TAG, "%s loop disabled", this->get_component_source());
|
||||||
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
|
this->component_state_ |= COMPONENT_STATE_LOOP_DONE;
|
||||||
|
App.disable_component_loop_(this);
|
||||||
|
}
|
||||||
|
void Component::enable_loop() {
|
||||||
|
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
|
||||||
|
ESP_LOGD(TAG, "%s loop enabled", this->get_component_source());
|
||||||
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
|
this->component_state_ |= COMPONENT_STATE_LOOP;
|
||||||
|
App.enable_component_loop_(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void Component::reset_to_construction_state() {
|
void Component::reset_to_construction_state() {
|
||||||
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
|
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
|
||||||
ESP_LOGI(TAG, "Component %s is being reset to construction state.", this->get_component_source());
|
ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source());
|
||||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
||||||
// Clear error status when resetting
|
// Clear error status when resetting
|
||||||
@@ -276,8 +296,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
|||||||
}
|
}
|
||||||
if (should_warn) {
|
if (should_warn) {
|
||||||
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
||||||
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time);
|
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
|
||||||
ESP_LOGW(TAG, "Components should block for at most 30 ms.");
|
ESP_LOGW(TAG, "Components should block for at most 30 ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
return curr_time;
|
return curr_time;
|
||||||
|
@@ -58,6 +58,7 @@ extern const uint8_t COMPONENT_STATE_CONSTRUCTION;
|
|||||||
extern const uint8_t COMPONENT_STATE_SETUP;
|
extern const uint8_t COMPONENT_STATE_SETUP;
|
||||||
extern const uint8_t COMPONENT_STATE_LOOP;
|
extern const uint8_t COMPONENT_STATE_LOOP;
|
||||||
extern const uint8_t COMPONENT_STATE_FAILED;
|
extern const uint8_t COMPONENT_STATE_FAILED;
|
||||||
|
extern const uint8_t COMPONENT_STATE_LOOP_DONE;
|
||||||
extern const uint8_t STATUS_LED_MASK;
|
extern const uint8_t STATUS_LED_MASK;
|
||||||
extern const uint8_t STATUS_LED_OK;
|
extern const uint8_t STATUS_LED_OK;
|
||||||
extern const uint8_t STATUS_LED_WARNING;
|
extern const uint8_t STATUS_LED_WARNING;
|
||||||
@@ -150,6 +151,26 @@ class Component {
|
|||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Disable this component's loop. The loop() method will no longer be called.
|
||||||
|
*
|
||||||
|
* This is useful for components that only need to run for a certain period of time
|
||||||
|
* or when inactive, saving CPU cycles.
|
||||||
|
*
|
||||||
|
* @note Components should call this->disable_loop() on themselves, not on other components.
|
||||||
|
* This ensures the component's state is properly updated along with the loop partition.
|
||||||
|
*/
|
||||||
|
void disable_loop();
|
||||||
|
|
||||||
|
/** Enable this component's loop. The loop() method will be called normally.
|
||||||
|
*
|
||||||
|
* This is useful for components that transition between active and inactive states
|
||||||
|
* and need to re-enable their loop() method when becoming active again.
|
||||||
|
*
|
||||||
|
* @note Components should call this->enable_loop() on themselves, not on other components.
|
||||||
|
* This ensures the component's state is properly updated along with the loop partition.
|
||||||
|
*/
|
||||||
|
void enable_loop();
|
||||||
|
|
||||||
bool is_failed() const;
|
bool is_failed() const;
|
||||||
|
|
||||||
bool is_ready() const;
|
bool is_ready() const;
|
||||||
|
@@ -29,7 +29,9 @@ Component = esphome_ns.class_("Component")
|
|||||||
ComponentPtr = Component.operator("ptr")
|
ComponentPtr = Component.operator("ptr")
|
||||||
PollingComponent = esphome_ns.class_("PollingComponent", Component)
|
PollingComponent = esphome_ns.class_("PollingComponent", Component)
|
||||||
Application = esphome_ns.class_("Application")
|
Application = esphome_ns.class_("Application")
|
||||||
optional = esphome_ns.class_("optional")
|
# Create optional with explicit namespace to avoid ambiguity with std::optional
|
||||||
|
# The generated code will use esphome::optional instead of just optional
|
||||||
|
optional = global_ns.namespace("esphome").class_("optional")
|
||||||
arduino_json_ns = global_ns.namespace("ArduinoJson")
|
arduino_json_ns = global_ns.namespace("ArduinoJson")
|
||||||
JsonObject = arduino_json_ns.class_("JsonObject")
|
JsonObject = arduino_json_ns.class_("JsonObject")
|
||||||
JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")
|
JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")
|
||||||
|
@@ -65,14 +65,14 @@ lib_deps =
|
|||||||
SPI ; spi (Arduino built-in)
|
SPI ; spi (Arduino built-in)
|
||||||
Wire ; i2c (Arduino built-int)
|
Wire ; i2c (Arduino built-int)
|
||||||
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
|
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
|
||||||
esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base
|
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
|
||||||
fastled/FastLED@3.9.16 ; fastled_base
|
fastled/FastLED@3.9.16 ; fastled_base
|
||||||
mikalhart/TinyGPSPlus@1.1.0 ; gps
|
mikalhart/TinyGPSPlus@1.1.0 ; gps
|
||||||
freekode/TM1651@1.0.1 ; tm1651
|
freekode/TM1651@1.0.1 ; tm1651
|
||||||
glmnet/Dsmr@0.7 ; dsmr
|
glmnet/Dsmr@0.7 ; dsmr
|
||||||
rweather/Crypto@0.4.0 ; dsmr
|
rweather/Crypto@0.4.0 ; dsmr
|
||||||
dudanov/MideaUART@1.1.9 ; midea
|
dudanov/MideaUART@1.1.9 ; midea
|
||||||
tonia/HeatpumpIR@1.0.32 ; heatpumpir
|
tonia/HeatpumpIR@1.0.35 ; heatpumpir
|
||||||
build_flags =
|
build_flags =
|
||||||
${common.build_flags}
|
${common.build_flags}
|
||||||
-DUSE_ARDUINO
|
-DUSE_ARDUINO
|
||||||
@@ -100,7 +100,7 @@ lib_deps =
|
|||||||
${common:arduino.lib_deps}
|
${common:arduino.lib_deps}
|
||||||
ESP8266WiFi ; wifi (Arduino built-in)
|
ESP8266WiFi ; wifi (Arduino built-in)
|
||||||
Update ; ota (Arduino built-in)
|
Update ; ota (Arduino built-in)
|
||||||
esphome/ESPAsyncTCP-esphome@2.0.0 ; async_tcp
|
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
|
||||||
ESP8266HTTPClient ; http_request (Arduino built-in)
|
ESP8266HTTPClient ; http_request (Arduino built-in)
|
||||||
ESP8266mDNS ; mdns (Arduino built-in)
|
ESP8266mDNS ; mdns (Arduino built-in)
|
||||||
DNSServer ; captive_portal (Arduino built-in)
|
DNSServer ; captive_portal (Arduino built-in)
|
||||||
@@ -130,12 +130,12 @@ lib_deps =
|
|||||||
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
|
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
|
||||||
Update ; ota,web_server_base (Arduino built-in)
|
Update ; ota,web_server_base (Arduino built-in)
|
||||||
${common:arduino.lib_deps}
|
${common:arduino.lib_deps}
|
||||||
esphome/AsyncTCP-esphome@2.1.4 ; async_tcp
|
ESP32Async/AsyncTCP@3.4.4 ; async_tcp
|
||||||
WiFiClientSecure ; http_request,nextion (Arduino built-in)
|
WiFiClientSecure ; http_request,nextion (Arduino built-in)
|
||||||
HTTPClient ; http_request,nextion (Arduino built-in)
|
HTTPClient ; http_request,nextion (Arduino built-in)
|
||||||
ESPmDNS ; mdns (Arduino built-in)
|
ESPmDNS ; mdns (Arduino built-in)
|
||||||
DNSServer ; captive_portal (Arduino built-in)
|
DNSServer ; captive_portal (Arduino built-in)
|
||||||
esphome/ESP32-audioI2S@2.2.0 ; i2s_audio
|
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
|
||||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||||
esphome/esp-audio-libs@1.1.4 ; audio
|
esphome/esp-audio-libs@1.1.4 ; audio
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ build_unflags =
|
|||||||
; This are common settings for the LibreTiny (all variants) using Arduino.
|
; This are common settings for the LibreTiny (all variants) using Arduino.
|
||||||
[common:libretiny-arduino]
|
[common:libretiny-arduino]
|
||||||
extends = common:arduino
|
extends = common:arduino
|
||||||
platform = libretiny
|
platform = libretiny@1.9.1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
lib_deps =
|
lib_deps =
|
||||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||||
|
@@ -120,10 +120,12 @@ select = [
|
|||||||
|
|
||||||
ignore = [
|
ignore = [
|
||||||
"E501", # line too long
|
"E501", # line too long
|
||||||
|
"PLC0415", # `import` should be at the top-level of a file
|
||||||
"PLR0911", # Too many return statements ({returns} > {max_returns})
|
"PLR0911", # Too many return statements ({returns} > {max_returns})
|
||||||
"PLR0912", # Too many branches ({branches} > {max_branches})
|
"PLR0912", # Too many branches ({branches} > {max_branches})
|
||||||
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
|
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
|
||||||
"PLR0915", # Too many statements ({statements} > {max_statements})
|
"PLR0915", # Too many statements ({statements} > {max_statements})
|
||||||
|
"PLW1641", # Object does not implement `__hash__` method
|
||||||
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
|
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
|
||||||
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
|
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
|
||||||
"UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681
|
"UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
pylint==3.3.7
|
pylint==3.3.7
|
||||||
flake8==7.2.0 # also change in .pre-commit-config.yaml when updating
|
flake8==7.2.0 # also change in .pre-commit-config.yaml when updating
|
||||||
ruff==0.11.13 # also change in .pre-commit-config.yaml when updating
|
ruff==0.12.0 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
|
@@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
|
|||||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert "it_4->set_template([=]() -> optional<std::string> {" in main_cpp
|
assert "it_4->set_template([=]() -> esphome::optional<std::string> {" in main_cpp
|
||||||
assert 'return std::string{"Hello"};' in main_cpp
|
assert 'return std::string{"Hello"};' in main_cpp
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
sensor:
|
|
||||||
- platform: esp32_hall
|
|
||||||
name: ESP32 Hall Sensor
|
|
12
tests/components/xiaomi_xmwsdj04mmc/common.yaml
Normal file
12
tests/components/xiaomi_xmwsdj04mmc/common.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
esp32_ble_tracker:
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: xiaomi_xmwsdj04mmc
|
||||||
|
mac_address: 84:B4:DB:5D:A3:8F
|
||||||
|
bindkey: d8ca2ed09bb5541dc8f045ca360b00ea
|
||||||
|
temperature:
|
||||||
|
name: Xiaomi XMWSDJ04MMC Temperature
|
||||||
|
humidity:
|
||||||
|
name: Xiaomi XMWSDJ04MMC Humidity
|
||||||
|
battery_level:
|
||||||
|
name: Xiaomi XMWSDJ04MMC Battery Level
|
1
tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml
Normal file
1
tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml
Normal file
1
tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncGenerator, Generator
|
from collections.abc import AsyncGenerator, Callable, Generator
|
||||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -46,6 +46,7 @@ if platform.system() == "Windows":
|
|||||||
"Integration tests are not supported on Windows", allow_module_level=True
|
"Integration tests are not supported on Windows", allow_module_level=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
import pty # not available on Windows
|
import pty # not available on Windows
|
||||||
|
|
||||||
|
|
||||||
@@ -362,7 +363,10 @@ async def api_client_connected(
|
|||||||
|
|
||||||
|
|
||||||
async def _read_stream_lines(
|
async def _read_stream_lines(
|
||||||
stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO
|
stream: asyncio.StreamReader,
|
||||||
|
lines: list[str],
|
||||||
|
output_stream: TextIO,
|
||||||
|
line_callback: Callable[[str], None] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Read lines from a stream, append to list, and echo to output stream."""
|
"""Read lines from a stream, append to list, and echo to output stream."""
|
||||||
log_parser = LogParser()
|
log_parser = LogParser()
|
||||||
@@ -380,6 +384,9 @@ async def _read_stream_lines(
|
|||||||
file=output_stream,
|
file=output_stream,
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
# Call the callback if provided
|
||||||
|
if line_callback:
|
||||||
|
line_callback(decoded_line.rstrip())
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -388,6 +395,7 @@ async def run_binary_and_wait_for_port(
|
|||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
timeout: float = PORT_WAIT_TIMEOUT,
|
timeout: float = PORT_WAIT_TIMEOUT,
|
||||||
|
line_callback: Callable[[str], None] | None = None,
|
||||||
) -> AsyncGenerator[None]:
|
) -> AsyncGenerator[None]:
|
||||||
"""Run a binary, wait for it to open a port, and clean up on exit."""
|
"""Run a binary, wait for it to open a port, and clean up on exit."""
|
||||||
# Create a pseudo-terminal to make the binary think it's running interactively
|
# Create a pseudo-terminal to make the binary think it's running interactively
|
||||||
@@ -435,7 +443,9 @@ async def run_binary_and_wait_for_port(
|
|||||||
# Read from output stream
|
# Read from output stream
|
||||||
output_tasks = [
|
output_tasks = [
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
_read_stream_lines(output_reader, stdout_lines, sys.stdout)
|
_read_stream_lines(
|
||||||
|
output_reader, stdout_lines, sys.stdout, line_callback
|
||||||
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -515,6 +525,7 @@ async def run_compiled_context(
|
|||||||
compile_esphome: CompileFunction,
|
compile_esphome: CompileFunction,
|
||||||
port: int,
|
port: int,
|
||||||
port_socket: socket.socket | None = None,
|
port_socket: socket.socket | None = None,
|
||||||
|
line_callback: Callable[[str], None] | None = None,
|
||||||
) -> AsyncGenerator[None]:
|
) -> AsyncGenerator[None]:
|
||||||
"""Context manager to write, compile and run an ESPHome configuration."""
|
"""Context manager to write, compile and run an ESPHome configuration."""
|
||||||
# Write the YAML config
|
# Write the YAML config
|
||||||
@@ -528,7 +539,9 @@ async def run_compiled_context(
|
|||||||
port_socket.close()
|
port_socket.close()
|
||||||
|
|
||||||
# Run the binary and wait for the API server to start
|
# Run the binary and wait for the API server to start
|
||||||
async with run_binary_and_wait_for_port(binary_path, LOCALHOST, port):
|
async with run_binary_and_wait_for_port(
|
||||||
|
binary_path, LOCALHOST, port, line_callback=line_callback
|
||||||
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@@ -542,7 +555,9 @@ async def run_compiled(
|
|||||||
port, port_socket = reserved_tcp_port
|
port, port_socket = reserved_tcp_port
|
||||||
|
|
||||||
def _run_compiled(
|
def _run_compiled(
|
||||||
yaml_content: str, filename: str | None = None
|
yaml_content: str,
|
||||||
|
filename: str | None = None,
|
||||||
|
line_callback: Callable[[str], None] | None = None,
|
||||||
) -> AbstractAsyncContextManager[asyncio.subprocess.Process]:
|
) -> AbstractAsyncContextManager[asyncio.subprocess.Process]:
|
||||||
return run_compiled_context(
|
return run_compiled_context(
|
||||||
yaml_content,
|
yaml_content,
|
||||||
@@ -551,6 +566,7 @@ async def run_compiled(
|
|||||||
compile_esphome,
|
compile_esphome,
|
||||||
port,
|
port,
|
||||||
port_socket,
|
port_socket,
|
||||||
|
line_callback=line_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield _run_compiled
|
yield _run_compiled
|
||||||
|
@@ -0,0 +1,78 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
|
||||||
|
|
||||||
|
CODEOWNERS = ["@esphome/tests"]
|
||||||
|
|
||||||
|
loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component")
|
||||||
|
LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component)
|
||||||
|
|
||||||
|
CONF_DISABLE_AFTER = "disable_after"
|
||||||
|
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
||||||
|
|
||||||
|
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||||
|
cv.Required(CONF_NAME): cv.string,
|
||||||
|
cv.Optional(CONF_DISABLE_AFTER, default=0): cv.int_,
|
||||||
|
cv.Optional(CONF_TEST_REDUNDANT_OPERATIONS, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||||
|
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
# Define actions
|
||||||
|
EnableAction = loop_test_component_ns.class_("EnableAction", automation.Action)
|
||||||
|
DisableAction = loop_test_component_ns.class_("DisableAction", automation.Action)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"loop_test_component.enable",
|
||||||
|
EnableAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(LoopTestComponent),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def enable_to_code(config, action_id, template_arg, args):
|
||||||
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"loop_test_component.disable",
|
||||||
|
DisableAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(LoopTestComponent),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def disable_to_code(config, action_id, template_arg, args):
|
||||||
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
# The parent config doesn't actually create a component
|
||||||
|
# We just create each sub-component
|
||||||
|
for comp_config in config[CONF_COMPONENTS]:
|
||||||
|
var = cg.new_Pvariable(comp_config[CONF_ID])
|
||||||
|
await cg.register_component(var, comp_config)
|
||||||
|
|
||||||
|
cg.add(var.set_name(comp_config[CONF_NAME]))
|
||||||
|
cg.add(var.set_disable_after(comp_config[CONF_DISABLE_AFTER]))
|
||||||
|
cg.add(
|
||||||
|
var.set_test_redundant_operations(
|
||||||
|
comp_config[CONF_TEST_REDUNDANT_OPERATIONS]
|
||||||
|
)
|
||||||
|
)
|
@@ -0,0 +1,43 @@
|
|||||||
|
#include "loop_test_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace loop_test_component {
|
||||||
|
|
||||||
|
void LoopTestComponent::setup() { ESP_LOGI(TAG, "[%s] Setup called", this->name_.c_str()); }
|
||||||
|
|
||||||
|
void LoopTestComponent::loop() {
|
||||||
|
this->loop_count_++;
|
||||||
|
ESP_LOGI(TAG, "[%s] Loop count: %d", this->name_.c_str(), this->loop_count_);
|
||||||
|
|
||||||
|
// Test self-disable after specified count
|
||||||
|
if (this->disable_after_ > 0 && this->loop_count_ == this->disable_after_) {
|
||||||
|
ESP_LOGI(TAG, "[%s] Disabling self after %d loops", this->name_.c_str(), this->disable_after_);
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test redundant operations
|
||||||
|
if (this->test_redundant_operations_ && this->loop_count_ == 5) {
|
||||||
|
if (this->name_ == "redundant_enable") {
|
||||||
|
ESP_LOGI(TAG, "[%s] Testing enable when already enabled", this->name_.c_str());
|
||||||
|
this->enable_loop();
|
||||||
|
} else if (this->name_ == "redundant_disable") {
|
||||||
|
ESP_LOGI(TAG, "[%s] Testing disable when will be disabled", this->name_.c_str());
|
||||||
|
// We'll disable at count 10, but try to disable again at 5
|
||||||
|
this->disable_loop();
|
||||||
|
ESP_LOGI(TAG, "[%s] First disable complete", this->name_.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopTestComponent::service_enable() {
|
||||||
|
ESP_LOGI(TAG, "[%s] Service enable called", this->name_.c_str());
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopTestComponent::service_disable() {
|
||||||
|
ESP_LOGI(TAG, "[%s] Service disable called", this->name_.c_str());
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace loop_test_component
|
||||||
|
} // namespace esphome
|
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace loop_test_component {
|
||||||
|
|
||||||
|
static const char *const TAG = "loop_test_component";
|
||||||
|
|
||||||
|
class LoopTestComponent : public Component {
|
||||||
|
public:
|
||||||
|
void set_name(const std::string &name) { this->name_ = name; }
|
||||||
|
void set_disable_after(int count) { this->disable_after_ = count; }
|
||||||
|
void set_test_redundant_operations(bool test) { this->test_redundant_operations_ = test; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
// Service methods for external control
|
||||||
|
void service_enable();
|
||||||
|
void service_disable();
|
||||||
|
|
||||||
|
int get_loop_count() const { return this->loop_count_; }
|
||||||
|
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string name_;
|
||||||
|
int loop_count_{0};
|
||||||
|
int disable_after_{0};
|
||||||
|
bool test_redundant_operations_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class EnableAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
EnableAction(LoopTestComponent *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
void play(Ts... x) override { this->parent_->service_enable(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LoopTestComponent *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class DisableAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
DisableAction(LoopTestComponent *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
void play(Ts... x) override { this->parent_->service_disable(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LoopTestComponent *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace loop_test_component
|
||||||
|
} // namespace esphome
|
48
tests/integration/fixtures/loop_disable_enable.yaml
Normal file
48
tests/integration/fixtures/loop_disable_enable.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
esphome:
|
||||||
|
name: loop-test
|
||||||
|
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source:
|
||||||
|
type: local
|
||||||
|
path: EXTERNAL_COMPONENT_PATH
|
||||||
|
|
||||||
|
loop_test_component:
|
||||||
|
components:
|
||||||
|
# Component that disables itself after 10 loops
|
||||||
|
- id: self_disable_10
|
||||||
|
name: "self_disable_10"
|
||||||
|
disable_after: 10
|
||||||
|
|
||||||
|
# Component that never disables itself (for re-enable test)
|
||||||
|
- id: normal_component
|
||||||
|
name: "normal_component"
|
||||||
|
disable_after: 0
|
||||||
|
|
||||||
|
# Component that tests enable when already enabled
|
||||||
|
- id: redundant_enable
|
||||||
|
name: "redundant_enable"
|
||||||
|
test_redundant_operations: true
|
||||||
|
disable_after: 0
|
||||||
|
|
||||||
|
# Component that tests disable when already disabled
|
||||||
|
- id: redundant_disable
|
||||||
|
name: "redundant_disable"
|
||||||
|
test_redundant_operations: true
|
||||||
|
disable_after: 10
|
||||||
|
|
||||||
|
# Interval to re-enable the self_disable_10 component after some time
|
||||||
|
interval:
|
||||||
|
- interval: 0.5s
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return id(self_disable_10).get_loop_count() == 10;'
|
||||||
|
then:
|
||||||
|
- logger.log: "Re-enabling self_disable_10 via service"
|
||||||
|
- loop_test_component.enable:
|
||||||
|
id: self_disable_10
|
150
tests/integration/test_loop_disable_enable.py
Normal file
150
tests/integration/test_loop_disable_enable.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""Integration test for loop disable/enable functionality."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_loop_disable_enable(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that components can disable and enable their loop() method."""
|
||||||
|
# Get the absolute path to the external components directory
|
||||||
|
external_components_path = str(
|
||||||
|
Path(__file__).parent / "fixtures" / "external_components"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace the placeholder in the YAML config with the actual path
|
||||||
|
yaml_config = yaml_config.replace(
|
||||||
|
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track log messages and events
|
||||||
|
log_messages: list[str] = []
|
||||||
|
|
||||||
|
# Event fired when self_disable_10 component disables itself after 10 loops
|
||||||
|
self_disable_10_disabled = asyncio.Event()
|
||||||
|
# Event fired when normal_component reaches 10 loops
|
||||||
|
normal_component_10_loops = asyncio.Event()
|
||||||
|
# Event fired when redundant_enable component tests enabling when already enabled
|
||||||
|
redundant_enable_tested = asyncio.Event()
|
||||||
|
# Event fired when redundant_disable component tests disabling when already disabled
|
||||||
|
redundant_disable_tested = asyncio.Event()
|
||||||
|
# Event fired when self_disable_10 component is re-enabled and runs again (count > 10)
|
||||||
|
self_disable_10_re_enabled = asyncio.Event()
|
||||||
|
|
||||||
|
# Track loop counts for components
|
||||||
|
self_disable_10_counts: list[int] = []
|
||||||
|
normal_component_counts: list[int] = []
|
||||||
|
|
||||||
|
def on_log_line(line: str) -> None:
|
||||||
|
"""Process each log line from the process output."""
|
||||||
|
# Strip ANSI color codes
|
||||||
|
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||||
|
|
||||||
|
if "loop_test_component" not in clean_line:
|
||||||
|
return
|
||||||
|
|
||||||
|
log_messages.append(clean_line)
|
||||||
|
|
||||||
|
# Track specific events using the cleaned line
|
||||||
|
if "[self_disable_10]" in clean_line:
|
||||||
|
if "Loop count:" in clean_line:
|
||||||
|
# Extract loop count
|
||||||
|
try:
|
||||||
|
count = int(clean_line.split("Loop count: ")[1])
|
||||||
|
self_disable_10_counts.append(count)
|
||||||
|
# Check if component was re-enabled (count > 10)
|
||||||
|
if count > 10:
|
||||||
|
self_disable_10_re_enabled.set()
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
pass
|
||||||
|
elif "Disabling self after 10 loops" in clean_line:
|
||||||
|
self_disable_10_disabled.set()
|
||||||
|
|
||||||
|
elif "[normal_component]" in clean_line and "Loop count:" in clean_line:
|
||||||
|
try:
|
||||||
|
count = int(clean_line.split("Loop count: ")[1])
|
||||||
|
normal_component_counts.append(count)
|
||||||
|
if count >= 10:
|
||||||
|
normal_component_10_loops.set()
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif (
|
||||||
|
"[redundant_enable]" in clean_line
|
||||||
|
and "Testing enable when already enabled" in clean_line
|
||||||
|
):
|
||||||
|
redundant_enable_tested.set()
|
||||||
|
|
||||||
|
elif (
|
||||||
|
"[redundant_disable]" in clean_line
|
||||||
|
and "Testing disable when will be disabled" in clean_line
|
||||||
|
):
|
||||||
|
redundant_disable_tested.set()
|
||||||
|
|
||||||
|
# Write, compile and run the ESPHome device with log callback
|
||||||
|
async with (
|
||||||
|
run_compiled(yaml_config, line_callback=on_log_line),
|
||||||
|
api_client_connected() as client,
|
||||||
|
):
|
||||||
|
# Verify we can connect and get device info
|
||||||
|
device_info = await client.device_info()
|
||||||
|
assert device_info is not None
|
||||||
|
assert device_info.name == "loop-test"
|
||||||
|
|
||||||
|
# Wait for self_disable_10 to disable itself
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("self_disable_10 did not disable itself within 10 seconds")
|
||||||
|
|
||||||
|
# Verify it ran at least 10 times before disabling
|
||||||
|
assert len([c for c in self_disable_10_counts if c <= 10]) == 10, (
|
||||||
|
f"Expected exactly 10 loops before disable, got {[c for c in self_disable_10_counts if c <= 10]}"
|
||||||
|
)
|
||||||
|
assert self_disable_10_counts[:10] == list(range(1, 11)), (
|
||||||
|
f"Expected first 10 counts to be 1-10, got {self_disable_10_counts[:10]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for normal_component to run at least 10 times
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for redundant operation tests
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("redundant_enable did not test enabling when already enabled")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
"redundant_disable did not test disabling when will be disabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait to see if self_disable_10 gets re-enabled
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("self_disable_10 was not re-enabled within 5 seconds")
|
||||||
|
|
||||||
|
# Component was re-enabled - verify it ran more times
|
||||||
|
later_self_disable_counts = [c for c in self_disable_10_counts if c > 10]
|
||||||
|
assert later_self_disable_counts, (
|
||||||
|
"self_disable_10 was re-enabled but did not run additional times"
|
||||||
|
)
|
@@ -13,7 +13,19 @@ from aioesphomeapi import APIClient
|
|||||||
ConfigWriter = Callable[[str, str | None], Awaitable[Path]]
|
ConfigWriter = Callable[[str, str | None], Awaitable[Path]]
|
||||||
CompileFunction = Callable[[Path], Awaitable[Path]]
|
CompileFunction = Callable[[Path], Awaitable[Path]]
|
||||||
RunFunction = Callable[[Path], Awaitable[asyncio.subprocess.Process]]
|
RunFunction = Callable[[Path], Awaitable[asyncio.subprocess.Process]]
|
||||||
RunCompiledFunction = Callable[[str, str | None], AbstractAsyncContextManager[None]]
|
|
||||||
|
|
||||||
|
class RunCompiledFunction(Protocol):
|
||||||
|
"""Protocol for run_compiled function with optional line callback."""
|
||||||
|
|
||||||
|
def __call__( # noqa: E704
|
||||||
|
self,
|
||||||
|
yaml_content: str,
|
||||||
|
filename: str | None = None,
|
||||||
|
line_callback: Callable[[str], None] | None = None,
|
||||||
|
) -> AbstractAsyncContextManager[None]: ...
|
||||||
|
|
||||||
|
|
||||||
WaitFunction = Callable[[APIClient, float], Awaitable[bool]]
|
WaitFunction = Callable[[APIClient, float], Awaitable[bool]]
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user