1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

Merge remote-tracking branch 'upstream/dev' into web_server_cap_portal_co_exist

This commit is contained in:
J. Nick Koston
2026-01-09 10:17:51 -10:00
23 changed files with 362 additions and 55 deletions

View File

@@ -1,5 +1,3 @@
#ifdef USE_ARDUINO
#include "ac_dimmer.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -9,12 +7,12 @@
#ifdef USE_ESP8266
#include <core_esp8266_waveform.h>
#endif
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <esp32-hal-timer.h>
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#endif
namespace esphome {
namespace ac_dimmer {
namespace esphome::ac_dimmer {
static const char *const TAG = "ac_dimmer";
@@ -27,7 +25,14 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no
/// However other factors like gate driver propagation time
/// are also considered and a really low value is not important
/// See also: https://github.com/esphome/issues/issues/1632
static const uint32_t GATE_ENABLE_TIME = 50;
static constexpr uint32_t GATE_ENABLE_TIME = 50;
#ifdef USE_ESP32
/// Timer frequency in Hz (1 MHz = 1µs resolution)
static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
/// Timer interrupt interval in microseconds
static constexpr uint64_t TIMER_INTERVAL_US = 50;
#endif
/// Function called from timer interrupt
/// Input is current time in microseconds (micros())
@@ -154,7 +159,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
#ifdef USE_ESP32
// ESP32 implementation, uses basically the same code but needs to wrap
// timer_interrupt() function to auto-reschedule
static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
#endif
@@ -194,15 +199,15 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
// timer frequency of 1mhz
dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timerAlarm(dimmer_timer, 50, true, 0);
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
#endif
}
void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
@@ -210,6 +215,7 @@ void AcDimmer::write_state(float state) {
this->store_.init_cycle = this->init_with_half_cycle_;
this->store_.value = new_value;
}
void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
@@ -230,7 +236,4 @@ void AcDimmer::dump_config() {
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
}
} // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO
} // namespace esphome::ac_dimmer

View File

@@ -1,13 +1,10 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
namespace esphome {
namespace ac_dimmer {
namespace esphome::ac_dimmer {
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
@@ -64,7 +61,4 @@ class AcDimmer : public output::FloatOutput, public Component {
DimMethod method_;
};
} // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO
} // namespace esphome::ac_dimmer

View File

@@ -0,0 +1,152 @@
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#include "freertos/FreeRTOS.h"
#include "esphome/core/log.h"
#include "driver/gptimer.h"
#include "esp_clk_tree.h"
#include "soc/clk_tree_defs.h"
static const char *const TAG = "hw_timer_esp_idf";
namespace esphome::ac_dimmer {
// GPTimer divider constraints from ESP-IDF documentation
static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2;
static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536;
using voidFuncPtr = void (*)();
using voidFuncPtrArg = void (*)(void *);
struct InterruptConfigT {
voidFuncPtr fn{nullptr};
void *arg{nullptr};
};
struct HWTimer {
gptimer_handle_t timer_handle{nullptr};
InterruptConfigT interrupt_handle{};
bool timer_started{false};
};
HWTimer *timer_begin(uint32_t frequency) {
esp_err_t err = ESP_OK;
uint32_t counter_src_hz = 0;
uint32_t divider = 0;
soc_module_clk_t clk;
for (auto clk_candidate : SOC_GPTIMER_CLKS) {
clk = clk_candidate;
esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz);
divider = counter_src_hz / frequency;
if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) {
break;
} else {
divider = 0;
}
}
if (divider == 0) {
ESP_LOGE(TAG, "Resolution not possible; aborting");
return nullptr;
}
gptimer_config_t config = {
.clk_src = static_cast<gptimer_clock_source_t>(clk),
.direction = GPTIMER_COUNT_UP,
.resolution_hz = frequency,
.flags = {.intr_shared = true},
};
HWTimer *timer = new HWTimer();
err = gptimer_new_timer(&config, &timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer creation failed; error %d", err);
delete timer;
return nullptr;
}
err = gptimer_enable(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer enable failed; error %d", err);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
err = gptimer_start(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer start failed; error %d", err);
gptimer_disable(timer->timer_handle);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
timer->timer_started = true;
return timer;
}
bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) {
auto *isr = static_cast<InterruptConfigT *>(args);
if (isr->fn) {
if (isr->arg) {
reinterpret_cast<voidFuncPtrArg>(isr->fn)(isr->arg);
} else {
isr->fn();
}
}
// Return false to indicate that no higher-priority task was woken and no context switch is requested.
return false;
}
static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_event_callbacks_t cbs = {
.on_alarm = timer_fn_wrapper,
};
timer->interrupt_handle.fn = reinterpret_cast<voidFuncPtr>(user_func);
timer->interrupt_handle.arg = arg;
if (timer->timer_started) {
gptimer_stop(timer->timer_handle);
}
gptimer_disable(timer->timer_handle);
esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err);
}
gptimer_enable(timer->timer_handle);
if (timer->timer_started) {
gptimer_start(timer->timer_handle);
}
}
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) {
timer_attach_interrupt_functional_arg(timer, reinterpret_cast<voidFuncPtrArg>(user_func), nullptr);
}
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_alarm_config_t alarm_cfg = {
.alarm_count = alarm_value,
.reload_count = reload_count,
.flags = {.auto_reload_on_alarm = autoreload},
};
esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err);
}
}
} // namespace esphome::ac_dimmer
#endif

View File

@@ -0,0 +1,17 @@
#pragma once
#ifdef USE_ESP32
#include "driver/gptimer_types.h"
namespace esphome::ac_dimmer {
struct HWTimer;
HWTimer *timer_begin(uint32_t frequency);
void timer_attach_interrupt(HWTimer *timer, void (*user_func)());
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
} // namespace esphome::ac_dimmer
#endif

View File

@@ -32,7 +32,6 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_arduino,
)

View File

@@ -186,14 +186,17 @@ void APIServer::loop() {
}
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername()));
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
#endif
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
@@ -205,6 +208,11 @@ void APIServer::loop() {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_->trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}
}

View File

@@ -39,6 +39,24 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
}
/// Count number of varints in a packed buffer
inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
uint16_t count = 0;
while (len > 0) {
// Skip varint bytes until we find one without continuation bit
while (len > 0 && (*data & 0x80)) {
data++;
len--;
}
if (len > 0) {
data++;
len--;
count++;
}
}
return count;
}
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
@@ -180,9 +198,10 @@ class ProtoVarInt {
uint64_t value_;
};
// Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage;
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
class ProtoDecodableMessage;
class ProtoMessage;
class ProtoSize;
class ProtoLengthDelimited {
public:
@@ -334,6 +353,8 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
/// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
@@ -341,9 +362,6 @@ class ProtoWriteBuffer {
std::vector<uint8_t> *buffer_;
};
// Forward declaration
class ProtoSize;
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
@@ -792,8 +810,43 @@ class ProtoSize {
}
}
}
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
}
};
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
return;
// Calculate packed size
size_t packed_size = 0;
for (int value : values) {
packed_size += ProtoSize::varint(encode_zigzag32(value));
}
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
this->encode_varint_raw(packed_size);
for (int value : values) {
this->encode_varint_raw(encode_zigzag32(value));
}
}
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message

View File

@@ -550,12 +550,12 @@ async def to_code(config: ConfigType) -> None:
ref="0.2.2",
)
# Set compile-time configuration via defines
# Set compile-time configuration via build flags (so external library sees them)
if CONF_BIT_DEPTH in config:
cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH])
cg.add_build_flag(f"-DHUB75_BIT_DEPTH={config[CONF_BIT_DEPTH]}")
if CONF_GAMMA_CORRECT in config:
cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT])
cg.add_build_flag(f"-DHUB75_GAMMA_MODE={config[CONF_GAMMA_CORRECT].enum_value}")
# Await all pin expressions
pin_expressions = {

View File

@@ -128,6 +128,39 @@ void RemoteReceiverBase::call_dumpers_() {
void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); }
/* RemoteTransmitData */
void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count) {
this->data_.clear();
this->data_.reserve(count);
while (len > 0) {
// Parse varint (inline, no dependency on api component)
uint32_t raw = 0;
uint32_t shift = 0;
uint32_t consumed = 0;
for (; consumed < len && consumed < 5; consumed++) {
uint8_t byte = data[consumed];
raw |= (byte & 0x7F) << shift;
if ((byte & 0x80) == 0) {
consumed++;
break;
}
shift += 7;
}
if (consumed == 0)
break; // Parse error
// Zigzag decode: (n >> 1) ^ -(n & 1)
int32_t decoded = static_cast<int32_t>((raw >> 1) ^ (~(raw & 1) + 1));
this->data_.push_back(decoded);
data += consumed;
len -= consumed;
}
}
/* RemoteTransmitterBase */
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
const auto &vec = this->temp_.get_data();

View File

@@ -31,6 +31,11 @@ class RemoteTransmitData {
uint32_t get_carrier_frequency() const { return this->carrier_frequency_; }
const RawTimings &get_data() const { return this->data_; }
void set_data(const RawTimings &data) { this->data_ = data; }
/// Set data from packed protobuf sint32 buffer (zigzag + varint encoded)
/// @param data Pointer to packed zigzag-varint-encoded sint32 values
/// @param len Length of the buffer in bytes
/// @param count Number of values (for reserve optimization)
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
void reset() {
this->data_.clear();
this->carrier_frequency_ = 0;

View File

@@ -502,14 +502,16 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
// Build id into stack buffer - ArduinoJson copies the string
// Format: {prefix}/{device?}/{name}
// Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120):
// With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin
// Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin
// Buffer sizes use constants from entity_base.h validated in core/config.py
// Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN
// (hostname)
#ifdef USE_DEVICES
char id_buf[280];
static constexpr size_t ID_BUF_SIZE =
ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1;
#else
char id_buf[150];
static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1;
#endif
char id_buf[ID_BUF_SIZE];
char *p = id_buf;
memcpy(p, prefix, prefix_len);
p += prefix_len;

View File

@@ -238,12 +238,21 @@ def _apply_min_auth_mode_default(config):
def final_validate(config):
has_sta = bool(config.get(CONF_NETWORKS, True))
has_ap = CONF_AP in config
has_improv = "esp32_improv" in fv.full_config.get()
has_improv_serial = "improv_serial" in fv.full_config.get()
full_config = fv.full_config.get()
has_improv = "esp32_improv" in full_config
has_improv_serial = "improv_serial" in full_config
has_captive_portal = "captive_portal" in full_config
has_web_server = "web_server" in full_config
if not (has_sta or has_ap or has_improv or has_improv_serial):
raise cv.Invalid(
"Please specify at least an SSID or an Access Point to create."
)
if has_ap and not has_captive_portal and not has_web_server:
_LOGGER.warning(
"WiFi AP is configured but neither captive_portal nor web_server is enabled. "
"The AP will not be usable for configuration or monitoring. "
"Add 'captive_portal:' or 'web_server:' to your configuration."
)
FINAL_VALIDATE_SCHEMA = cv.All(
@@ -466,7 +475,7 @@ async def to_code(config):
)
cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT]))
cg.add_define("USE_WIFI_AP")
elif CORE.is_esp32:
elif CORE.is_esp32 and not CORE.using_arduino:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)

View File

@@ -184,17 +184,24 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ:
else:
_compile_process_limit_default = cv.UNDEFINED
# Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h
FRIENDLY_NAME_MAX_LEN = 120
AREA_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
cv.Required(CONF_NAME): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
),
}
)
DEVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Device),
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
cv.Required(CONF_NAME): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
),
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
}
)
@@ -210,7 +217,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_NAME): cv.valid_name,
# Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(
cv.string_no_slash, cv.Length(max=120)
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
),
cv.Optional(CONF_AREA): validate_area_config,
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),

View File

@@ -16,7 +16,14 @@ namespace esphome {
// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py
static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31;
// Maximum size for object_id buffer - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py
// Maximum friendly name length for entities and sub-devices - keep in sync with FRIENDLY_NAME_MAX_LEN in
// esphome/core/config.py
static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120;
// Maximum domain length (longest: "alarm_control_panel" = 19)
static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20;
// Maximum size for object_id buffer (friendly_name + null + margin)
static constexpr size_t OBJECT_ID_MAX_LEN = 128;
enum EntityCategory : uint8_t {

View File

@@ -11,7 +11,7 @@ pyserial==3.5
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
esphome-dashboard==20260110.0
aioesphomeapi==43.10.1
zeroconf==0.148.0
puremagic==1.30

View File

@@ -1,5 +0,0 @@
substitutions:
gate_pin: GPIO4
zero_cross_pin: GPIO5
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
gate_pin: GPIO18
zero_cross_pin: GPIO19
<<: !include common.yaml

View File

@@ -6,6 +6,7 @@ display:
panel_height: 32
double_buffer: true
brightness: 128
gamma_correct: gamma_2_2
pages:
- id: page1
lambda: |-

View File

@@ -5,6 +5,7 @@ display:
panel_height: 32
double_buffer: true
brightness: 128
gamma_correct: cie1931
r1_pin: GPIO42
g1_pin: GPIO41
b1_pin: GPIO40

View File

@@ -7,6 +7,7 @@ display:
platform: ili9xxx
id: main_lcd
model: ili9342
data_rate: 31.25MHz
cs_pin: 20
dc_pin: 21
reset_pin: 22

View File

@@ -42,4 +42,3 @@ status_led:
# Include API at priority 40
api:
password: "apipassword"

View File

@@ -24,6 +24,14 @@ api:
- logger.log:
format: "Client %s disconnected from %s"
args: [client_info.c_str(), client_address.c_str()]
# Verify fix for issue #11131: api.connected should reflect true state in trigger
- if:
condition:
api.connected:
then:
- logger.log: "Other clients still connected"
else:
- logger.log: "No clients remaining"
logger:
level: DEBUG

View File

@@ -23,12 +23,14 @@ async def test_api_conditional_memory(
# Track log messages
connected_future = loop.create_future()
disconnected_future = loop.create_future()
no_clients_future = loop.create_future()
service_simple_future = loop.create_future()
service_args_future = loop.create_future()
# Patterns to match in logs
connected_pattern = re.compile(r"Client .* connected from")
disconnected_pattern = re.compile(r"Client .* disconnected from")
no_clients_pattern = re.compile(r"No clients remaining")
service_simple_pattern = re.compile(r"Simple service called")
service_args_pattern = re.compile(
r"Service called with: test_string, 123, 1, 42\.50"
@@ -40,6 +42,8 @@ async def test_api_conditional_memory(
connected_future.set_result(True)
elif not disconnected_future.done() and disconnected_pattern.search(line):
disconnected_future.set_result(True)
elif not no_clients_future.done() and no_clients_pattern.search(line):
no_clients_future.set_result(True)
elif not service_simple_future.done() and service_simple_pattern.search(line):
service_simple_future.set_result(True)
elif not service_args_future.done() and service_args_pattern.search(line):
@@ -109,3 +113,7 @@ async def test_api_conditional_memory(
# Client disconnected here, wait for disconnect log
await asyncio.wait_for(disconnected_future, timeout=5.0)
# Verify fix for issue #11131: api.connected should be false in trigger
# when the last client disconnects
await asyncio.wait_for(no_clients_future, timeout=5.0)