mirror of
https://github.com/esphome/esphome.git
synced 2025-11-08 19:11:49 +00:00
Compare commits
58 Commits
2025.7.0b2
...
2025.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe4ffa0cf | ||
|
|
576ce7ee35 | ||
|
|
8a45e877bb | ||
|
|
84607c1255 | ||
|
|
8664ec0a3b | ||
|
|
32d8c60a0b | ||
|
|
976a1e27b4 | ||
|
|
cc2c1b1d89 | ||
|
|
85495d38b7 | ||
|
|
84a77ee427 | ||
|
|
11a4115e30 | ||
|
|
121ed687f3 | ||
|
|
c602f3082e | ||
|
|
4a43f922c6 | ||
|
|
21e66b76e4 | ||
|
|
cdeed7afa7 | ||
|
|
1a9f02fa63 | ||
|
|
7ad1b039f9 | ||
|
|
e255d73c29 | ||
|
|
46f5c44b37 | ||
|
|
9d80889bc9 | ||
|
|
08a5ba6ef1 | ||
|
|
28128c65e5 | ||
|
|
efcad565ee | ||
|
|
cd987feb5b | ||
|
|
5707389faa | ||
|
|
3f78db5c63 | ||
|
|
de0656a188 | ||
|
|
90a16ffa89 | ||
|
|
4182076f64 | ||
|
|
8c8c08d40c | ||
|
|
18e2f41424 | ||
|
|
bd0fe34b14 | ||
|
|
37982290f7 | ||
|
|
02b7db7311 | ||
|
|
9bc3ff5f53 | ||
|
|
786cb7ded5 | ||
|
|
7f01c25782 | ||
|
|
321f2f87b0 | ||
|
|
11a051401f | ||
|
|
6148dd7e41 | ||
|
|
42b6939e90 | ||
|
|
35b3f75f7c | ||
|
|
78e8001aa8 | ||
|
|
84fc6ff71a | ||
|
|
16292a9f13 | ||
|
|
90f0ebb22b | ||
|
|
4153380f99 | ||
|
|
740c0ef9d7 | ||
|
|
b4521e1d8c | ||
|
|
10ca7ed85b | ||
|
|
e43efdaaec | ||
|
|
9207bf97f3 | ||
|
|
c13317f807 | ||
|
|
77d1d0414d | ||
|
|
8f42bc6aac | ||
|
|
9beb4e2cd4 | ||
|
|
097aac2183 |
@@ -1,6 +1,14 @@
|
||||
---
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||
autoupdate_schedule: weekly
|
||||
autofix_prs: false
|
||||
# Skip hooks that have issues in pre-commit CI environment
|
||||
skip: [pylint, yamllint]
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.7.0b2
|
||||
PROJECT_NUMBER = 2025.7.2
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -11,6 +11,18 @@ namespace esphome {
|
||||
namespace api {
|
||||
|
||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
return val ? std::string(val) : std::string();
|
||||
} // For lambdas returning char* (e.g., itoa)
|
||||
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||
static std::string value_to_string(const std::string &val) { return val; }
|
||||
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||
|
||||
public:
|
||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||
|
||||
@@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
||||
TemplatableStringValue(F f)
|
||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
|
||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
|
||||
};
|
||||
|
||||
template<typename... Ts> class TemplatableKeyValuePair {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/as3935/as3935.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as3935_spi {
|
||||
|
||||
@@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
|
||||
elif CORE.is_esp8266:
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <lwip/igmp.h>
|
||||
#include <lwip/init.h>
|
||||
@@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
|
||||
ip4_addr_t multicast_addr =
|
||||
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
|
||||
|
||||
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||
err_t err;
|
||||
{
|
||||
LwIPLock lock;
|
||||
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
|
||||
@@ -104,6 +109,7 @@ void E131Component::leave_(int universe) {
|
||||
if (listen_method_ == E131_MULTICAST) {
|
||||
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
||||
|
||||
LwIPLock lock;
|
||||
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
#include "lwip/priv/tcpip_priv.h"
|
||||
#endif
|
||||
|
||||
LwIPLock::LwIPLock() {
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
|
||||
// its internal state. Any thread can take this lock to safely access lwIP APIs.
|
||||
//
|
||||
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
|
||||
// already holds the lwIP core lock. This prevents recursive locking attempts and
|
||||
// allows nested LwIPLock instances to work correctly.
|
||||
//
|
||||
// If we don't already hold the lock, acquire it. This will block until the lock
|
||||
// is available if another thread currently holds it.
|
||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
LOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
LwIPLock::~LwIPLock() {
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
// Only release the lwIP core lock if this thread currently holds it.
|
||||
//
|
||||
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
|
||||
// ownership tracking. It returns true only if the current thread is registered
|
||||
// as the lock holder.
|
||||
//
|
||||
// This check is essential because:
|
||||
// 1. We may not have acquired the lock in the constructor (if we already held it)
|
||||
// 2. The lock might have been released by other means between constructor and destructor
|
||||
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
|
||||
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
UNLOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
||||
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
@@ -8,6 +10,7 @@ from esphome.const import (
|
||||
CONF_CONTRAST,
|
||||
CONF_DATA_PINS,
|
||||
CONF_FREQUENCY,
|
||||
CONF_I2C,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_PIN,
|
||||
@@ -20,6 +23,9 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
import esphome.final_validate as fv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
@@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
if CONF_I2C_PINS not in config:
|
||||
return
|
||||
fconf = fv.full_config.get()
|
||||
if fconf.get(CONF_I2C):
|
||||
raise cv.Invalid(
|
||||
"The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
SETTERS = {
|
||||
# pin assignment
|
||||
CONF_DATA_PINS: "set_data_pins",
|
||||
|
||||
@@ -22,6 +22,10 @@ void Mutex::unlock() {}
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
||||
|
||||
// ESP8266 doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
wifi_get_macaddr(STATION_IF, mac);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,16 @@ adjusted_ids = set()
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EspLdo),
|
||||
cv.Required(CONF_VOLTAGE): cv.All(
|
||||
cv.voltage, cv.float_range(min=0.5, max=2.7)
|
||||
),
|
||||
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
||||
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
||||
}
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EspLdo),
|
||||
cv.Required(CONF_VOLTAGE): cv.All(
|
||||
cv.voltage, cv.float_range(min=0.5, max=2.7)
|
||||
),
|
||||
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
||||
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.only_with_esp_idf,
|
||||
only_on_variant(supported=[VARIANT_ESP32P4]),
|
||||
|
||||
@@ -17,6 +17,9 @@ class EspLdo : public Component {
|
||||
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
|
||||
void set_voltage(float voltage) { this->voltage_ = voltage; }
|
||||
void adjust_voltage(float voltage);
|
||||
float get_setup_priority() const override {
|
||||
return setup_priority::BUS; // LDO setup should be done early
|
||||
}
|
||||
|
||||
protected:
|
||||
int channel_;
|
||||
|
||||
@@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
}
|
||||
|
||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||
LwIPLock lock;
|
||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||
return dns_ip;
|
||||
}
|
||||
@@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns1;
|
||||
@@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
const ip_addr_t *dns_ip1 = dns_getserver(0);
|
||||
const ip_addr_t *dns_ip2 = dns_getserver(1);
|
||||
const ip_addr_t *dns_ip1;
|
||||
const ip_addr_t *dns_ip2;
|
||||
{
|
||||
LwIPLock lock;
|
||||
dns_ip1 = dns_getserver(0);
|
||||
dns_ip2 = dns_getserver(1);
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
|
||||
@@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
|
||||
return {};
|
||||
}
|
||||
void Fan::save_state_() {
|
||||
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
|
||||
return;
|
||||
}
|
||||
|
||||
FanRestoreState state{};
|
||||
state.state = this->state;
|
||||
state.oscillating = this->oscillating;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_PIN
|
||||
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
|
||||
from esphome.core import CORE
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
GPIOBinarySensor = gpio_ns.class_(
|
||||
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
)
|
||||
@@ -24,7 +29,21 @@ CONFIG_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
|
||||
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
|
||||
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
||||
# stable operation on these platforms. Future maintainers should verify platform
|
||||
# capabilities before changing this default behavior.
|
||||
cv.SplitDefault(
|
||||
CONF_USE_INTERRUPT,
|
||||
bk72xx=False,
|
||||
esp32=True,
|
||||
esp8266=True,
|
||||
host=True,
|
||||
ln882x=False,
|
||||
nrf52=True,
|
||||
rp2040=True,
|
||||
rtl87xx=False,
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
|
||||
INTERRUPT_TYPES, upper=True
|
||||
),
|
||||
@@ -41,6 +60,22 @@ async def to_code(config):
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
|
||||
if config[CONF_USE_INTERRUPT]:
|
||||
# Check for ESP8266 GPIO16 interrupt limitation
|
||||
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
|
||||
# the Arduino attachInterrupt() function. This is the only known GPIO pin
|
||||
# across all supported platforms that has this limitation, so we handle it
|
||||
# here instead of in the platform-specific code.
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
|
||||
_LOGGER.warning(
|
||||
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
|
||||
"Falling back to polling mode (same as in ESPHome <2025.7). "
|
||||
"The sensor will work exactly as before, but other pins have better "
|
||||
"performance with interrupts.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
if use_interrupt:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
|
||||
@@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
|
||||
if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
@@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
||||
|
||||
for (auto build : root["builds"].as<JsonArray>()) {
|
||||
if (!build.containsKey("chipFamily")) {
|
||||
if (!build["chipFamily"].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
||||
if (!build.containsKey("ota")) {
|
||||
if (!build["ota"].is<JsonObject>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
auto ota = build["ota"];
|
||||
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
|
||||
JsonObject ota = build["ota"].as<JsonObject>();
|
||||
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||
this_update->update_info_.md5 = ota["md5"].as<std::string>();
|
||||
|
||||
if (ota.containsKey("summary"))
|
||||
if (ota["summary"].is<const char *>())
|
||||
this_update->update_info_.summary = ota["summary"].as<std::string>();
|
||||
if (ota.containsKey("release_url"))
|
||||
if (ota["release_url"].is<const char *>())
|
||||
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -180,7 +180,7 @@ async def to_code(config):
|
||||
await speaker.register_speaker(var, config)
|
||||
|
||||
if config[CONF_DAC_TYPE] == "internal":
|
||||
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
|
||||
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
|
||||
else:
|
||||
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
||||
if use_legacy():
|
||||
|
||||
@@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
@coroutine_with_priority(1.0)
|
||||
async def to_code(config):
|
||||
cg.add_library("bblanchon/ArduinoJson", "6.18.5")
|
||||
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
|
||||
cg.add_define("USE_JSON")
|
||||
cg.add_global(json_ns.using)
|
||||
|
||||
@@ -1,83 +1,76 @@
|
||||
#include "json_util.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
|
||||
|
||||
namespace esphome {
|
||||
namespace json {
|
||||
|
||||
static const char *const TAG = "json";
|
||||
|
||||
static std::vector<char> global_json_build_buffer; // NOLINT
|
||||
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||
struct SpiRamAllocator : ArduinoJson::Allocator {
|
||||
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
|
||||
|
||||
void deallocate(void *pointer) override {
|
||||
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
|
||||
// RAMAllocator::deallocate() requires the size, which we don't have access to here.
|
||||
// RAMAllocator::deallocate implementation just calls free() regardless of whether
|
||||
// the memory was allocated with heap_caps_malloc or malloc.
|
||||
// This is safe because ESP-IDF's heap implementation internally tracks the memory region
|
||||
// and routes free() to the appropriate heap.
|
||||
free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
|
||||
}
|
||||
|
||||
void *reallocate(void *ptr, size_t new_size) override {
|
||||
return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
|
||||
}
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
|
||||
};
|
||||
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// Here we are allocating up to 5kb of memory,
|
||||
// with the heap size minus 2kb to be safe if less than 5kb
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
while (true) {
|
||||
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
if (request_size == free_heap) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes",
|
||||
free_heap);
|
||||
return "{}";
|
||||
}
|
||||
request_size = std::min(request_size * 2, free_heap);
|
||||
continue;
|
||||
}
|
||||
json_document.shrinkToFit();
|
||||
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto doc_allocator = SpiRamAllocator();
|
||||
JsonDocument json_document(&doc_allocator);
|
||||
if (json_document.overflowed()) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||
return "{}";
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||
return "{}";
|
||||
}
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// Here we are allocating 1.5 times the data size,
|
||||
// with the heap size minus 2kb to be safe if less than that
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
|
||||
while (true) {
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size,
|
||||
free_heap);
|
||||
return false;
|
||||
}
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
json_document.shrinkToFit();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto doc_allocator = SpiRamAllocator();
|
||||
JsonDocument json_document(&doc_allocator);
|
||||
if (json_document.overflowed()) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||
return false;
|
||||
}
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
|
||||
if (err == DeserializationError::Ok) {
|
||||
return f(root);
|
||||
} else if (err == DeserializationError::NoMemory) {
|
||||
if (request_size * 2 >= free_heap) {
|
||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Increasing memory allocation.");
|
||||
request_size *= 2;
|
||||
continue;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if (err == DeserializationError::Ok) {
|
||||
return f(root);
|
||||
} else if (err == DeserializationError::NoMemory) {
|
||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||
return false;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
static const char *const TAG = "LD2420.binary_sensor";
|
||||
static const char *const TAG = "ld2420.binary_sensor";
|
||||
|
||||
void LD2420BinarySensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:");
|
||||
ESP_LOGCONFIG(TAG, "Binary Sensor:");
|
||||
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const char *const TAG = "LD2420.button";
|
||||
static const char *const TAG = "ld2420.button";
|
||||
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
@@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple";
|
||||
// Memory-efficient lookup tables
|
||||
struct StringToUint8 {
|
||||
const char *str;
|
||||
uint8_t value;
|
||||
const uint8_t value;
|
||||
};
|
||||
|
||||
static constexpr StringToUint8 OP_MODE_BY_STR[] = {
|
||||
@@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = {
|
||||
// Helper function for lookups
|
||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
|
||||
for (const auto &entry : arr) {
|
||||
if (str == entry.str)
|
||||
if (str == entry.str) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return 0xFF; // Not found
|
||||
}
|
||||
@@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() {
|
||||
|
||||
void LD2420Component::loop() {
|
||||
// If there is a active send command do not process it here, the send command call will handle it.
|
||||
if (!this->get_cmd_active_()) {
|
||||
if (!this->available())
|
||||
return;
|
||||
static uint8_t buffer[2048];
|
||||
static uint8_t rx_data;
|
||||
while (this->available()) {
|
||||
rx_data = this->read();
|
||||
this->readline_(rx_data, buffer, sizeof(buffer));
|
||||
}
|
||||
while (!this->cmd_active_ && this->available()) {
|
||||
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() {
|
||||
|
||||
// Store average and peak values
|
||||
this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
|
||||
if (this->gate_peak[gate] < peak)
|
||||
if (this->gate_peak[gate] < peak) {
|
||||
this->gate_peak[gate] = peak;
|
||||
}
|
||||
|
||||
uint32_t calculated_value =
|
||||
(static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate])));
|
||||
@@ -403,8 +398,9 @@ void LD2420Component::set_operating_mode(const std::string &state) {
|
||||
}
|
||||
} else {
|
||||
// Set the current data back so we don't have new data that can be applied in error.
|
||||
if (this->get_calibration_())
|
||||
if (this->get_calibration_()) {
|
||||
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
|
||||
}
|
||||
this->set_calibration_(false);
|
||||
}
|
||||
} else {
|
||||
@@ -414,30 +410,32 @@ void LD2420Component::set_operating_mode(const std::string &state) {
|
||||
}
|
||||
|
||||
void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) {
|
||||
static int pos = 0;
|
||||
|
||||
if (rx_data >= 0) {
|
||||
if (pos < len - 1) {
|
||||
buffer[pos++] = rx_data;
|
||||
buffer[pos] = 0;
|
||||
} else {
|
||||
pos = 0;
|
||||
}
|
||||
if (pos >= 4) {
|
||||
if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
|
||||
this->set_cmd_active_(false); // Set command state to inactive after responce.
|
||||
this->handle_ack_data_(buffer, pos);
|
||||
pos = 0;
|
||||
} else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) &&
|
||||
(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
|
||||
this->handle_simple_mode_(buffer, pos);
|
||||
pos = 0;
|
||||
} else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
|
||||
(this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
|
||||
this->handle_energy_mode_(buffer, pos);
|
||||
pos = 0;
|
||||
}
|
||||
}
|
||||
if (rx_data < 0) {
|
||||
return; // No data available
|
||||
}
|
||||
if (this->buffer_pos_ < len - 1) {
|
||||
buffer[this->buffer_pos_++] = rx_data;
|
||||
buffer[this->buffer_pos_] = 0;
|
||||
} else {
|
||||
// We should never get here, but just in case...
|
||||
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
if (this->buffer_pos_ < 4) {
|
||||
return; // Not enough data to process yet
|
||||
}
|
||||
if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
|
||||
this->cmd_active_ = false; // Set command state to inactive after response
|
||||
this->handle_ack_data_(buffer, this->buffer_pos_);
|
||||
this->buffer_pos_ = 0;
|
||||
} else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) &&
|
||||
(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
|
||||
this->handle_simple_mode_(buffer, this->buffer_pos_);
|
||||
this->buffer_pos_ = 0;
|
||||
} else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
|
||||
(this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
|
||||
this->handle_energy_mode_(buffer, this->buffer_pos_);
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,8 +460,9 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) {
|
||||
|
||||
// Resonable refresh rate for home assistant database size health
|
||||
const int32_t current_millis = App.get_loop_component_start_time();
|
||||
if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS)
|
||||
if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) {
|
||||
return;
|
||||
}
|
||||
this->last_periodic_millis = current_millis;
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_distance(this->get_distance_());
|
||||
@@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
}
|
||||
outbuf[index] = '\0';
|
||||
if (index > 1)
|
||||
if (index > 1) {
|
||||
this->set_distance_(strtol(outbuf, &endptr, 10));
|
||||
}
|
||||
|
||||
if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
|
||||
// Resonable refresh rate for home assistant database size health
|
||||
const int32_t current_millis = App.get_loop_component_start_time();
|
||||
if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS)
|
||||
if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) {
|
||||
return;
|
||||
}
|
||||
this->last_normal_periodic_millis = current_millis;
|
||||
for (auto &listener : this->listeners_)
|
||||
listener->on_distance(this->get_distance_());
|
||||
@@ -593,11 +594,12 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
uint32_t start_millis = millis();
|
||||
uint8_t error = 0;
|
||||
uint8_t ack_buffer[64];
|
||||
uint8_t cmd_buffer[64];
|
||||
uint8_t ack_buffer[MAX_LINE_LENGTH];
|
||||
uint8_t cmd_buffer[MAX_LINE_LENGTH];
|
||||
this->cmd_reply_.ack = false;
|
||||
if (frame.command != CMD_RESTART)
|
||||
this->set_cmd_active_(true); // Restart does not reply, thus no ack state required.
|
||||
if (frame.command != CMD_RESTART) {
|
||||
this->cmd_active_ = true;
|
||||
} // Restart does not reply, thus no ack state required
|
||||
uint8_t retry = 3;
|
||||
while (retry) {
|
||||
frame.length = 0;
|
||||
@@ -619,9 +621,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
|
||||
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
|
||||
frame.length += sizeof(frame.footer);
|
||||
for (uint16_t index = 0; index < frame.length; index++) {
|
||||
this->write_byte(cmd_buffer[index]);
|
||||
}
|
||||
this->write_array(cmd_buffer, frame.length);
|
||||
|
||||
error = 0;
|
||||
if (frame.command == CMD_RESTART) {
|
||||
@@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
|
||||
while (!this->cmd_reply_.ack) {
|
||||
while (this->available()) {
|
||||
this->readline_(read(), ack_buffer, sizeof(ack_buffer));
|
||||
this->readline_(this->read(), ack_buffer, sizeof(ack_buffer));
|
||||
}
|
||||
delay_microseconds_safe(1450);
|
||||
// Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
|
||||
@@ -641,10 +641,12 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this->cmd_reply_.ack)
|
||||
if (this->cmd_reply_.ack) {
|
||||
retry = 0;
|
||||
if (this->cmd_reply_.error > 0)
|
||||
}
|
||||
if (this->cmd_reply_.error > 0) {
|
||||
this->handle_cmd_error(error);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
@@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) {
|
||||
cmd_frame.data_length += sizeof(unknown_parm);
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
|
||||
if (this->send_cmd_from_array(cmd_frame) == 0)
|
||||
if (this->send_cmd_from_array(cmd_frame) == 0) {
|
||||
this->set_mode_(mode);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::get_firmware_version_() {
|
||||
@@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) {
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void LD2420Component::init_gate_config_numbers() {
|
||||
if (this->gate_timeout_number_ != nullptr)
|
||||
if (this->gate_timeout_number_ != nullptr) {
|
||||
this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout));
|
||||
if (this->gate_select_number_ != nullptr)
|
||||
}
|
||||
if (this->gate_select_number_ != nullptr) {
|
||||
this->gate_select_number_->publish_state(0);
|
||||
if (this->min_gate_distance_number_ != nullptr)
|
||||
}
|
||||
if (this->min_gate_distance_number_ != nullptr) {
|
||||
this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate));
|
||||
if (this->max_gate_distance_number_ != nullptr)
|
||||
}
|
||||
if (this->max_gate_distance_number_ != nullptr) {
|
||||
this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate));
|
||||
if (this->gate_move_sensitivity_factor_number_ != nullptr)
|
||||
}
|
||||
if (this->gate_move_sensitivity_factor_number_ != nullptr) {
|
||||
this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor);
|
||||
if (this->gate_still_sensitivity_factor_number_ != nullptr)
|
||||
}
|
||||
if (this->gate_still_sensitivity_factor_number_ != nullptr) {
|
||||
this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor);
|
||||
}
|
||||
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
|
||||
if (this->gate_still_threshold_numbers_[gate] != nullptr) {
|
||||
this->gate_still_threshold_numbers_[gate]->publish_state(
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
static const uint8_t TOTAL_GATES = 16;
|
||||
static const uint8_t CALIBRATE_SAMPLES = 64;
|
||||
static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
|
||||
static const uint8_t TOTAL_GATES = 16;
|
||||
|
||||
enum OpMode : uint8_t {
|
||||
OP_NORMAL_MODE = 1,
|
||||
@@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
|
||||
float gate_move_sensitivity_factor{0.5};
|
||||
float gate_still_sensitivity_factor{0.5};
|
||||
int32_t last_periodic_millis = millis();
|
||||
int32_t report_periodic_millis = millis();
|
||||
int32_t monitor_periodic_millis = millis();
|
||||
int32_t last_normal_periodic_millis = millis();
|
||||
int32_t last_periodic_millis{0};
|
||||
int32_t report_periodic_millis{0};
|
||||
int32_t monitor_periodic_millis{0};
|
||||
int32_t last_normal_periodic_millis{0};
|
||||
uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES];
|
||||
uint16_t gate_avg[TOTAL_GATES];
|
||||
uint16_t gate_peak[TOTAL_GATES];
|
||||
@@ -161,8 +162,6 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
void set_presence_(bool presence) { this->presence_ = presence; };
|
||||
uint16_t get_distance_() { return this->distance_; };
|
||||
void set_distance_(uint16_t distance) { this->distance_ = distance; };
|
||||
bool get_cmd_active_() { return this->cmd_active_; };
|
||||
void set_cmd_active_(bool active) { this->cmd_active_ = active; };
|
||||
void handle_simple_mode_(const uint8_t *inbuf, int len);
|
||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||
void handle_ack_data_(uint8_t *buffer, int len);
|
||||
@@ -181,12 +180,11 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16);
|
||||
#endif
|
||||
|
||||
uint32_t max_distance_gate_;
|
||||
uint32_t min_distance_gate_;
|
||||
uint16_t distance_{0};
|
||||
uint16_t system_mode_;
|
||||
uint16_t gate_energy_[TOTAL_GATES];
|
||||
uint16_t distance_{0};
|
||||
uint8_t config_checksum_{0};
|
||||
uint8_t buffer_pos_{0}; // where to resume processing/populating buffer
|
||||
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||
char firmware_ver_[8]{"v0.0.0"};
|
||||
bool cmd_active_{false};
|
||||
bool presence_{false};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const char *const TAG = "LD2420.number";
|
||||
static const char *const TAG = "ld2420.number";
|
||||
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
static const char *const TAG = "LD2420.select";
|
||||
static const char *const TAG = "ld2420.select";
|
||||
|
||||
void LD2420Select::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
static const char *const TAG = "LD2420.sensor";
|
||||
static const char *const TAG = "ld2420.sensor";
|
||||
|
||||
void LD2420Sensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "LD2420 Sensor:");
|
||||
ESP_LOGCONFIG(TAG, "Sensor:");
|
||||
LOG_SENSOR(" ", "Distance", this->distance_sensor_);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace esphome {
|
||||
namespace ld2420 {
|
||||
|
||||
static const char *const TAG = "LD2420.text_sensor";
|
||||
static const char *const TAG = "ld2420.text_sensor";
|
||||
|
||||
void LD2420TextSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "LD2420 TextSensor:");
|
||||
ESP_LOGCONFIG(TAG, "Text Sensor:");
|
||||
LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||
|
||||
// LibreTiny doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
WiFi.macAddress(mac);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace light {
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (state.supports_effects())
|
||||
root["effect"] = state.get_effect_name();
|
||||
|
||||
@@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
|
||||
root["brightness"] = uint8_t(values.get_brightness() * 255);
|
||||
|
||||
JsonObject color = root.createNestedObject("color");
|
||||
JsonObject color = root["color"].to<JsonObject>();
|
||||
if (values.get_color_mode() & ColorCapability::RGB) {
|
||||
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
|
||||
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
|
||||
@@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
}
|
||||
|
||||
void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) {
|
||||
if (root.containsKey("state")) {
|
||||
if (root["state"].is<const char *>()) {
|
||||
auto val = parse_on_off(root["state"]);
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
@@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("brightness")) {
|
||||
if (root["brightness"].is<uint8_t>()) {
|
||||
call.set_brightness(float(root["brightness"]) / 255.0f);
|
||||
}
|
||||
|
||||
if (root.containsKey("color")) {
|
||||
if (root["color"].is<JsonObject>()) {
|
||||
JsonObject color = root["color"];
|
||||
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
|
||||
float max_rgb = 0.0f;
|
||||
if (color.containsKey("r")) {
|
||||
if (color["r"].is<uint8_t>()) {
|
||||
float r = float(color["r"]) / 255.0f;
|
||||
max_rgb = fmaxf(max_rgb, r);
|
||||
call.set_red(r);
|
||||
}
|
||||
if (color.containsKey("g")) {
|
||||
if (color["g"].is<uint8_t>()) {
|
||||
float g = float(color["g"]) / 255.0f;
|
||||
max_rgb = fmaxf(max_rgb, g);
|
||||
call.set_green(g);
|
||||
}
|
||||
if (color.containsKey("b")) {
|
||||
if (color["b"].is<uint8_t>()) {
|
||||
float b = float(color["b"]) / 255.0f;
|
||||
max_rgb = fmaxf(max_rgb, b);
|
||||
call.set_blue(b);
|
||||
}
|
||||
if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) {
|
||||
if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) {
|
||||
call.set_color_brightness(max_rgb);
|
||||
}
|
||||
|
||||
if (color.containsKey("c")) {
|
||||
if (color["c"].is<uint8_t>()) {
|
||||
call.set_cold_white(float(color["c"]) / 255.0f);
|
||||
}
|
||||
if (color.containsKey("w")) {
|
||||
if (color["w"].is<uint8_t>()) {
|
||||
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
|
||||
// white channel in RGBWW.
|
||||
if (color.containsKey("c")) {
|
||||
if (color["c"].is<uint8_t>()) {
|
||||
call.set_warm_white(float(color["w"]) / 255.0f);
|
||||
} else {
|
||||
call.set_white(float(color["w"]) / 255.0f);
|
||||
@@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("white_value")) { // legacy API
|
||||
if (root["white_value"].is<uint8_t>()) { // legacy API
|
||||
call.set_white(float(root["white_value"]) / 255.0f);
|
||||
}
|
||||
|
||||
if (root.containsKey("color_temp")) {
|
||||
if (root["color_temp"].is<uint16_t>()) {
|
||||
call.set_color_temperature(float(root["color_temp"]));
|
||||
}
|
||||
}
|
||||
@@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
|
||||
LightJSONSchema::parse_color_json(state, call, root);
|
||||
|
||||
if (root.containsKey("flash")) {
|
||||
if (root["flash"].is<uint32_t>()) {
|
||||
auto length = uint32_t(float(root["flash"]) * 1000);
|
||||
call.set_flash_length(length);
|
||||
}
|
||||
|
||||
if (root.containsKey("transition")) {
|
||||
if (root["transition"].is<uint16_t>()) {
|
||||
auto length = uint32_t(float(root["transition"]) * 1000);
|
||||
call.set_transition_length(length);
|
||||
}
|
||||
|
||||
if (root.containsKey("effect")) {
|
||||
if (root["effect"].is<const char *>()) {
|
||||
const char *effect = root["effect"];
|
||||
call.set_effect(effect);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ def validate_local_no_higher_than_global(value):
|
||||
Logger = logger_ns.class_("Logger", cg.Component)
|
||||
LoggerMessageTrigger = logger_ns.class_(
|
||||
"LoggerMessageTrigger",
|
||||
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
|
||||
automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr),
|
||||
)
|
||||
|
||||
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
|
||||
@@ -368,7 +368,7 @@ async def to_code(config):
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.int_, "level"),
|
||||
(cg.uint8, "level"),
|
||||
(cg.const_char_ptr, "tag"),
|
||||
(cg.const_char_ptr, "message"),
|
||||
],
|
||||
|
||||
@@ -192,7 +192,7 @@ class WidgetType:
|
||||
|
||||
class NumberType(WidgetType):
|
||||
def get_max(self, config: dict):
|
||||
return int(config[CONF_MAX_VALUE] or 100)
|
||||
return int(config.get(CONF_MAX_VALUE, 100))
|
||||
|
||||
def get_min(self, config: dict):
|
||||
return int(config[CONF_MIN_VALUE] or 0)
|
||||
return int(config.get(CONF_MIN_VALUE, 0))
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import IntLiteral
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -29,9 +30,9 @@ from ..defines import (
|
||||
)
|
||||
from ..helpers import add_lv_use, lvgl_components_required
|
||||
from ..lv_validation import (
|
||||
angle,
|
||||
get_end_value,
|
||||
get_start_value,
|
||||
lv_angle,
|
||||
lv_bool,
|
||||
lv_color,
|
||||
lv_float,
|
||||
@@ -162,7 +163,7 @@ SCALE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
||||
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
||||
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
||||
cv.Optional(CONF_ROTATION): angle,
|
||||
cv.Optional(CONF_ROTATION): lv_angle,
|
||||
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||
}
|
||||
)
|
||||
@@ -187,7 +188,9 @@ class MeterType(WidgetType):
|
||||
for scale_conf in config.get(CONF_SCALES, ()):
|
||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||
if CONF_ROTATION in scale_conf:
|
||||
rotation = scale_conf[CONF_ROTATION] // 10
|
||||
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
|
||||
if isinstance(rotation, IntLiteral):
|
||||
rotation = int(str(rotation)) // 10
|
||||
with LocalVariable(
|
||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||
) as meter_var:
|
||||
@@ -205,21 +208,20 @@ class MeterType(WidgetType):
|
||||
var,
|
||||
meter_var,
|
||||
ticks[CONF_COUNT],
|
||||
ticks[CONF_WIDTH],
|
||||
ticks[CONF_LENGTH],
|
||||
await size.process(ticks[CONF_WIDTH]),
|
||||
await size.process(ticks[CONF_LENGTH]),
|
||||
color,
|
||||
)
|
||||
if CONF_MAJOR in ticks:
|
||||
major = ticks[CONF_MAJOR]
|
||||
color = await lv_color.process(major[CONF_COLOR])
|
||||
lv.meter_set_scale_major_ticks(
|
||||
var,
|
||||
meter_var,
|
||||
major[CONF_STRIDE],
|
||||
major[CONF_WIDTH],
|
||||
major[CONF_LENGTH],
|
||||
color,
|
||||
major[CONF_LABEL_GAP],
|
||||
await size.process(major[CONF_WIDTH]),
|
||||
await size.process(major[CONF_LENGTH]),
|
||||
await lv_color.process(major[CONF_COLOR]),
|
||||
await size.process(major[CONF_LABEL_GAP]),
|
||||
)
|
||||
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
||||
(t, v) = next(iter(indicator.items()))
|
||||
@@ -233,7 +235,11 @@ class MeterType(WidgetType):
|
||||
lv_assign(
|
||||
ivar,
|
||||
lv_expr.meter_add_needle_line(
|
||||
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
||||
var,
|
||||
meter_var,
|
||||
await size.process(v[CONF_WIDTH]),
|
||||
color,
|
||||
await size.process(v[CONF_R_MOD]),
|
||||
),
|
||||
)
|
||||
if t == CONF_ARC:
|
||||
@@ -241,7 +247,11 @@ class MeterType(WidgetType):
|
||||
lv_assign(
|
||||
ivar,
|
||||
lv_expr.meter_add_arc(
|
||||
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
||||
var,
|
||||
meter_var,
|
||||
await size.process(v[CONF_WIDTH]),
|
||||
color,
|
||||
await size.process(v[CONF_R_MOD]),
|
||||
),
|
||||
)
|
||||
if t == CONF_TICK_STYLE:
|
||||
@@ -257,7 +267,7 @@ class MeterType(WidgetType):
|
||||
color_start,
|
||||
color_end,
|
||||
v[CONF_LOCAL],
|
||||
v[CONF_WIDTH],
|
||||
await size.process(v[CONF_WIDTH]),
|
||||
),
|
||||
)
|
||||
if t == CONF_IMAGE:
|
||||
|
||||
@@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
|
||||
}
|
||||
|
||||
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>();
|
||||
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
|
||||
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
||||
supported_features.add("arm_away");
|
||||
|
||||
@@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
|
||||
}
|
||||
|
||||
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->binary_sensor_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
|
||||
@@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() {
|
||||
}
|
||||
|
||||
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
config.state_topic = false;
|
||||
if (!this->button_->get_device_class().empty())
|
||||
if (!this->button_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
std::string MQTTButtonComponent::component_type() const { return "button"; }
|
||||
|
||||
@@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() {
|
||||
std::string topic = "esphome/discover/";
|
||||
topic.append(App.get_name());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
this->publish_json(
|
||||
topic,
|
||||
[](JsonObject root) {
|
||||
@@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() {
|
||||
#endif
|
||||
},
|
||||
2, this->discovery_info_.retain);
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void MQTTClientComponent::dump_config() {
|
||||
@@ -191,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
|
||||
this->dns_resolve_error_ = false;
|
||||
this->dns_resolved_ = false;
|
||||
ip_addr_t addr;
|
||||
err_t err;
|
||||
{
|
||||
LwIPLock lock;
|
||||
#if USE_NETWORK_IPV6
|
||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
||||
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||
this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
||||
#else
|
||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4);
|
||||
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||
this, LWIP_DNS_ADDRTYPE_IPV4);
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
switch (err) {
|
||||
case ERR_OK: {
|
||||
// Got IP immediately
|
||||
|
||||
@@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
|
||||
using namespace esphome::climate;
|
||||
|
||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto traits = this->device_->get_traits();
|
||||
// current_temperature_topic
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
@@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// mode_state_topic
|
||||
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
|
||||
// modes
|
||||
JsonArray modes = root.createNestedArray(MQTT_MODES);
|
||||
JsonArray modes = root[MQTT_MODES].to<JsonArray>();
|
||||
// sort array for nice UI in HA
|
||||
if (traits.supports_mode(CLIMATE_MODE_AUTO))
|
||||
modes.add("auto");
|
||||
@@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// preset_mode_state_topic
|
||||
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
|
||||
// presets
|
||||
JsonArray presets = root.createNestedArray("preset_modes");
|
||||
JsonArray presets = root["preset_modes"].to<JsonArray>();
|
||||
if (traits.supports_preset(CLIMATE_PRESET_HOME))
|
||||
presets.add("home");
|
||||
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
|
||||
@@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// fan_mode_state_topic
|
||||
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
|
||||
// fan_modes
|
||||
JsonArray fan_modes = root.createNestedArray("fan_modes");
|
||||
JsonArray fan_modes = root["fan_modes"].to<JsonArray>();
|
||||
if (traits.supports_fan_mode(CLIMATE_FAN_ON))
|
||||
fan_modes.add("on");
|
||||
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
|
||||
@@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// swing_mode_state_topic
|
||||
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
|
||||
// swing_modes
|
||||
JsonArray swing_modes = root.createNestedArray("swing_modes");
|
||||
JsonArray swing_modes = root["swing_modes"].to<JsonArray>();
|
||||
if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
|
||||
swing_modes.add("off");
|
||||
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
|
||||
@@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
|
||||
config.state_topic = false;
|
||||
config.command_topic = false;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
void MQTTClimateComponent::setup() {
|
||||
auto traits = this->device_->get_traits();
|
||||
|
||||
@@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return global_mqtt_client->publish_json(
|
||||
this->get_discovery_topic_(discovery_info),
|
||||
[this](JsonObject root) {
|
||||
@@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
}
|
||||
std::string node_area = App.get_area();
|
||||
|
||||
JsonObject device_info = root.createNestedObject(MQTT_DEVICE);
|
||||
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||
const auto mac = get_mac_address();
|
||||
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
|
||||
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
|
||||
@@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
|
||||
},
|
||||
this->qos_, discovery_info.retain);
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
||||
|
||||
@@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->cover_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
|
||||
void MQTTDateComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->date_->make_call();
|
||||
if (root.containsKey("year")) {
|
||||
if (root["year"].is<uint16_t>()) {
|
||||
call.set_year(root["year"]);
|
||||
}
|
||||
if (root.containsKey("month")) {
|
||||
if (root["month"].is<uint8_t>()) {
|
||||
call.set_month(root["month"]);
|
||||
}
|
||||
if (root.containsKey("day")) {
|
||||
if (root["day"].is<uint8_t>()) {
|
||||
call.set_day(root["day"]);
|
||||
}
|
||||
call.perform();
|
||||
@@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
|
||||
}
|
||||
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["year"] = year;
|
||||
root["month"] = month;
|
||||
root["day"] = day;
|
||||
|
||||
@@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
|
||||
void MQTTDateTimeComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->datetime_->make_call();
|
||||
if (root.containsKey("year")) {
|
||||
if (root["year"].is<uint16_t>()) {
|
||||
call.set_year(root["year"]);
|
||||
}
|
||||
if (root.containsKey("month")) {
|
||||
if (root["month"].is<uint8_t>()) {
|
||||
call.set_month(root["month"]);
|
||||
}
|
||||
if (root.containsKey("day")) {
|
||||
if (root["day"].is<uint8_t>()) {
|
||||
call.set_day(root["day"]);
|
||||
}
|
||||
if (root.containsKey("hour")) {
|
||||
if (root["hour"].is<uint8_t>()) {
|
||||
call.set_hour(root["hour"]);
|
||||
}
|
||||
if (root.containsKey("minute")) {
|
||||
if (root["minute"].is<uint8_t>()) {
|
||||
call.set_minute(root["minute"]);
|
||||
}
|
||||
if (root.containsKey("second")) {
|
||||
if (root["second"].is<uint8_t>()) {
|
||||
call.set_second(root["second"]);
|
||||
}
|
||||
call.perform();
|
||||
@@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() {
|
||||
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
|
||||
uint8_t second) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["year"] = year;
|
||||
root["month"] = month;
|
||||
root["day"] = day;
|
||||
|
||||
@@ -16,7 +16,8 @@ using namespace esphome::event;
|
||||
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
|
||||
|
||||
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>();
|
||||
for (const auto &event_type : this->event_->get_event_types())
|
||||
event_types.add(event_type);
|
||||
|
||||
@@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
|
||||
}
|
||||
|
||||
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
||||
return this->publish_json(this->get_state_topic_(),
|
||||
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; });
|
||||
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[MQTT_EVENT_TYPE] = event_type;
|
||||
});
|
||||
}
|
||||
|
||||
std::string MQTTEventComponent::component_type() const { return "event"; }
|
||||
|
||||
@@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() {
|
||||
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
|
||||
|
||||
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
||||
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
||||
|
||||
@@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() {
|
||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||
|
||||
bool MQTTJSONLightComponent::publish_state_() {
|
||||
return this->publish_json(this->get_state_topic_(),
|
||||
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); });
|
||||
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
LightJSONSchema::dump_json(*this->state_, root);
|
||||
});
|
||||
}
|
||||
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
|
||||
|
||||
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["schema"] = "json";
|
||||
auto traits = this->state_->get_traits();
|
||||
|
||||
root[MQTT_COLOR_MODE] = true;
|
||||
JsonArray color_modes = root.createNestedArray("supported_color_modes");
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray color_modes = root["supported_color_modes"].to<JsonArray>();
|
||||
if (traits.supports_color_mode(ColorMode::ON_OFF))
|
||||
color_modes.add("onoff");
|
||||
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
|
||||
@@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
||||
|
||||
if (this->state_->supports_effects()) {
|
||||
root["effect"] = true;
|
||||
JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST);
|
||||
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
||||
for (auto *effect : this->state_->get_effects())
|
||||
effect_list.add(effect->get_name());
|
||||
effect_list.add("None");
|
||||
|
||||
@@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
|
||||
std::string MQTTLockComponent::component_type() const { return "lock"; }
|
||||
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
||||
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (this->lock_->traits.get_assumed_state())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (this->lock_->traits.get_assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
}
|
||||
if (this->lock_->traits.get_supports_open())
|
||||
root[MQTT_PAYLOAD_OPEN] = "OPEN";
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_
|
||||
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
const auto &traits = number_->traits;
|
||||
// https://www.home-assistant.io/integrations/number.mqtt/
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[MQTT_MIN] = traits.get_min_value();
|
||||
root[MQTT_MAX] = traits.get_max_value();
|
||||
root[MQTT_STEP] = traits.get_step();
|
||||
|
||||
@@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_
|
||||
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
const auto &traits = select_->traits;
|
||||
// https://www.home-assistant.io/integrations/select.mqtt/
|
||||
JsonArray options = root.createNestedArray(MQTT_OPTIONS);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray options = root[MQTT_OPTIONS].to<JsonArray>();
|
||||
for (const auto &option : traits.get_options())
|
||||
options.add(option);
|
||||
|
||||
|
||||
@@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire
|
||||
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (!this->sensor_->get_device_class().empty())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->sensor_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||
}
|
||||
|
||||
if (!this->sensor_->get_unit_of_measurement().empty())
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
|
||||
|
||||
@@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() {
|
||||
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
|
||||
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
|
||||
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (this->switch_->assumed_state())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (this->switch_->assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
}
|
||||
}
|
||||
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; }
|
||||
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||
|
||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
switch (this->text_->traits.get_mode()) {
|
||||
case TEXT_MODE_TEXT:
|
||||
root[MQTT_MODE] = "text";
|
||||
|
||||
@@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
|
||||
|
||||
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
||||
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (!this->sensor_->get_device_class().empty())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->sensor_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||
}
|
||||
config.command_topic = false;
|
||||
}
|
||||
void MQTTTextSensor::setup() {
|
||||
|
||||
@@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
|
||||
void MQTTTimeComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->time_->make_call();
|
||||
if (root.containsKey("hour")) {
|
||||
if (root["hour"].is<uint8_t>()) {
|
||||
call.set_hour(root["hour"]);
|
||||
}
|
||||
if (root.containsKey("minute")) {
|
||||
if (root["minute"].is<uint8_t>()) {
|
||||
call.set_minute(root["minute"]);
|
||||
}
|
||||
if (root.containsKey("second")) {
|
||||
if (root["second"].is<uint8_t>()) {
|
||||
call.set_second(root["second"]);
|
||||
}
|
||||
call.perform();
|
||||
@@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
|
||||
}
|
||||
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["hour"] = hour;
|
||||
root["minute"] = minute;
|
||||
root["second"] = second;
|
||||
|
||||
@@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() {
|
||||
}
|
||||
|
||||
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["schema"] = "json";
|
||||
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
|
||||
}
|
||||
|
||||
@@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (!this->valve_->get_device_class().empty())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->valve_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
|
||||
}
|
||||
|
||||
auto traits = this->valve_->get_traits();
|
||||
if (traits.get_is_assumed_state()) {
|
||||
|
||||
@@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) {
|
||||
|
||||
// map 16 bit humidity value into range [-6%, 118%]
|
||||
float const humidity_partial = double(humidity) / (1 << 16);
|
||||
float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0);
|
||||
float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial);
|
||||
float const compensated_humidity_percentage =
|
||||
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
|
||||
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_REQUEST_HEADERS
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
|
||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||
from esphome.components.image import (
|
||||
CONF_INVERT_ALPHA,
|
||||
@@ -11,6 +11,7 @@ from esphome.components.image import (
|
||||
Image_,
|
||||
get_image_type_enum,
|
||||
get_transparency_enum,
|
||||
validate_settings,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
host=cv.Version(0, 0, 0),
|
||||
),
|
||||
validate_settings,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -213,6 +215,7 @@ async def to_code(config):
|
||||
get_image_type_enum(config[CONF_TYPE]),
|
||||
transparent,
|
||||
config[CONF_BUFFER_SIZE],
|
||||
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
|
||||
@@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) {
|
||||
}
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||
image::Transparency transparency, uint32_t download_buffer_size)
|
||||
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
buffer_(nullptr),
|
||||
download_buffer_(download_buffer_size),
|
||||
download_buffer_initial_size_(download_buffer_size),
|
||||
format_(format),
|
||||
fixed_width_(width),
|
||||
fixed_height_(height) {
|
||||
fixed_height_(height),
|
||||
is_big_endian_(is_big_endian) {
|
||||
this->set_url(url);
|
||||
}
|
||||
|
||||
@@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
@@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
case ImageType::IMAGE_TYPE_RGB565: {
|
||||
this->map_chroma_key(color);
|
||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
if (this->is_big_endian_) {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
} else {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
}
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 2] = color.w;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent,
|
||||
* @param buffer_size Size of the buffer used to download the image.
|
||||
*/
|
||||
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||
image::Transparency transparency, uint32_t buffer_size);
|
||||
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
@@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent,
|
||||
const int fixed_width_;
|
||||
/** height requested on configuration, or 0 if non specified. */
|
||||
const int fixed_height_;
|
||||
/**
|
||||
* Whether the image is stored in big-endian format.
|
||||
* This is used to determine how to store 16 bit colors in the buffer.
|
||||
*/
|
||||
bool is_big_endian_;
|
||||
/**
|
||||
* Actual width of the current image. If fixed_width_ is specified,
|
||||
* this will be equal to it; otherwise it will be set once the decoding
|
||||
|
||||
@@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) {
|
||||
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
|
||||
this->state = state < 0.003 && this->zero_means_zero_
|
||||
? 0.0
|
||||
: clamp(lerp(state, min_value_, max_value_), min_value_, max_value_);
|
||||
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||
this->has_state_ = true;
|
||||
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ void Mutex::unlock() {}
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
|
||||
// RP2040 doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
#ifdef USE_WIFI
|
||||
WiFi.macAddress(mac);
|
||||
|
||||
@@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
|
||||
value = clamp(value, -1.0f, 1.0f);
|
||||
float level;
|
||||
if (value < 0.0) {
|
||||
level = lerp(-value, this->idle_level_, this->min_level_);
|
||||
level = std::lerp(this->idle_level_, this->min_level_, -value);
|
||||
} else {
|
||||
level = lerp(value, this->idle_level_, this->max_level_);
|
||||
level = std::lerp(this->idle_level_, this->max_level_, value);
|
||||
}
|
||||
this->output_->set_level(level);
|
||||
this->current_value_ = value;
|
||||
|
||||
@@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() {
|
||||
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
|
||||
this->delete_tasks_();
|
||||
if (this->hard_stop_) {
|
||||
// Stop command was sent, so immediately end of the playback
|
||||
// Stop command was sent, so immediately end the playback
|
||||
this->speaker_->stop();
|
||||
this->hard_stop_ = false;
|
||||
} else {
|
||||
@@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() {
|
||||
}
|
||||
}
|
||||
this->is_playing_ = false;
|
||||
return AudioPipelineState::STOPPED;
|
||||
if (!this->speaker_->is_running()) {
|
||||
return AudioPipelineState::STOPPED;
|
||||
} else {
|
||||
this->is_finishing_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->pause_state_) {
|
||||
return AudioPipelineState::PAUSED;
|
||||
}
|
||||
|
||||
if (this->is_finishing_) {
|
||||
if (!this->speaker_->is_running()) {
|
||||
this->is_finishing_ = false;
|
||||
} else {
|
||||
return AudioPipelineState::PLAYING;
|
||||
}
|
||||
}
|
||||
|
||||
if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
|
||||
// No tasks are running, so the pipeline is stopped.
|
||||
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
|
||||
|
||||
@@ -114,6 +114,7 @@ class AudioPipeline {
|
||||
|
||||
bool hard_stop_{false};
|
||||
bool is_playing_{false};
|
||||
bool is_finishing_{false};
|
||||
bool pause_state_{false};
|
||||
bool task_stack_in_psram_;
|
||||
|
||||
|
||||
@@ -151,8 +151,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
|
||||
if sub is not None:
|
||||
item[k] = sub
|
||||
for old, new in replace_keys:
|
||||
item[new] = merge_config(item.get(old), item.get(new))
|
||||
del item[old]
|
||||
if str(new) == str(old):
|
||||
item[new] = item[old]
|
||||
else:
|
||||
item[new] = merge_config(item.get(old), item.get(new))
|
||||
del item[old]
|
||||
elif isinstance(item, str):
|
||||
sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing)
|
||||
if isinstance(sub, JinjaStr) or sub != item:
|
||||
|
||||
@@ -35,6 +35,27 @@ void VoiceAssistant::setup() {
|
||||
temp_ring_buffer->write((void *) data.data(), data.size());
|
||||
}
|
||||
});
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->media_player_->add_on_state_callback([this]() {
|
||||
switch (this->media_player_->state) {
|
||||
case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING:
|
||||
if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) {
|
||||
// State changed to announcing after receiving the url
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::PLAYING;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING) {
|
||||
// No longer announcing the TTS response
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::FINISHED;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
@@ -223,6 +244,13 @@ void VoiceAssistant::loop() {
|
||||
msg.wake_word_phrase = this->wake_word_;
|
||||
this->wake_word_ = "";
|
||||
|
||||
// Reset media player state tracking
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
|
||||
ESP_LOGW(TAG, "Could not request start");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start");
|
||||
@@ -314,17 +342,10 @@ void VoiceAssistant::loop() {
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING);
|
||||
playing = (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING);
|
||||
|
||||
if (playing && this->media_player_wait_for_announcement_start_) {
|
||||
// Announcement has started playing, wait for it to finish
|
||||
this->media_player_wait_for_announcement_start_ = false;
|
||||
this->media_player_wait_for_announcement_end_ = true;
|
||||
}
|
||||
|
||||
if (!playing && this->media_player_wait_for_announcement_end_) {
|
||||
// Announcement has finished playing
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
||||
this->cancel_timeout("playing");
|
||||
ESP_LOGD(TAG, "Announcement finished playing");
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
@@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() {
|
||||
break;
|
||||
case State::AWAITING_RESPONSE:
|
||||
this->signal_stop_();
|
||||
// Fallthrough intended to stop a streaming TTS announcement that has potentially started
|
||||
break;
|
||||
case State::STREAMING_RESPONSE:
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
// Stop any ongoing media player announcement
|
||||
@@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() {
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
}
|
||||
if (this->started_streaming_tts_) {
|
||||
// Haven't reached the TTS_END stage, so send the stop signal to HA.
|
||||
this->signal_stop_();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case State::RESPONSE_FINISHED:
|
||||
@@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
if (this->media_player_ != nullptr) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||
|
||||
this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform();
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
this->started_streaming_tts_ = true;
|
||||
this->start_playback_timeout_();
|
||||
|
||||
tts_url_for_trigger = this->tts_response_url_;
|
||||
this->tts_response_url_.clear(); // Reset streaming URL
|
||||
this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,18 +741,22 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
this->defer([this, url]() {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||
|
||||
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
// Start the playback timeout, as the media player state isn't immediately updated
|
||||
this->start_playback_timeout_();
|
||||
}
|
||||
this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
|
||||
#endif
|
||||
this->tts_end_trigger_->trigger(url);
|
||||
});
|
||||
State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE;
|
||||
this->set_state_(new_state, new_state);
|
||||
if (new_state != this->state_) {
|
||||
// Don't needlessly change the state. The intent progress stage may have already changed the state to streaming
|
||||
// response.
|
||||
this->set_state_(new_state, new_state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_RUN_END: {
|
||||
@@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->tts_start_trigger_->trigger(msg.text);
|
||||
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||
|
||||
if (!msg.preannounce_media_id.empty()) {
|
||||
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform();
|
||||
}
|
||||
@@ -886,9 +921,6 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
||||
.perform();
|
||||
this->continue_conversation_ = msg.start_conversation;
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
// Start the playback timeout, as the media player state isn't immediately updated
|
||||
this->start_playback_timeout_();
|
||||
|
||||
if (this->continuous_) {
|
||||
|
||||
@@ -90,6 +90,15 @@ struct Configuration {
|
||||
uint32_t max_active_wake_words;
|
||||
};
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
enum class MediaPlayerResponseState {
|
||||
IDLE,
|
||||
URL_SENT,
|
||||
PLAYING,
|
||||
FINISHED,
|
||||
};
|
||||
#endif
|
||||
|
||||
class VoiceAssistant : public Component {
|
||||
public:
|
||||
VoiceAssistant();
|
||||
@@ -272,8 +281,8 @@ class VoiceAssistant : public Component {
|
||||
media_player::MediaPlayer *media_player_{nullptr};
|
||||
std::string tts_response_url_{""};
|
||||
bool started_streaming_tts_{false};
|
||||
bool media_player_wait_for_announcement_start_{false};
|
||||
bool media_player_wait_for_announcement_end_{false};
|
||||
|
||||
MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE};
|
||||
#endif
|
||||
|
||||
bool local_output_{false};
|
||||
|
||||
@@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_ota_removed(config: ConfigType) -> ConfigType:
|
||||
# Only raise error if OTA is explicitly enabled (True)
|
||||
# If it's False or not specified, we can safely ignore it
|
||||
if config.get(CONF_OTA):
|
||||
def validate_ota(config: ConfigType) -> ConfigType:
|
||||
# The OTA option only accepts False to explicitly disable OTA for web_server
|
||||
# IMPORTANT: Setting ota: false ONLY affects the web_server component
|
||||
# The captive_portal component will still be able to perform OTA updates
|
||||
if CONF_OTA in config and config[CONF_OTA] is not False:
|
||||
raise cv.Invalid(
|
||||
f"The '{CONF_OTA}' option has been removed from 'web_server'. "
|
||||
f"Please use the new OTA platform structure instead:\n\n"
|
||||
f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
|
||||
f"To enable OTA, please use the new OTA platform structure instead:\n\n"
|
||||
f"ota:\n"
|
||||
f" - platform: web_server\n\n"
|
||||
f"See https://esphome.io/components/ota for more information."
|
||||
@@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
web_server_base.WebServerBase
|
||||
),
|
||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OTA, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OTA): cv.boolean,
|
||||
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
||||
@@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
default_url,
|
||||
validate_local,
|
||||
validate_sorting_groups,
|
||||
validate_ota_removed,
|
||||
validate_ota,
|
||||
)
|
||||
|
||||
|
||||
@@ -288,7 +289,11 @@ async def to_code(config):
|
||||
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||
# OTA is now handled by the web_server OTA platform
|
||||
# The CONF_OTA option is kept only for backwards compatibility validation
|
||||
# The CONF_OTA option is kept to allow explicitly disabling OTA for web_server
|
||||
# IMPORTANT: This ONLY affects the web_server component, NOT captive_portal
|
||||
# Captive portal will still be able to perform OTA updates even when this is set
|
||||
if config.get(CONF_OTA) is False:
|
||||
cg.add_define("USE_WEBSERVER_OTA_DISABLED")
|
||||
cg.add(var.set_expose_log(config[CONF_LOG]))
|
||||
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
||||
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
#include "esphome/components/captive_portal/captive_portal.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#ifdef USE_ESP8266
|
||||
#include <Updater.h>
|
||||
@@ -25,7 +29,22 @@ class OTARequestHandler : public AsyncWebHandler {
|
||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||
bool final) override;
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
return request->url() == "/update" && request->method() == HTTP_POST;
|
||||
// Check if this is an OTA update request
|
||||
bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST;
|
||||
|
||||
#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
|
||||
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
|
||||
// Captive portal can still perform OTA updates - check if request is from active captive portal
|
||||
// Note: global_captive_portal is the standard way components communicate in ESPHome
|
||||
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
|
||||
captive_portal::global_captive_portal->is_active();
|
||||
#elif defined(USE_WEBSERVER_OTA_DISABLED)
|
||||
// OTA disabled for web_server and no captive portal compiled in
|
||||
return false;
|
||||
#else
|
||||
// OTA enabled for web_server
|
||||
return is_ota_request;
|
||||
#endif
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
@@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
||||
|
||||
// Finalize
|
||||
if (final) {
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len,
|
||||
this->ota_read_length_, request->contentLength());
|
||||
|
||||
// For Arduino framework, the Update library tracks expected size from firmware header
|
||||
|
||||
@@ -268,10 +268,10 @@ std::string WebServer::get_config_json() {
|
||||
return json::build_json([this](JsonObject root) {
|
||||
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
||||
root["comment"] = App.get_comment();
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
root["ota"] = true; // web_server OTA platform is configured
|
||||
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
||||
root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||
#else
|
||||
root["ota"] = false;
|
||||
root["ota"] = true;
|
||||
#endif
|
||||
root["log"] = this->expose_log_;
|
||||
root["lang"] = "en";
|
||||
@@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
|
||||
|
||||
light::LightJSONSchema::dump_json(*obj, root);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("effects");
|
||||
JsonArray opt = root["effects"].to<JsonArray>();
|
||||
opt.add("None");
|
||||
for (auto const &option : obj->get_effects()) {
|
||||
opt.add(option->get_name());
|
||||
@@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
|
||||
return json::build_json([this, obj, value, start_config](JsonObject root) {
|
||||
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("option");
|
||||
JsonArray opt = root["option"].to<JsonArray>();
|
||||
for (auto &option : obj->traits.get_options()) {
|
||||
opt.add(option);
|
||||
}
|
||||
@@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s
|
||||
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
|
||||
}
|
||||
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
|
||||
const auto traits = obj->get_traits();
|
||||
@@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
char buf[16];
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("modes");
|
||||
JsonArray opt = root["modes"].to<JsonArray>();
|
||||
for (climate::ClimateMode m : traits.get_supported_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
JsonArray opt = root.createNestedArray("fan_modes");
|
||||
JsonArray opt = root["fan_modes"].to<JsonArray>();
|
||||
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
||||
}
|
||||
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
JsonArray opt = root.createNestedArray("custom_fan_modes");
|
||||
JsonArray opt = root["custom_fan_modes"].to<JsonArray>();
|
||||
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
|
||||
opt.add(custom_fan_mode);
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
JsonArray opt = root.createNestedArray("swing_modes");
|
||||
JsonArray opt = root["swing_modes"].to<JsonArray>();
|
||||
for (auto swing_mode : traits.get_supported_swing_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
||||
}
|
||||
if (traits.get_supports_presets() && obj->preset.has_value()) {
|
||||
JsonArray opt = root.createNestedArray("presets");
|
||||
JsonArray opt = root["presets"].to<JsonArray>();
|
||||
for (climate::ClimatePreset m : traits.get_supported_presets())
|
||||
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
|
||||
JsonArray opt = root.createNestedArray("custom_presets");
|
||||
JsonArray opt = root["custom_presets"].to<JsonArray>();
|
||||
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
||||
opt.add(custom_preset);
|
||||
}
|
||||
@@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
root["state"] = root["target_temperature"];
|
||||
}
|
||||
});
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1618,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; }
|
||||
static std::string get_event_type(event::Event *event) {
|
||||
return (event && event->last_event_type) ? *event->last_event_type : "";
|
||||
}
|
||||
|
||||
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
||||
auto *event = static_cast<event::Event *>(source);
|
||||
@@ -1635,7 +1639,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
|
||||
root["event_type"] = event_type;
|
||||
}
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray event_types = root.createNestedArray("event_types");
|
||||
JsonArray event_types = root["event_types"].to<JsonArray>();
|
||||
for (auto const &event_type : obj->get_event_types()) {
|
||||
event_types.add(event_type);
|
||||
}
|
||||
@@ -1682,6 +1686,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so
|
||||
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
||||
root["value"] = obj->update_info.latest_version;
|
||||
@@ -1707,6 +1712,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
});
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -192,7 +192,9 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
|
||||
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
|
||||
"REST API documentation.</p>"));
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
|
||||
// Show OTA form only if web_server OTA is not explicitly disabled
|
||||
// Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
||||
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
||||
#endif
|
||||
|
||||
@@ -40,4 +40,4 @@ async def to_code(config):
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
|
||||
|
||||
@@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
for (auto &group : ws->sorting_groups_) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
message = json::build_json([group](JsonObject root) {
|
||||
root["name"] = group.second.name;
|
||||
root["sorting_weight"] = group.second.weight;
|
||||
});
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
// a (very) large number of these should be able to be queued initially without defer
|
||||
// since the only thing in the send buffer at this point is the initial ping/config
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
#include "lwip/priv/tcpip_priv.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -295,25 +291,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
}
|
||||
|
||||
if (!manual_ip.has_value()) {
|
||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||
// https://github.com/esphome/issues/issues/6591
|
||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
LOCK_TCPIP_CORE();
|
||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||
// https://github.com/esphome/issues/issues/6591
|
||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||
{
|
||||
LwIPLock lock;
|
||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||
// https://github.com/esphome/issues/issues/2299
|
||||
sntp_servermode_dhcp(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||
// https://github.com/esphome/issues/issues/2299
|
||||
sntp_servermode_dhcp(false);
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
UNLOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
|
||||
// No manual IP is set; use DHCP client
|
||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/time.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <esp_wireguard.h>
|
||||
#include <esp_wireguard_err.h>
|
||||
@@ -42,7 +43,10 @@ void Wireguard::setup() {
|
||||
|
||||
this->publish_enabled_state();
|
||||
|
||||
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
|
||||
{
|
||||
LwIPLock lock;
|
||||
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
|
||||
}
|
||||
|
||||
if (this->wg_initialized_ == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Initialized");
|
||||
@@ -249,7 +253,10 @@ void Wireguard::start_connection_() {
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Starting connection");
|
||||
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
|
||||
{
|
||||
LwIPLock lock;
|
||||
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
|
||||
}
|
||||
|
||||
if (this->wg_connected_ == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Connection started");
|
||||
@@ -280,7 +287,10 @@ void Wireguard::start_connection_() {
|
||||
void Wireguard::stop_connection_() {
|
||||
if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) {
|
||||
ESP_LOGD(TAG, "Stopping connection");
|
||||
esp_wireguard_disconnect(&(this->wg_ctx_));
|
||||
{
|
||||
LwIPLock lock;
|
||||
esp_wireguard_disconnect(&(this->wg_ctx_));
|
||||
}
|
||||
this->wg_connected_ = ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.7.0b2"
|
||||
__version__ = "2025.7.2"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -309,6 +309,12 @@ void Application::disable_component_loop_(Component *component) {
|
||||
if (this->in_loop_ && i == this->current_loop_index_) {
|
||||
// Decrement so we'll process the swapped component next
|
||||
this->current_loop_index_--;
|
||||
// Update the loop start time to current time so the swapped component
|
||||
// gets correct timing instead of inheriting stale timing.
|
||||
// This prevents integer underflow in timing calculations by ensuring
|
||||
// the swapped component starts with a fresh timing reference, avoiding
|
||||
// errors caused by stale or wrapped timing values.
|
||||
this->loop_component_start_time_ = millis();
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -158,14 +158,14 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
void play_complex(Ts... x) override {
|
||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||
this->num_running_++;
|
||||
this->set_timeout(this->delay_.value(x...), f);
|
||||
this->set_timeout("delay", this->delay_.value(x...), f);
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void play(Ts... x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
void stop() override { this->cancel_timeout(""); }
|
||||
void stop() override { this->cancel_timeout("delay"); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class LambdaAction : public Action<Ts...> {
|
||||
|
||||
@@ -138,7 +138,7 @@ void Component::call_dump_config() {
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg);
|
||||
ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
|
||||
return false;
|
||||
}
|
||||
void Component::mark_failed() {
|
||||
ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source());
|
||||
ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source());
|
||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||
this->component_state_ |= COMPONENT_STATE_FAILED;
|
||||
this->status_set_error();
|
||||
@@ -229,7 +229,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
|
||||
}
|
||||
void Component::reset_to_construction_state() {
|
||||
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, "%s is being reset to construction state", this->get_component_source());
|
||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
||||
// Clear error status when resetting
|
||||
@@ -252,10 +252,10 @@ void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_timeout(this, name, 0, std::move(f));
|
||||
}
|
||||
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_timeout(this, "", timeout, std::move(f));
|
||||
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
|
||||
}
|
||||
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_interval(this, "", interval, std::move(f));
|
||||
App.scheduler.set_interval(this, static_cast<const char *>(nullptr), interval, std::move(f));
|
||||
}
|
||||
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
|
||||
float backoff_increase_factor) { // NOLINT
|
||||
@@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
|
||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||
bool Component::is_ready() const {
|
||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||
}
|
||||
bool Component::can_proceed() { return true; }
|
||||
@@ -275,14 +276,14 @@ void Component::status_set_warning(const char *message) {
|
||||
return;
|
||||
this->component_state_ |= STATUS_LED_WARNING;
|
||||
App.app_state_ |= STATUS_LED_WARNING;
|
||||
ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message);
|
||||
ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message);
|
||||
}
|
||||
void Component::status_set_error(const char *message) {
|
||||
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
||||
return;
|
||||
this->component_state_ |= STATUS_LED_ERROR;
|
||||
App.app_state_ |= STATUS_LED_ERROR;
|
||||
ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
|
||||
ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message);
|
||||
if (strcmp(message, "unspecified") != 0) {
|
||||
// Lazy allocate the error messages vector if needed
|
||||
if (!component_error_messages) {
|
||||
@@ -303,13 +304,13 @@ void Component::status_clear_warning() {
|
||||
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
||||
return;
|
||||
this->component_state_ &= ~STATUS_LED_WARNING;
|
||||
ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source());
|
||||
ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source());
|
||||
}
|
||||
void Component::status_clear_error() {
|
||||
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
|
||||
return;
|
||||
this->component_state_ &= ~STATUS_LED_ERROR;
|
||||
ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source());
|
||||
ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source());
|
||||
}
|
||||
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
|
||||
this->status_set_warning();
|
||||
@@ -403,7 +404,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
||||
}
|
||||
if (should_warn) {
|
||||
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, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
|
||||
ESP_LOGW(TAG, "Components should block for at most 30 ms");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32)
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
@@ -78,4 +78,4 @@ template<class T, uint8_t SIZE> class EventPool {
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#endif // defined(USE_ESP32)
|
||||
|
||||
@@ -683,6 +683,23 @@ class InterruptLock {
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
|
||||
*
|
||||
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled.
|
||||
* It ensures thread-safe access to lwIP APIs.
|
||||
*
|
||||
* @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp
|
||||
*/
|
||||
class LwIPLock {
|
||||
public:
|
||||
LwIPLock();
|
||||
~LwIPLock();
|
||||
|
||||
// Delete copy constructor and copy assignment operator to prevent accidental copying
|
||||
LwIPLock(const LwIPLock &) = delete;
|
||||
LwIPLock &operator=(const LwIPLock &) = delete;
|
||||
};
|
||||
|
||||
/** Helper class to request `loop()` to be called as fast as possible.
|
||||
*
|
||||
* Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher
|
||||
@@ -783,7 +800,7 @@ template<class T> class RAMAllocator {
|
||||
T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
|
||||
|
||||
T *reallocate(T *p, size_t n, size_t manual_size) {
|
||||
size_t size = n * sizeof(T);
|
||||
size_t size = n * manual_size;
|
||||
T *ptr = nullptr;
|
||||
#ifdef USE_ESP32
|
||||
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32)
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Lock-free queue for single-producer single-consumer scenarios.
|
||||
@@ -148,4 +143,4 @@ template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQu
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#endif // defined(USE_ESP32)
|
||||
|
||||
@@ -446,7 +446,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
|
||||
// Helper to cancel items by name - must be called with lock held
|
||||
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
|
||||
// Early return if name is invalid - no items to cancel
|
||||
if (name_cstr == nullptr || name_cstr[0] == '\0') {
|
||||
if (name_cstr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,16 +114,17 @@ class Scheduler {
|
||||
name_is_dynamic = false;
|
||||
}
|
||||
|
||||
if (!name || !name[0]) {
|
||||
if (!name) {
|
||||
// nullptr case - no name provided
|
||||
name_.static_name = nullptr;
|
||||
} else if (make_copy) {
|
||||
// Make a copy for dynamic strings
|
||||
// Make a copy for dynamic strings (including empty strings)
|
||||
size_t len = strlen(name);
|
||||
name_.dynamic_name = new char[len + 1];
|
||||
memcpy(name_.dynamic_name, name, len + 1);
|
||||
name_is_dynamic = true;
|
||||
} else {
|
||||
// Use static string directly
|
||||
// Use static string directly (including empty strings)
|
||||
name_.static_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ.setdefault(
|
||||
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
|
||||
)
|
||||
# Suppress Python syntax warnings from third-party scripts during compilation
|
||||
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
|
||||
cmd = ["platformio"] + list(args)
|
||||
|
||||
if not CORE.verbose:
|
||||
|
||||
@@ -147,6 +147,13 @@ class RedirectText:
|
||||
continue
|
||||
|
||||
self._write_color_replace(line)
|
||||
# Check for flash size error and provide helpful guidance
|
||||
if (
|
||||
"Error: The program size" in line
|
||||
and "is greater than maximum allowed" in line
|
||||
and (help_msg := get_esp32_arduino_flash_error_help())
|
||||
):
|
||||
self._write_color_replace(help_msg)
|
||||
else:
|
||||
self._write_color_replace(s)
|
||||
|
||||
@@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]:
|
||||
|
||||
result.sort(key=lambda x: x.path)
|
||||
return result
|
||||
|
||||
|
||||
def get_esp32_arduino_flash_error_help() -> str | None:
|
||||
"""Returns helpful message when ESP32 with Arduino runs out of flash space."""
|
||||
from esphome.core import CORE
|
||||
|
||||
if not (CORE.is_esp32 and CORE.using_arduino):
|
||||
return None
|
||||
|
||||
from esphome.log import AnsiFore, color
|
||||
|
||||
return (
|
||||
"\n"
|
||||
+ color(
|
||||
AnsiFore.YELLOW,
|
||||
"💡 TIP: Your ESP32 with Arduino framework has run out of flash space.\n",
|
||||
)
|
||||
+ "\n"
|
||||
+ "To fix this, switch to the ESP-IDF framework which is more memory efficient:\n"
|
||||
+ "\n"
|
||||
+ "1. In your YAML configuration, modify the framework section:\n"
|
||||
+ "\n"
|
||||
+ " esp32:\n"
|
||||
+ " framework:\n"
|
||||
+ " type: esp-idf\n"
|
||||
+ "\n"
|
||||
+ "2. Clean build files and compile again\n"
|
||||
+ "\n"
|
||||
+ "Note: ESP-IDF uses less flash space and provides better performance.\n"
|
||||
+ "Some Arduino-specific libraries may need alternatives.\n\n"
|
||||
)
|
||||
|
||||
@@ -162,6 +162,9 @@ def get_ini_content():
|
||||
# Sort to avoid changing build unflags order
|
||||
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
||||
|
||||
# Add extra script for C++ flags
|
||||
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||
|
||||
content = "[platformio]\n"
|
||||
content += f"description = ESPHome {__version__}\n"
|
||||
|
||||
@@ -222,6 +225,9 @@ def write_platformio_project():
|
||||
write_gitignore()
|
||||
write_platformio_ini(content)
|
||||
|
||||
# Write extra script for C++ specific flags
|
||||
write_cxx_flags_script()
|
||||
|
||||
|
||||
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
|
||||
#pragma once
|
||||
@@ -394,3 +400,20 @@ def write_gitignore():
|
||||
if not os.path.isfile(path):
|
||||
with open(file=path, mode="w", encoding="utf-8") as f:
|
||||
f.write(GITIGNORE_CONTENT)
|
||||
|
||||
|
||||
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
|
||||
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
|
||||
Import("env")
|
||||
|
||||
# Add C++ specific flags
|
||||
"""
|
||||
|
||||
|
||||
def write_cxx_flags_script() -> None:
|
||||
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||
contents = CXX_FLAGS_FILE_CONTENTS
|
||||
if not CORE.is_host:
|
||||
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||
contents += "\n"
|
||||
write_file_if_changed(path, contents)
|
||||
|
||||
@@ -35,7 +35,7 @@ build_flags =
|
||||
lib_deps =
|
||||
esphome/noise-c@0.1.10 ; api
|
||||
improv/Improv@1.2.4 ; improv_serial / esp32_improv
|
||||
bblanchon/ArduinoJson@6.18.5 ; json
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
@@ -138,7 +138,7 @@ lib_deps =
|
||||
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
|
||||
Update ; ota,web_server_base (Arduino built-in)
|
||||
${common:arduino.lib_deps}
|
||||
ESP32Async/AsyncTCP@3.4.4 ; async_tcp
|
||||
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
|
||||
NetworkClientSecure ; http_request,nextion (Arduino built-in)
|
||||
HTTPClient ; http_request,nextion (Arduino built-in)
|
||||
ESPmDNS ; mdns (Arduino built-in)
|
||||
@@ -235,7 +235,7 @@ build_flags =
|
||||
-DUSE_ZEPHYR
|
||||
-DUSE_NRF52
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@7.0.0 ; json
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
|
||||
69
tests/component_tests/gpio/test_gpio_binary_sensor.py
Normal file
69
tests/component_tests/gpio/test_gpio_binary_sensor.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Tests for the GPIO binary sensor component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_basic_setup(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
When the GPIO binary sensor is set in the yaml file, it should be registered in main
|
||||
"""
|
||||
main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml")
|
||||
|
||||
assert "new gpio::GPIOBinarySensor();" in main_cpp
|
||||
assert "App.register_binary_sensor" in main_cpp
|
||||
assert "bs_gpio->set_use_interrupt(true);" in main_cpp
|
||||
assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_esp8266_gpio16_disables_interrupt(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""
|
||||
Test that ESP8266 GPIO16 automatically disables interrupt mode with a warning
|
||||
"""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
|
||||
)
|
||||
|
||||
# Check that interrupt is disabled for GPIO16
|
||||
assert "bs_gpio16->set_use_interrupt(false);" in main_cpp
|
||||
|
||||
# Check that the warning was logged
|
||||
assert "GPIO16 on ESP8266 doesn't support interrupts" in caplog.text
|
||||
assert "Falling back to polling mode" in caplog.text
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
Test that ESP8266 pins other than GPIO16 still use interrupt mode
|
||||
"""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
|
||||
)
|
||||
|
||||
# GPIO5 should still use interrupts
|
||||
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp
|
||||
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_explicit_polling_mode(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
Test that explicitly setting use_interrupt: false works
|
||||
"""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml"
|
||||
)
|
||||
|
||||
assert "bs_polling->set_use_interrupt(false);" in main_cpp
|
||||
11
tests/component_tests/gpio/test_gpio_binary_sensor.yaml
Normal file
11
tests/component_tests/gpio/test_gpio_binary_sensor.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 5
|
||||
name: "Test GPIO Binary Sensor"
|
||||
id: bs_gpio
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: 16
|
||||
mode: INPUT_PULLDOWN_16
|
||||
name: "GPIO16 Touch Sensor"
|
||||
id: bs_gpio16
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: 5
|
||||
mode: INPUT_PULLUP
|
||||
name: "GPIO5 Button"
|
||||
id: bs_gpio5
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 5
|
||||
name: "Polling Mode Sensor"
|
||||
id: bs_polling
|
||||
use_interrupt: false
|
||||
@@ -8,31 +8,31 @@ from esphome.types import ConfigType
|
||||
|
||||
def test_web_server_ota_true_fails_validation() -> None:
|
||||
"""Test that web_server with ota: true fails validation with helpful message."""
|
||||
from esphome.components.web_server import validate_ota_removed
|
||||
from esphome.components.web_server import validate_ota
|
||||
|
||||
# Config with ota: true should fail
|
||||
config: ConfigType = {"ota": True}
|
||||
|
||||
with pytest.raises(cv.Invalid) as exc_info:
|
||||
validate_ota_removed(config)
|
||||
validate_ota(config)
|
||||
|
||||
# Check error message contains migration instructions
|
||||
error_msg = str(exc_info.value)
|
||||
assert "has been removed from 'web_server'" in error_msg
|
||||
assert "only accepts 'false' to disable OTA" in error_msg
|
||||
assert "platform: web_server" in error_msg
|
||||
assert "ota:" in error_msg
|
||||
|
||||
|
||||
def test_web_server_ota_false_passes_validation() -> None:
|
||||
"""Test that web_server with ota: false passes validation."""
|
||||
from esphome.components.web_server import validate_ota_removed
|
||||
from esphome.components.web_server import validate_ota
|
||||
|
||||
# Config with ota: false should pass
|
||||
config: ConfigType = {"ota": False}
|
||||
result = validate_ota_removed(config)
|
||||
result = validate_ota(config)
|
||||
assert result == config
|
||||
|
||||
# Config without ota should also pass
|
||||
config: ConfigType = {}
|
||||
result = validate_ota_removed(config)
|
||||
result = validate_ota(config)
|
||||
assert result == config
|
||||
|
||||
1
tests/components/captive_portal/test.bk72xx-ard.yaml
Normal file
1
tests/components/captive_portal/test.bk72xx-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -6,6 +6,7 @@ esp_ldo:
|
||||
- id: ldo_4
|
||||
channel: 4
|
||||
voltage: 2.0V
|
||||
setup_priority: 900
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
|
||||
18
tests/components/logger/test-on_message.host.yaml
Normal file
18
tests/components/logger/test-on_message.host.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
logger:
|
||||
id: logger_id
|
||||
level: DEBUG
|
||||
on_message:
|
||||
- level: DEBUG
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("test", "Got message level %d: %s - %s", level, tag, message);
|
||||
- level: WARN
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGW("test", "Warning level %d from %s", level, tag);
|
||||
- level: ERROR
|
||||
then:
|
||||
- lambda: |-
|
||||
// Test that level is uint8_t by using it in calculations
|
||||
uint8_t adjusted_level = level + 1;
|
||||
ESP_LOGE("test", "Error with adjusted level %d", adjusted_level);
|
||||
@@ -919,21 +919,27 @@ lvgl:
|
||||
text_color: 0xFFFFFF
|
||||
scales:
|
||||
- ticks:
|
||||
width: 1
|
||||
width: !lambda return 1;
|
||||
count: 61
|
||||
length: 20
|
||||
length: 20%
|
||||
color: 0xFFFFFF
|
||||
range_from: 0
|
||||
range_to: 60
|
||||
angle_range: 360
|
||||
rotation: 270
|
||||
rotation: !lambda return 2700;
|
||||
indicators:
|
||||
- tick_style:
|
||||
start_value: 0
|
||||
end_value: 60
|
||||
color_start: 0x0000bd
|
||||
color_end: 0xbd0000
|
||||
width: !lambda return 1;
|
||||
- line:
|
||||
opa: 50%
|
||||
id: minute_hand
|
||||
color: 0xFF0000
|
||||
r_mod: -1
|
||||
width: 3
|
||||
r_mod: !lambda return -1;
|
||||
width: !lambda return 3;
|
||||
-
|
||||
angle_range: 330
|
||||
rotation: 300
|
||||
|
||||
87
tests/integration/fixtures/api_string_lambda.yaml
Normal file
87
tests/integration/fixtures/api_string_lambda.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
esphome:
|
||||
name: api-string-lambda-test
|
||||
host:
|
||||
|
||||
api:
|
||||
actions:
|
||||
# Service that tests string lambda functionality
|
||||
- action: test_string_lambda
|
||||
variables:
|
||||
input_string: string
|
||||
then:
|
||||
# Log the input to verify service was called
|
||||
- logger.log:
|
||||
format: "Service called with string: %s"
|
||||
args: [input_string.c_str()]
|
||||
|
||||
# This is the key test - using a lambda that returns x.c_str()
|
||||
# where x is already a string. This would fail to compile in 2025.7.0b5
|
||||
# with "no matching function for call to 'to_string(std::string)'"
|
||||
# This is the exact case from issue #9539
|
||||
- homeassistant.tag_scanned: !lambda 'return input_string.c_str();'
|
||||
|
||||
# Also test with homeassistant.event to verify our fix works with data fields
|
||||
- homeassistant.event:
|
||||
event: esphome.test_string_lambda
|
||||
data:
|
||||
value: !lambda 'return input_string.c_str();'
|
||||
|
||||
# Service that tests int lambda functionality
|
||||
- action: test_int_lambda
|
||||
variables:
|
||||
input_number: int
|
||||
then:
|
||||
# Log the input to verify service was called
|
||||
- logger.log:
|
||||
format: "Service called with int: %d"
|
||||
args: [input_number]
|
||||
|
||||
# Test that int lambdas still work correctly with to_string
|
||||
# The TemplatableStringValue should automatically convert int to string
|
||||
- homeassistant.event:
|
||||
event: esphome.test_int_lambda
|
||||
data:
|
||||
value: !lambda 'return input_number;'
|
||||
|
||||
# Service that tests float lambda functionality
|
||||
- action: test_float_lambda
|
||||
variables:
|
||||
input_float: float
|
||||
then:
|
||||
# Log the input to verify service was called
|
||||
- logger.log:
|
||||
format: "Service called with float: %.2f"
|
||||
args: [input_float]
|
||||
|
||||
# Test that float lambdas still work correctly with to_string
|
||||
# The TemplatableStringValue should automatically convert float to string
|
||||
- homeassistant.event:
|
||||
event: esphome.test_float_lambda
|
||||
data:
|
||||
value: !lambda 'return input_float;'
|
||||
|
||||
# Service that tests char* lambda functionality (e.g., from itoa or sprintf)
|
||||
- action: test_char_ptr_lambda
|
||||
variables:
|
||||
input_number: int
|
||||
input_string: string
|
||||
then:
|
||||
# Log the input to verify service was called
|
||||
- logger.log:
|
||||
format: "Service called with number for char* test: %d"
|
||||
args: [input_number]
|
||||
|
||||
# Test that char* lambdas work correctly
|
||||
# This would fail in issue #9628 with "invalid conversion from 'char*' to 'long long unsigned int'"
|
||||
- homeassistant.event:
|
||||
event: esphome.test_char_ptr_lambda
|
||||
data:
|
||||
# Test snprintf returning char*
|
||||
decimal_value: !lambda 'static char buffer[20]; snprintf(buffer, sizeof(buffer), "%d", input_number); return buffer;'
|
||||
# Test strdup returning char* (dynamically allocated)
|
||||
string_copy: !lambda 'return strdup(input_string.c_str());'
|
||||
# Test string literal (const char*)
|
||||
literal: !lambda 'return "test literal";'
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
24
tests/integration/fixtures/delay_action_cancellation.yaml
Normal file
24
tests/integration/fixtures/delay_action_cancellation.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
esphome:
|
||||
name: test-delay-action
|
||||
|
||||
host:
|
||||
api:
|
||||
actions:
|
||||
- action: start_delay_then_restart
|
||||
then:
|
||||
- logger.log: "Starting first script execution"
|
||||
- script.execute: test_delay_script
|
||||
- delay: 250ms # Give first script time to start delay
|
||||
- logger.log: "Restarting script (should cancel first delay)"
|
||||
- script.execute: test_delay_script
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
script:
|
||||
- id: test_delay_script
|
||||
mode: restart
|
||||
then:
|
||||
- logger.log: "Script started, beginning delay"
|
||||
- delay: 500ms # Long enough that it won't complete before restart
|
||||
- logger.log: "Delay completed successfully"
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL
|
||||
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
|
||||
@@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
|
||||
LoopTestISRComponent = loop_test_component_ns.class_(
|
||||
"LoopTestISRComponent", cg.Component
|
||||
)
|
||||
LoopTestUpdateComponent = loop_test_component_ns.class_(
|
||||
"LoopTestUpdateComponent", cg.PollingComponent
|
||||
)
|
||||
|
||||
CONF_DISABLE_AFTER = "disable_after"
|
||||
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
||||
CONF_ISR_COMPONENTS = "isr_components"
|
||||
CONF_UPDATE_COMPONENTS = "update_components"
|
||||
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
|
||||
|
||||
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_,
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list(
|
||||
UPDATE_COMPONENT_CONFIG_SCHEMA
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -94,3 +111,12 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(isr_config[CONF_ID])
|
||||
await cg.register_component(var, isr_config)
|
||||
cg.add(var.set_name(isr_config[CONF_NAME]))
|
||||
|
||||
# Create update test components
|
||||
for update_config in config.get(CONF_UPDATE_COMPONENTS, []):
|
||||
var = cg.new_Pvariable(update_config[CONF_ID])
|
||||
await cg.register_component(var, update_config)
|
||||
|
||||
cg.add(var.set_name(update_config[CONF_NAME]))
|
||||
cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER]))
|
||||
cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL]))
|
||||
|
||||
@@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() {
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
// LoopTestUpdateComponent implementation
|
||||
void LoopTestUpdateComponent::setup() {
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str());
|
||||
}
|
||||
|
||||
void LoopTestUpdateComponent::loop() {
|
||||
this->loop_count_++;
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_);
|
||||
|
||||
// Disable loop after specified count to test component.update when loop is disabled
|
||||
if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_);
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void LoopTestUpdateComponent::update() {
|
||||
this->update_count_++;
|
||||
// Check if loop is disabled by testing component state
|
||||
bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE;
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(),
|
||||
this->update_count_, loop_disabled ? "YES" : "NO");
|
||||
}
|
||||
|
||||
} // namespace loop_test_component
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace loop_test_component {
|
||||
@@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
|
||||
LoopTestComponent *parent_;
|
||||
};
|
||||
|
||||
// Component with update() method to test component.update action
|
||||
class LoopTestUpdateComponent : public PollingComponent {
|
||||
public:
|
||||
LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval
|
||||
|
||||
void set_name(const std::string &name) { this->name_ = name; }
|
||||
void set_disable_loop_after(int count) { this->disable_loop_after_ = count; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
int get_update_count() const { return this->update_count_; }
|
||||
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 update_count_{0};
|
||||
int disable_loop_after_{0};
|
||||
};
|
||||
|
||||
} // namespace loop_test_component
|
||||
} // namespace esphome
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user