mirror of
https://github.com/esphome/esphome.git
synced 2025-11-09 19:41: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 for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# 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:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# 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
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# 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
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ namespace esphome {
|
|||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
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:
|
public:
|
||||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
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>
|
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
||||||
TemplatableStringValue(F f)
|
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 {
|
template<typename... Ts> class TemplatableKeyValuePair {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/as3935/as3935.h"
|
#include "esphome/components/as3935/as3935.h"
|
||||||
#include "esphome/components/spi/spi.h"
|
#include "esphome/components/spi/spi.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace as3935_spi {
|
namespace as3935_spi {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if CORE.is_esp32 or CORE.is_libretiny:
|
if CORE.is_esp32 or CORE.is_libretiny:
|
||||||
# https://github.com/ESP32Async/AsyncTCP
|
# 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:
|
elif CORE.is_esp8266:
|
||||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "esphome/components/network/ip_address.h"
|
#include "esphome/components/network/ip_address.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <lwip/igmp.h>
|
#include <lwip/igmp.h>
|
||||||
#include <lwip/init.h>
|
#include <lwip/init.h>
|
||||||
@@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
|
|||||||
ip4_addr_t multicast_addr =
|
ip4_addr_t multicast_addr =
|
||||||
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
|
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) {
|
if (err) {
|
||||||
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
|
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) {
|
if (listen_method_ == E131_MULTICAST) {
|
||||||
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
||||||
|
|
||||||
|
LwIPLock lock;
|
||||||
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
|||||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_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)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
||||||
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from esphome import automation, pins
|
from esphome import automation, pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import i2c
|
from esphome.components import i2c
|
||||||
@@ -8,6 +10,7 @@ from esphome.const import (
|
|||||||
CONF_CONTRAST,
|
CONF_CONTRAST,
|
||||||
CONF_DATA_PINS,
|
CONF_DATA_PINS,
|
||||||
CONF_FREQUENCY,
|
CONF_FREQUENCY,
|
||||||
|
CONF_I2C,
|
||||||
CONF_I2C_ID,
|
CONF_I2C_ID,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
@@ -20,6 +23,9 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.core.entity_helpers import setup_entity
|
from esphome.core.entity_helpers import setup_entity
|
||||||
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ["esp32"]
|
DEPENDENCIES = ["esp32"]
|
||||||
|
|
||||||
@@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
|
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 = {
|
SETTERS = {
|
||||||
# pin assignment
|
# pin assignment
|
||||||
CONF_DATA_PINS: "set_data_pins",
|
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() { state_ = xt_rsil(15); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
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)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
wifi_get_macaddr(STATION_IF, mac);
|
wifi_get_macaddr(STATION_IF, mac);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ adjusted_ids = set()
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.ensure_list(
|
cv.ensure_list(
|
||||||
{
|
cv.COMPONENT_SCHEMA.extend(
|
||||||
cv.GenerateID(): cv.declare_id(EspLdo),
|
{
|
||||||
cv.Required(CONF_VOLTAGE): cv.All(
|
cv.GenerateID(): cv.declare_id(EspLdo),
|
||||||
cv.voltage, cv.float_range(min=0.5, max=2.7)
|
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.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
||||||
}
|
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
),
|
),
|
||||||
cv.only_with_esp_idf,
|
cv.only_with_esp_idf,
|
||||||
only_on_variant(supported=[VARIANT_ESP32P4]),
|
only_on_variant(supported=[VARIANT_ESP32P4]),
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class EspLdo : public Component {
|
|||||||
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
|
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
|
||||||
void set_voltage(float voltage) { this->voltage_ = voltage; }
|
void set_voltage(float voltage) { this->voltage_ = voltage; }
|
||||||
void adjust_voltage(float voltage);
|
void adjust_voltage(float voltage);
|
||||||
|
float get_setup_priority() const override {
|
||||||
|
return setup_priority::BUS; // LDO setup should be done early
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int channel_;
|
int channel_;
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||||
|
LwIPLock lock;
|
||||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||||
return dns_ip;
|
return dns_ip;
|
||||||
}
|
}
|
||||||
@@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
|
|||||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||||
|
|
||||||
if (this->manual_ip_.has_value()) {
|
if (this->manual_ip_.has_value()) {
|
||||||
|
LwIPLock lock;
|
||||||
if (this->manual_ip_->dns1.is_set()) {
|
if (this->manual_ip_->dns1.is_set()) {
|
||||||
ip_addr_t d;
|
ip_addr_t d;
|
||||||
d = this->manual_ip_->dns1;
|
d = this->manual_ip_->dns1;
|
||||||
@@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
|
|||||||
void EthernetComponent::dump_connect_params_() {
|
void EthernetComponent::dump_connect_params_() {
|
||||||
esp_netif_ip_info_t ip;
|
esp_netif_ip_info_t ip;
|
||||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||||
const ip_addr_t *dns_ip1 = dns_getserver(0);
|
const ip_addr_t *dns_ip1;
|
||||||
const ip_addr_t *dns_ip2 = dns_getserver(1);
|
const ip_addr_t *dns_ip2;
|
||||||
|
{
|
||||||
|
LwIPLock lock;
|
||||||
|
dns_ip1 = dns_getserver(0);
|
||||||
|
dns_ip2 = dns_getserver(1);
|
||||||
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" IP Address: %s\n"
|
" IP Address: %s\n"
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
void Fan::save_state_() {
|
void Fan::save_state_() {
|
||||||
|
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FanRestoreState state{};
|
FanRestoreState state{};
|
||||||
state.state = this->state;
|
state.state = this->state;
|
||||||
state.oscillating = this->oscillating;
|
state.oscillating = this->oscillating;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import binary_sensor
|
from esphome.components import binary_sensor
|
||||||
import esphome.config_validation as cv
|
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
|
from .. import gpio_ns
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
GPIOBinarySensor = gpio_ns.class_(
|
GPIOBinarySensor = gpio_ns.class_(
|
||||||
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||||
)
|
)
|
||||||
@@ -24,7 +29,21 @@ CONFIG_SCHEMA = (
|
|||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
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(
|
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
|
||||||
INTERRUPT_TYPES, upper=True
|
INTERRUPT_TYPES, upper=True
|
||||||
),
|
),
|
||||||
@@ -41,6 +60,22 @@ async def to_code(config):
|
|||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
cg.add(var.set_pin(pin))
|
cg.add(var.set_pin(pin))
|
||||||
|
|
||||||
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
|
# Check for ESP8266 GPIO16 interrupt limitation
|
||||||
if config[CONF_USE_INTERRUPT]:
|
# 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]))
|
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
|
container.reset(); // Release ownership of the container's shared_ptr
|
||||||
|
|
||||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
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");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
|
|||||||
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
||||||
|
|
||||||
for (auto build : root["builds"].as<JsonArray>()) {
|
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");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
||||||
if (!build.containsKey("ota")) {
|
if (!build["ota"].is<JsonObject>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto ota = build["ota"];
|
JsonObject ota = build["ota"].as<JsonObject>();
|
||||||
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
|
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||||
this_update->update_info_.md5 = ota["md5"].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>();
|
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>();
|
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ async def to_code(config):
|
|||||||
await speaker.register_speaker(var, config)
|
await speaker.register_speaker(var, config)
|
||||||
|
|
||||||
if config[CONF_DAC_TYPE] == "internal":
|
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:
|
else:
|
||||||
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
||||||
if use_legacy():
|
if use_legacy():
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
|
|
||||||
@coroutine_with_priority(1.0)
|
@coroutine_with_priority(1.0)
|
||||||
async def to_code(config):
|
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_define("USE_JSON")
|
||||||
cg.add_global(json_ns.using)
|
cg.add_global(json_ns.using)
|
||||||
|
|||||||
@@ -1,83 +1,76 @@
|
|||||||
#include "json_util.h"
|
#include "json_util.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace json {
|
namespace json {
|
||||||
|
|
||||||
static const char *const TAG = "json";
|
static const char *const TAG = "json";
|
||||||
|
|
||||||
static std::vector<char> global_json_build_buffer; // NOLINT
|
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||||
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
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) {
|
std::string build_json(const json_build_t &f) {
|
||||||
// Here we are allocating up to 5kb of memory,
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
// with the heap size minus 2kb to be safe if less than 5kb
|
auto doc_allocator = SpiRamAllocator();
|
||||||
// as we can not have a true dynamic sized document.
|
JsonDocument json_document(&doc_allocator);
|
||||||
// The excess memory is freed below with `shrinkToFit()`
|
if (json_document.overflowed()) {
|
||||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
return "{}";
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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) {
|
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||||
// Here we are allocating 1.5 times the data size,
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
// with the heap size minus 2kb to be safe if less than that
|
auto doc_allocator = SpiRamAllocator();
|
||||||
// as we can not have a true dynamic sized document.
|
JsonDocument json_document(&doc_allocator);
|
||||||
// The excess memory is freed below with `shrinkToFit()`
|
if (json_document.overflowed()) {
|
||||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||||
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
|
return false;
|
||||||
while (true) {
|
}
|
||||||
DynamicJsonDocument json_document(request_size);
|
DeserializationError err = deserializeJson(json_document, data);
|
||||||
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();
|
|
||||||
|
|
||||||
JsonObject root = json_document.as<JsonObject>();
|
JsonObject root = json_document.as<JsonObject>();
|
||||||
|
|
||||||
if (err == DeserializationError::Ok) {
|
if (err == DeserializationError::Ok) {
|
||||||
return f(root);
|
return f(root);
|
||||||
} else if (err == DeserializationError::NoMemory) {
|
} 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");
|
||||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||||
ESP_LOGV(TAG, "Increasing memory allocation.");
|
|
||||||
request_size *= 2;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return false;
|
return false;
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace json
|
} // namespace json
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|
||||||
static const char *const TAG = "LD2420.binary_sensor";
|
static const char *const TAG = "ld2420.binary_sensor";
|
||||||
|
|
||||||
void LD2420BinarySensor::dump_config() {
|
void LD2420BinarySensor::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:");
|
ESP_LOGCONFIG(TAG, "Binary Sensor:");
|
||||||
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
|
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
static const char *const TAG = "LD2420.button";
|
static const char *const TAG = "ld2420.button";
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple";
|
|||||||
// Memory-efficient lookup tables
|
// Memory-efficient lookup tables
|
||||||
struct StringToUint8 {
|
struct StringToUint8 {
|
||||||
const char *str;
|
const char *str;
|
||||||
uint8_t value;
|
const uint8_t value;
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr StringToUint8 OP_MODE_BY_STR[] = {
|
static constexpr StringToUint8 OP_MODE_BY_STR[] = {
|
||||||
@@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = {
|
|||||||
// Helper function for lookups
|
// Helper function for lookups
|
||||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
|
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
|
||||||
for (const auto &entry : arr) {
|
for (const auto &entry : arr) {
|
||||||
if (str == entry.str)
|
if (str == entry.str) {
|
||||||
return entry.value;
|
return entry.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0xFF; // Not found
|
return 0xFF; // Not found
|
||||||
}
|
}
|
||||||
@@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() {
|
|||||||
|
|
||||||
void LD2420Component::loop() {
|
void LD2420Component::loop() {
|
||||||
// If there is a active send command do not process it here, the send command call will handle it.
|
// If there is a active send command do not process it here, the send command call will handle it.
|
||||||
if (!this->get_cmd_active_()) {
|
while (!this->cmd_active_ && this->available()) {
|
||||||
if (!this->available())
|
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() {
|
|||||||
|
|
||||||
// Store average and peak values
|
// Store average and peak values
|
||||||
this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
|
this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
|
||||||
if (this->gate_peak[gate] < peak)
|
if (this->gate_peak[gate] < peak) {
|
||||||
this->gate_peak[gate] = peak;
|
this->gate_peak[gate] = peak;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t calculated_value =
|
uint32_t calculated_value =
|
||||||
(static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate])));
|
(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 {
|
} else {
|
||||||
// Set the current data back so we don't have new data that can be applied in error.
|
// 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));
|
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
|
||||||
|
}
|
||||||
this->set_calibration_(false);
|
this->set_calibration_(false);
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) {
|
||||||
static int pos = 0;
|
if (rx_data < 0) {
|
||||||
|
return; // No data available
|
||||||
if (rx_data >= 0) {
|
}
|
||||||
if (pos < len - 1) {
|
if (this->buffer_pos_ < len - 1) {
|
||||||
buffer[pos++] = rx_data;
|
buffer[this->buffer_pos_++] = rx_data;
|
||||||
buffer[pos] = 0;
|
buffer[this->buffer_pos_] = 0;
|
||||||
} else {
|
} else {
|
||||||
pos = 0;
|
// We should never get here, but just in case...
|
||||||
}
|
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
|
||||||
if (pos >= 4) {
|
this->buffer_pos_ = 0;
|
||||||
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.
|
if (this->buffer_pos_ < 4) {
|
||||||
this->handle_ack_data_(buffer, pos);
|
return; // Not enough data to process yet
|
||||||
pos = 0;
|
}
|
||||||
} else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) &&
|
if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
|
||||||
(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
|
this->cmd_active_ = false; // Set command state to inactive after response
|
||||||
this->handle_simple_mode_(buffer, pos);
|
this->handle_ack_data_(buffer, this->buffer_pos_);
|
||||||
pos = 0;
|
this->buffer_pos_ = 0;
|
||||||
} else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
|
} else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) &&
|
||||||
(this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
|
(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
|
||||||
this->handle_energy_mode_(buffer, pos);
|
this->handle_simple_mode_(buffer, this->buffer_pos_);
|
||||||
pos = 0;
|
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
|
// Resonable refresh rate for home assistant database size health
|
||||||
const int32_t current_millis = App.get_loop_component_start_time();
|
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;
|
return;
|
||||||
|
}
|
||||||
this->last_periodic_millis = current_millis;
|
this->last_periodic_millis = current_millis;
|
||||||
for (auto &listener : this->listeners_) {
|
for (auto &listener : this->listeners_) {
|
||||||
listener->on_distance(this->get_distance_());
|
listener->on_distance(this->get_distance_());
|
||||||
@@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
outbuf[index] = '\0';
|
outbuf[index] = '\0';
|
||||||
if (index > 1)
|
if (index > 1) {
|
||||||
this->set_distance_(strtol(outbuf, &endptr, 10));
|
this->set_distance_(strtol(outbuf, &endptr, 10));
|
||||||
|
}
|
||||||
|
|
||||||
if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
|
if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
|
||||||
// Resonable refresh rate for home assistant database size health
|
// Resonable refresh rate for home assistant database size health
|
||||||
const int32_t current_millis = App.get_loop_component_start_time();
|
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;
|
return;
|
||||||
|
}
|
||||||
this->last_normal_periodic_millis = current_millis;
|
this->last_normal_periodic_millis = current_millis;
|
||||||
for (auto &listener : this->listeners_)
|
for (auto &listener : this->listeners_)
|
||||||
listener->on_distance(this->get_distance_());
|
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) {
|
int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||||
uint32_t start_millis = millis();
|
uint32_t start_millis = millis();
|
||||||
uint8_t error = 0;
|
uint8_t error = 0;
|
||||||
uint8_t ack_buffer[64];
|
uint8_t ack_buffer[MAX_LINE_LENGTH];
|
||||||
uint8_t cmd_buffer[64];
|
uint8_t cmd_buffer[MAX_LINE_LENGTH];
|
||||||
this->cmd_reply_.ack = false;
|
this->cmd_reply_.ack = false;
|
||||||
if (frame.command != CMD_RESTART)
|
if (frame.command != CMD_RESTART) {
|
||||||
this->set_cmd_active_(true); // Restart does not reply, thus no ack state required.
|
this->cmd_active_ = true;
|
||||||
|
} // Restart does not reply, thus no ack state required
|
||||||
uint8_t retry = 3;
|
uint8_t retry = 3;
|
||||||
while (retry) {
|
while (retry) {
|
||||||
frame.length = 0;
|
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));
|
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
|
||||||
frame.length += sizeof(frame.footer);
|
frame.length += sizeof(frame.footer);
|
||||||
for (uint16_t index = 0; index < frame.length; index++) {
|
this->write_array(cmd_buffer, frame.length);
|
||||||
this->write_byte(cmd_buffer[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
error = 0;
|
error = 0;
|
||||||
if (frame.command == CMD_RESTART) {
|
if (frame.command == CMD_RESTART) {
|
||||||
@@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
|||||||
|
|
||||||
while (!this->cmd_reply_.ack) {
|
while (!this->cmd_reply_.ack) {
|
||||||
while (this->available()) {
|
while (this->available()) {
|
||||||
this->readline_(read(), ack_buffer, sizeof(ack_buffer));
|
this->readline_(this->read(), ack_buffer, sizeof(ack_buffer));
|
||||||
}
|
}
|
||||||
delay_microseconds_safe(1450);
|
delay_microseconds_safe(1450);
|
||||||
// Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->cmd_reply_.ack)
|
if (this->cmd_reply_.ack) {
|
||||||
retry = 0;
|
retry = 0;
|
||||||
if (this->cmd_reply_.error > 0)
|
}
|
||||||
|
if (this->cmd_reply_.error > 0) {
|
||||||
this->handle_cmd_error(error);
|
this->handle_cmd_error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
@@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) {
|
|||||||
cmd_frame.data_length += sizeof(unknown_parm);
|
cmd_frame.data_length += sizeof(unknown_parm);
|
||||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||||
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
|
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);
|
this->set_mode_(mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LD2420Component::get_firmware_version_() {
|
void LD2420Component::get_firmware_version_() {
|
||||||
@@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) {
|
|||||||
|
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
void LD2420Component::init_gate_config_numbers() {
|
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));
|
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);
|
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));
|
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));
|
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);
|
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);
|
this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor);
|
||||||
|
}
|
||||||
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
|
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
|
||||||
if (this->gate_still_threshold_numbers_[gate] != nullptr) {
|
if (this->gate_still_threshold_numbers_[gate] != nullptr) {
|
||||||
this->gate_still_threshold_numbers_[gate]->publish_state(
|
this->gate_still_threshold_numbers_[gate]->publish_state(
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|
||||||
static const uint8_t TOTAL_GATES = 16;
|
|
||||||
static const uint8_t CALIBRATE_SAMPLES = 64;
|
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 {
|
enum OpMode : uint8_t {
|
||||||
OP_NORMAL_MODE = 1,
|
OP_NORMAL_MODE = 1,
|
||||||
@@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
|||||||
|
|
||||||
float gate_move_sensitivity_factor{0.5};
|
float gate_move_sensitivity_factor{0.5};
|
||||||
float gate_still_sensitivity_factor{0.5};
|
float gate_still_sensitivity_factor{0.5};
|
||||||
int32_t last_periodic_millis = millis();
|
int32_t last_periodic_millis{0};
|
||||||
int32_t report_periodic_millis = millis();
|
int32_t report_periodic_millis{0};
|
||||||
int32_t monitor_periodic_millis = millis();
|
int32_t monitor_periodic_millis{0};
|
||||||
int32_t last_normal_periodic_millis = millis();
|
int32_t last_normal_periodic_millis{0};
|
||||||
uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES];
|
uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES];
|
||||||
uint16_t gate_avg[TOTAL_GATES];
|
uint16_t gate_avg[TOTAL_GATES];
|
||||||
uint16_t gate_peak[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; };
|
void set_presence_(bool presence) { this->presence_ = presence; };
|
||||||
uint16_t get_distance_() { return this->distance_; };
|
uint16_t get_distance_() { return this->distance_; };
|
||||||
void set_distance_(uint16_t distance) { this->distance_ = 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_simple_mode_(const uint8_t *inbuf, int len);
|
||||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||||
void handle_ack_data_(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);
|
std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
uint32_t max_distance_gate_;
|
uint16_t distance_{0};
|
||||||
uint32_t min_distance_gate_;
|
|
||||||
uint16_t system_mode_;
|
uint16_t system_mode_;
|
||||||
uint16_t gate_energy_[TOTAL_GATES];
|
uint16_t gate_energy_[TOTAL_GATES];
|
||||||
uint16_t distance_{0};
|
uint8_t buffer_pos_{0}; // where to resume processing/populating buffer
|
||||||
uint8_t config_checksum_{0};
|
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||||
char firmware_ver_[8]{"v0.0.0"};
|
char firmware_ver_[8]{"v0.0.0"};
|
||||||
bool cmd_active_{false};
|
bool cmd_active_{false};
|
||||||
bool presence_{false};
|
bool presence_{false};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
static const char *const TAG = "LD2420.number";
|
static const char *const TAG = "ld2420.number";
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|
||||||
static const char *const TAG = "LD2420.select";
|
static const char *const TAG = "ld2420.select";
|
||||||
|
|
||||||
void LD2420Select::control(const std::string &value) {
|
void LD2420Select::control(const std::string &value) {
|
||||||
this->publish_state(value);
|
this->publish_state(value);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|
||||||
static const char *const TAG = "LD2420.sensor";
|
static const char *const TAG = "ld2420.sensor";
|
||||||
|
|
||||||
void LD2420Sensor::dump_config() {
|
void LD2420Sensor::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "LD2420 Sensor:");
|
ESP_LOGCONFIG(TAG, "Sensor:");
|
||||||
LOG_SENSOR(" ", "Distance", this->distance_sensor_);
|
LOG_SENSOR(" ", "Distance", this->distance_sensor_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ld2420 {
|
namespace ld2420 {
|
||||||
|
|
||||||
static const char *const TAG = "LD2420.text_sensor";
|
static const char *const TAG = "ld2420.text_sensor";
|
||||||
|
|
||||||
void LD2420TextSensor::dump_config() {
|
void LD2420TextSensor::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "LD2420 TextSensor:");
|
ESP_LOGCONFIG(TAG, "Text Sensor:");
|
||||||
LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_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() { portDISABLE_INTERRUPTS(); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_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)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
WiFi.macAddress(mac);
|
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
|
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||||
|
|
||||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
if (state.supports_effects())
|
if (state.supports_effects())
|
||||||
root["effect"] = state.get_effect_name();
|
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)
|
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
|
||||||
root["brightness"] = uint8_t(values.get_brightness() * 255);
|
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) {
|
if (values.get_color_mode() & ColorCapability::RGB) {
|
||||||
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
|
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
|
||||||
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 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) {
|
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"]);
|
auto val = parse_on_off(root["state"]);
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case PARSE_ON:
|
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);
|
call.set_brightness(float(root["brightness"]) / 255.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("color")) {
|
if (root["color"].is<JsonObject>()) {
|
||||||
JsonObject color = root["color"];
|
JsonObject color = root["color"];
|
||||||
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
|
// 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;
|
float max_rgb = 0.0f;
|
||||||
if (color.containsKey("r")) {
|
if (color["r"].is<uint8_t>()) {
|
||||||
float r = float(color["r"]) / 255.0f;
|
float r = float(color["r"]) / 255.0f;
|
||||||
max_rgb = fmaxf(max_rgb, r);
|
max_rgb = fmaxf(max_rgb, r);
|
||||||
call.set_red(r);
|
call.set_red(r);
|
||||||
}
|
}
|
||||||
if (color.containsKey("g")) {
|
if (color["g"].is<uint8_t>()) {
|
||||||
float g = float(color["g"]) / 255.0f;
|
float g = float(color["g"]) / 255.0f;
|
||||||
max_rgb = fmaxf(max_rgb, g);
|
max_rgb = fmaxf(max_rgb, g);
|
||||||
call.set_green(g);
|
call.set_green(g);
|
||||||
}
|
}
|
||||||
if (color.containsKey("b")) {
|
if (color["b"].is<uint8_t>()) {
|
||||||
float b = float(color["b"]) / 255.0f;
|
float b = float(color["b"]) / 255.0f;
|
||||||
max_rgb = fmaxf(max_rgb, b);
|
max_rgb = fmaxf(max_rgb, b);
|
||||||
call.set_blue(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);
|
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);
|
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
|
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
|
||||||
// white channel in RGBWW.
|
// white channel in RGBWW.
|
||||||
if (color.containsKey("c")) {
|
if (color["c"].is<uint8_t>()) {
|
||||||
call.set_warm_white(float(color["w"]) / 255.0f);
|
call.set_warm_white(float(color["w"]) / 255.0f);
|
||||||
} else {
|
} else {
|
||||||
call.set_white(float(color["w"]) / 255.0f);
|
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);
|
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"]));
|
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) {
|
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
|
||||||
LightJSONSchema::parse_color_json(state, call, 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);
|
auto length = uint32_t(float(root["flash"]) * 1000);
|
||||||
call.set_flash_length(length);
|
call.set_flash_length(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("transition")) {
|
if (root["transition"].is<uint16_t>()) {
|
||||||
auto length = uint32_t(float(root["transition"]) * 1000);
|
auto length = uint32_t(float(root["transition"]) * 1000);
|
||||||
call.set_transition_length(length);
|
call.set_transition_length(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("effect")) {
|
if (root["effect"].is<const char *>()) {
|
||||||
const char *effect = root["effect"];
|
const char *effect = root["effect"];
|
||||||
call.set_effect(effect);
|
call.set_effect(effect);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ def validate_local_no_higher_than_global(value):
|
|||||||
Logger = logger_ns.class_("Logger", cg.Component)
|
Logger = logger_ns.class_("Logger", cg.Component)
|
||||||
LoggerMessageTrigger = logger_ns.class_(
|
LoggerMessageTrigger = logger_ns.class_(
|
||||||
"LoggerMessageTrigger",
|
"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"
|
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(
|
await automation.build_automation(
|
||||||
trigger,
|
trigger,
|
||||||
[
|
[
|
||||||
(cg.int_, "level"),
|
(cg.uint8, "level"),
|
||||||
(cg.const_char_ptr, "tag"),
|
(cg.const_char_ptr, "tag"),
|
||||||
(cg.const_char_ptr, "message"),
|
(cg.const_char_ptr, "message"),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class WidgetType:
|
|||||||
|
|
||||||
class NumberType(WidgetType):
|
class NumberType(WidgetType):
|
||||||
def get_max(self, config: dict):
|
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):
|
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_VALUE,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
)
|
)
|
||||||
|
from esphome.cpp_generator import IntLiteral
|
||||||
|
|
||||||
from ..automation import action_to_code
|
from ..automation import action_to_code
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
@@ -29,9 +30,9 @@ from ..defines import (
|
|||||||
)
|
)
|
||||||
from ..helpers import add_lv_use, lvgl_components_required
|
from ..helpers import add_lv_use, lvgl_components_required
|
||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
angle,
|
|
||||||
get_end_value,
|
get_end_value,
|
||||||
get_start_value,
|
get_start_value,
|
||||||
|
lv_angle,
|
||||||
lv_bool,
|
lv_bool,
|
||||||
lv_color,
|
lv_color,
|
||||||
lv_float,
|
lv_float,
|
||||||
@@ -162,7 +163,7 @@ SCALE_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
||||||
cv.Optional(CONF_RANGE_TO, default=100.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_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),
|
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -187,7 +188,9 @@ class MeterType(WidgetType):
|
|||||||
for scale_conf in config.get(CONF_SCALES, ()):
|
for scale_conf in config.get(CONF_SCALES, ()):
|
||||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||||
if CONF_ROTATION in scale_conf:
|
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(
|
with LocalVariable(
|
||||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||||
) as meter_var:
|
) as meter_var:
|
||||||
@@ -205,21 +208,20 @@ class MeterType(WidgetType):
|
|||||||
var,
|
var,
|
||||||
meter_var,
|
meter_var,
|
||||||
ticks[CONF_COUNT],
|
ticks[CONF_COUNT],
|
||||||
ticks[CONF_WIDTH],
|
await size.process(ticks[CONF_WIDTH]),
|
||||||
ticks[CONF_LENGTH],
|
await size.process(ticks[CONF_LENGTH]),
|
||||||
color,
|
color,
|
||||||
)
|
)
|
||||||
if CONF_MAJOR in ticks:
|
if CONF_MAJOR in ticks:
|
||||||
major = ticks[CONF_MAJOR]
|
major = ticks[CONF_MAJOR]
|
||||||
color = await lv_color.process(major[CONF_COLOR])
|
|
||||||
lv.meter_set_scale_major_ticks(
|
lv.meter_set_scale_major_ticks(
|
||||||
var,
|
var,
|
||||||
meter_var,
|
meter_var,
|
||||||
major[CONF_STRIDE],
|
major[CONF_STRIDE],
|
||||||
major[CONF_WIDTH],
|
await size.process(major[CONF_WIDTH]),
|
||||||
major[CONF_LENGTH],
|
await size.process(major[CONF_LENGTH]),
|
||||||
color,
|
await lv_color.process(major[CONF_COLOR]),
|
||||||
major[CONF_LABEL_GAP],
|
await size.process(major[CONF_LABEL_GAP]),
|
||||||
)
|
)
|
||||||
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
||||||
(t, v) = next(iter(indicator.items()))
|
(t, v) = next(iter(indicator.items()))
|
||||||
@@ -233,7 +235,11 @@ class MeterType(WidgetType):
|
|||||||
lv_assign(
|
lv_assign(
|
||||||
ivar,
|
ivar,
|
||||||
lv_expr.meter_add_needle_line(
|
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:
|
if t == CONF_ARC:
|
||||||
@@ -241,7 +247,11 @@ class MeterType(WidgetType):
|
|||||||
lv_assign(
|
lv_assign(
|
||||||
ivar,
|
ivar,
|
||||||
lv_expr.meter_add_arc(
|
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:
|
if t == CONF_TICK_STYLE:
|
||||||
@@ -257,7 +267,7 @@ class MeterType(WidgetType):
|
|||||||
color_start,
|
color_start,
|
||||||
color_end,
|
color_end,
|
||||||
v[CONF_LOCAL],
|
v[CONF_LOCAL],
|
||||||
v[CONF_WIDTH],
|
await size.process(v[CONF_WIDTH]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_IMAGE:
|
if t == CONF_IMAGE:
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &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();
|
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
|
||||||
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
||||||
supported_features.add("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) {
|
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())
|
if (!this->binary_sensor_->get_device_class().empty())
|
||||||
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
||||||
if (this->binary_sensor_->is_status_binary_sensor())
|
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) {
|
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
config.state_topic = false;
|
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();
|
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
|
||||||
|
}
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MQTTButtonComponent::component_type() const { return "button"; }
|
std::string MQTTButtonComponent::component_type() const { return "button"; }
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() {
|
|||||||
std::string topic = "esphome/discover/";
|
std::string topic = "esphome/discover/";
|
||||||
topic.append(App.get_name());
|
topic.append(App.get_name());
|
||||||
|
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
this->publish_json(
|
this->publish_json(
|
||||||
topic,
|
topic,
|
||||||
[](JsonObject root) {
|
[](JsonObject root) {
|
||||||
@@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() {
|
|||||||
#endif
|
#endif
|
||||||
},
|
},
|
||||||
2, this->discovery_info_.retain);
|
2, this->discovery_info_.retain);
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTClientComponent::dump_config() {
|
void MQTTClientComponent::dump_config() {
|
||||||
@@ -191,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
|
|||||||
this->dns_resolve_error_ = false;
|
this->dns_resolve_error_ = false;
|
||||||
this->dns_resolved_ = false;
|
this->dns_resolved_ = false;
|
||||||
ip_addr_t addr;
|
ip_addr_t addr;
|
||||||
|
err_t err;
|
||||||
|
{
|
||||||
|
LwIPLock lock;
|
||||||
#if USE_NETWORK_IPV6
|
#if USE_NETWORK_IPV6
|
||||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
||||||
#else
|
#else
|
||||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4);
|
this, LWIP_DNS_ADDRTYPE_IPV4);
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
#endif /* USE_NETWORK_IPV6 */
|
||||||
|
}
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case ERR_OK: {
|
case ERR_OK: {
|
||||||
// Got IP immediately
|
// Got IP immediately
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
|
|||||||
using namespace esphome::climate;
|
using namespace esphome::climate;
|
||||||
|
|
||||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
auto traits = this->device_->get_traits();
|
auto traits = this->device_->get_traits();
|
||||||
// current_temperature_topic
|
// current_temperature_topic
|
||||||
if (traits.get_supports_current_temperature()) {
|
if (traits.get_supports_current_temperature()) {
|
||||||
@@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// mode_state_topic
|
// mode_state_topic
|
||||||
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
|
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
|
||||||
// modes
|
// modes
|
||||||
JsonArray modes = root.createNestedArray(MQTT_MODES);
|
JsonArray modes = root[MQTT_MODES].to<JsonArray>();
|
||||||
// sort array for nice UI in HA
|
// sort array for nice UI in HA
|
||||||
if (traits.supports_mode(CLIMATE_MODE_AUTO))
|
if (traits.supports_mode(CLIMATE_MODE_AUTO))
|
||||||
modes.add("auto");
|
modes.add("auto");
|
||||||
@@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// preset_mode_state_topic
|
// preset_mode_state_topic
|
||||||
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
|
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
|
||||||
// presets
|
// presets
|
||||||
JsonArray presets = root.createNestedArray("preset_modes");
|
JsonArray presets = root["preset_modes"].to<JsonArray>();
|
||||||
if (traits.supports_preset(CLIMATE_PRESET_HOME))
|
if (traits.supports_preset(CLIMATE_PRESET_HOME))
|
||||||
presets.add("home");
|
presets.add("home");
|
||||||
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
|
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
|
||||||
@@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// fan_mode_state_topic
|
// fan_mode_state_topic
|
||||||
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
|
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
|
||||||
// fan_modes
|
// 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))
|
if (traits.supports_fan_mode(CLIMATE_FAN_ON))
|
||||||
fan_modes.add("on");
|
fan_modes.add("on");
|
||||||
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
|
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
|
||||||
@@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// swing_mode_state_topic
|
// swing_mode_state_topic
|
||||||
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
|
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
|
||||||
// swing_modes
|
// 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))
|
if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
|
||||||
swing_modes.add("off");
|
swing_modes.add("off");
|
||||||
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
|
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.state_topic = false;
|
||||||
config.command_topic = false;
|
config.command_topic = false;
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
void MQTTClimateComponent::setup() {
|
void MQTTClimateComponent::setup() {
|
||||||
auto traits = this->device_->get_traits();
|
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());
|
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(
|
return global_mqtt_client->publish_json(
|
||||||
this->get_discovery_topic_(discovery_info),
|
this->get_discovery_topic_(discovery_info),
|
||||||
[this](JsonObject root) {
|
[this](JsonObject root) {
|
||||||
@@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
}
|
}
|
||||||
std::string node_area = App.get_area();
|
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();
|
const auto mac = get_mac_address();
|
||||||
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
|
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
|
||||||
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
|
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
|
||||||
@@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
|
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
|
||||||
},
|
},
|
||||||
this->qos_, discovery_info.retain);
|
this->qos_, discovery_info.retain);
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
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) {
|
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())
|
if (!this->cover_->get_device_class().empty())
|
||||||
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
|
|||||||
void MQTTDateComponent::setup() {
|
void MQTTDateComponent::setup() {
|
||||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||||
auto call = this->date_->make_call();
|
auto call = this->date_->make_call();
|
||||||
if (root.containsKey("year")) {
|
if (root["year"].is<uint16_t>()) {
|
||||||
call.set_year(root["year"]);
|
call.set_year(root["year"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("month")) {
|
if (root["month"].is<uint8_t>()) {
|
||||||
call.set_month(root["month"]);
|
call.set_month(root["month"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("day")) {
|
if (root["day"].is<uint8_t>()) {
|
||||||
call.set_day(root["day"]);
|
call.set_day(root["day"]);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
|
|||||||
}
|
}
|
||||||
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
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) {
|
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["year"] = year;
|
||||||
root["month"] = month;
|
root["month"] = month;
|
||||||
root["day"] = day;
|
root["day"] = day;
|
||||||
|
|||||||
@@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
|
|||||||
void MQTTDateTimeComponent::setup() {
|
void MQTTDateTimeComponent::setup() {
|
||||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||||
auto call = this->datetime_->make_call();
|
auto call = this->datetime_->make_call();
|
||||||
if (root.containsKey("year")) {
|
if (root["year"].is<uint16_t>()) {
|
||||||
call.set_year(root["year"]);
|
call.set_year(root["year"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("month")) {
|
if (root["month"].is<uint8_t>()) {
|
||||||
call.set_month(root["month"]);
|
call.set_month(root["month"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("day")) {
|
if (root["day"].is<uint8_t>()) {
|
||||||
call.set_day(root["day"]);
|
call.set_day(root["day"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("hour")) {
|
if (root["hour"].is<uint8_t>()) {
|
||||||
call.set_hour(root["hour"]);
|
call.set_hour(root["hour"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("minute")) {
|
if (root["minute"].is<uint8_t>()) {
|
||||||
call.set_minute(root["minute"]);
|
call.set_minute(root["minute"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("second")) {
|
if (root["second"].is<uint8_t>()) {
|
||||||
call.set_second(root["second"]);
|
call.set_second(root["second"]);
|
||||||
}
|
}
|
||||||
call.perform();
|
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,
|
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
|
||||||
uint8_t second) {
|
uint8_t second) {
|
||||||
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
|
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["year"] = year;
|
||||||
root["month"] = month;
|
root["month"] = month;
|
||||||
root["day"] = day;
|
root["day"] = day;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ using namespace esphome::event;
|
|||||||
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
|
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
|
||||||
|
|
||||||
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
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())
|
for (const auto &event_type : this->event_->get_event_types())
|
||||||
event_types.add(event_type);
|
event_types.add(event_type);
|
||||||
|
|
||||||
@@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
||||||
return this->publish_json(this->get_state_topic_(),
|
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
|
||||||
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; });
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
root[MQTT_EVENT_TYPE] = event_type;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MQTTEventComponent::component_type() const { return "event"; }
|
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(); }
|
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
|
||||||
|
|
||||||
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
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()) {
|
if (this->state_->get_traits().supports_direction()) {
|
||||||
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
||||||
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
||||||
|
|||||||
@@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() {
|
|||||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||||
|
|
||||||
bool MQTTJSONLightComponent::publish_state_() {
|
bool MQTTJSONLightComponent::publish_state_() {
|
||||||
return this->publish_json(this->get_state_topic_(),
|
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
|
||||||
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); });
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
LightJSONSchema::dump_json(*this->state_, root);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
|
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
|
||||||
|
|
||||||
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["schema"] = "json";
|
root["schema"] = "json";
|
||||||
auto traits = this->state_->get_traits();
|
auto traits = this->state_->get_traits();
|
||||||
|
|
||||||
root[MQTT_COLOR_MODE] = true;
|
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))
|
if (traits.supports_color_mode(ColorMode::ON_OFF))
|
||||||
color_modes.add("onoff");
|
color_modes.add("onoff");
|
||||||
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
|
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
|
||||||
@@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
|||||||
|
|
||||||
if (this->state_->supports_effects()) {
|
if (this->state_->supports_effects()) {
|
||||||
root["effect"] = true;
|
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())
|
for (auto *effect : this->state_->get_effects())
|
||||||
effect_list.add(effect->get_name());
|
effect_list.add(effect->get_name());
|
||||||
effect_list.add("None");
|
effect_list.add("None");
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
|
|||||||
std::string MQTTLockComponent::component_type() const { return "lock"; }
|
std::string MQTTLockComponent::component_type() const { return "lock"; }
|
||||||
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
||||||
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
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;
|
root[MQTT_OPTIMISTIC] = true;
|
||||||
|
}
|
||||||
if (this->lock_->traits.get_supports_open())
|
if (this->lock_->traits.get_supports_open())
|
||||||
root[MQTT_PAYLOAD_OPEN] = "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) {
|
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
const auto &traits = number_->traits;
|
const auto &traits = number_->traits;
|
||||||
// https://www.home-assistant.io/integrations/number.mqtt/
|
// 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_MIN] = traits.get_min_value();
|
||||||
root[MQTT_MAX] = traits.get_max_value();
|
root[MQTT_MAX] = traits.get_max_value();
|
||||||
root[MQTT_STEP] = traits.get_step();
|
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) {
|
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
const auto &traits = select_->traits;
|
const auto &traits = select_->traits;
|
||||||
// https://www.home-assistant.io/integrations/select.mqtt/
|
// 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())
|
for (const auto &option : traits.get_options())
|
||||||
options.add(option);
|
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::disable_expire_after() { this->expire_after_ = 0; }
|
||||||
|
|
||||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
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();
|
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||||
|
}
|
||||||
|
|
||||||
if (!this->sensor_->get_unit_of_measurement().empty())
|
if (!this->sensor_->get_unit_of_measurement().empty())
|
||||||
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
|
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"; }
|
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
|
||||||
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
|
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
|
||||||
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
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;
|
root[MQTT_OPTIMISTIC] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
|
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_; }
|
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||||
|
|
||||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
switch (this->text_->traits.get_mode()) {
|
switch (this->text_->traits.get_mode()) {
|
||||||
case TEXT_MODE_TEXT:
|
case TEXT_MODE_TEXT:
|
||||||
root[MQTT_MODE] = "text";
|
root[MQTT_MODE] = "text";
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
|
|||||||
|
|
||||||
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
||||||
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
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();
|
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||||
|
}
|
||||||
config.command_topic = false;
|
config.command_topic = false;
|
||||||
}
|
}
|
||||||
void MQTTTextSensor::setup() {
|
void MQTTTextSensor::setup() {
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
|
|||||||
void MQTTTimeComponent::setup() {
|
void MQTTTimeComponent::setup() {
|
||||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||||
auto call = this->time_->make_call();
|
auto call = this->time_->make_call();
|
||||||
if (root.containsKey("hour")) {
|
if (root["hour"].is<uint8_t>()) {
|
||||||
call.set_hour(root["hour"]);
|
call.set_hour(root["hour"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("minute")) {
|
if (root["minute"].is<uint8_t>()) {
|
||||||
call.set_minute(root["minute"]);
|
call.set_minute(root["minute"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("second")) {
|
if (root["second"].is<uint8_t>()) {
|
||||||
call.set_second(root["second"]);
|
call.set_second(root["second"]);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
|
|||||||
}
|
}
|
||||||
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
|
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) {
|
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["hour"] = hour;
|
||||||
root["minute"] = minute;
|
root["minute"] = minute;
|
||||||
root["second"] = second;
|
root["second"] = second;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["schema"] = "json";
|
root["schema"] = "json";
|
||||||
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
|
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &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();
|
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
|
||||||
|
}
|
||||||
|
|
||||||
auto traits = this->valve_->get_traits();
|
auto traits = this->valve_->get_traits();
|
||||||
if (traits.get_is_assumed_state()) {
|
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%]
|
// map 16 bit humidity value into range [-6%, 118%]
|
||||||
float const humidity_partial = double(humidity) / (1 << 16);
|
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 =
|
float const compensated_humidity_percentage =
|
||||||
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
|
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
|
||||||
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
|
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
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.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||||
from esphome.components.image import (
|
from esphome.components.image import (
|
||||||
CONF_INVERT_ALPHA,
|
CONF_INVERT_ALPHA,
|
||||||
@@ -11,6 +11,7 @@ from esphome.components.image import (
|
|||||||
Image_,
|
Image_,
|
||||||
get_image_type_enum,
|
get_image_type_enum,
|
||||||
get_transparency_enum,
|
get_transparency_enum,
|
||||||
|
validate_settings,
|
||||||
)
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
rp2040_arduino=cv.Version(0, 0, 0),
|
rp2040_arduino=cv.Version(0, 0, 0),
|
||||||
host=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]),
|
get_image_type_enum(config[CONF_TYPE]),
|
||||||
transparent,
|
transparent,
|
||||||
config[CONF_BUFFER_SIZE],
|
config[CONF_BUFFER_SIZE],
|
||||||
|
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
|
||||||
)
|
)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
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,
|
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),
|
: Image(nullptr, 0, 0, type, transparency),
|
||||||
buffer_(nullptr),
|
buffer_(nullptr),
|
||||||
download_buffer_(download_buffer_size),
|
download_buffer_(download_buffer_size),
|
||||||
download_buffer_initial_size_(download_buffer_size),
|
download_buffer_initial_size_(download_buffer_size),
|
||||||
format_(format),
|
format_(format),
|
||||||
fixed_width_(width),
|
fixed_width_(width),
|
||||||
fixed_height_(height) {
|
fixed_height_(height),
|
||||||
|
is_big_endian_(is_big_endian) {
|
||||||
this->set_url(url);
|
this->set_url(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
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 (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||||
if (gray == 1) {
|
if (gray == 1) {
|
||||||
gray = 0;
|
gray = 0;
|
||||||
@@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
|||||||
case ImageType::IMAGE_TYPE_RGB565: {
|
case ImageType::IMAGE_TYPE_RGB565: {
|
||||||
this->map_chroma_key(color);
|
this->map_chroma_key(color);
|
||||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
if (this->is_big_endian_) {
|
||||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
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) {
|
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
this->buffer_[pos + 2] = color.w;
|
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.
|
* @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,
|
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;
|
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_;
|
const int fixed_width_;
|
||||||
/** height requested on configuration, or 0 if non specified. */
|
/** height requested on configuration, or 0 if non specified. */
|
||||||
const int fixed_height_;
|
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,
|
* 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
|
* 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_);
|
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_
|
this->state = state < 0.003 && this->zero_means_zero_
|
||||||
? 0.0
|
? 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;
|
this->has_state_ = true;
|
||||||
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
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() { state_ = save_and_disable_interrupts(); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
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)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
#ifdef USE_WIFI
|
#ifdef USE_WIFI
|
||||||
WiFi.macAddress(mac);
|
WiFi.macAddress(mac);
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
|
|||||||
value = clamp(value, -1.0f, 1.0f);
|
value = clamp(value, -1.0f, 1.0f);
|
||||||
float level;
|
float level;
|
||||||
if (value < 0.0) {
|
if (value < 0.0) {
|
||||||
level = lerp(-value, this->idle_level_, this->min_level_);
|
level = std::lerp(this->idle_level_, this->min_level_, -value);
|
||||||
} else {
|
} 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->output_->set_level(level);
|
||||||
this->current_value_ = value;
|
this->current_value_ = value;
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() {
|
|||||||
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
|
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
|
||||||
this->delete_tasks_();
|
this->delete_tasks_();
|
||||||
if (this->hard_stop_) {
|
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->speaker_->stop();
|
||||||
this->hard_stop_ = false;
|
this->hard_stop_ = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->is_playing_ = false;
|
this->is_playing_ = false;
|
||||||
return AudioPipelineState::STOPPED;
|
if (!this->speaker_->is_running()) {
|
||||||
|
return AudioPipelineState::STOPPED;
|
||||||
|
} else {
|
||||||
|
this->is_finishing_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->pause_state_) {
|
if (this->pause_state_) {
|
||||||
return AudioPipelineState::PAUSED;
|
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)) {
|
if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
|
||||||
// No tasks are running, so the pipeline is stopped.
|
// No tasks are running, so the pipeline is stopped.
|
||||||
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
|
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class AudioPipeline {
|
|||||||
|
|
||||||
bool hard_stop_{false};
|
bool hard_stop_{false};
|
||||||
bool is_playing_{false};
|
bool is_playing_{false};
|
||||||
|
bool is_finishing_{false};
|
||||||
bool pause_state_{false};
|
bool pause_state_{false};
|
||||||
bool task_stack_in_psram_;
|
bool task_stack_in_psram_;
|
||||||
|
|
||||||
|
|||||||
@@ -151,8 +151,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
|
|||||||
if sub is not None:
|
if sub is not None:
|
||||||
item[k] = sub
|
item[k] = sub
|
||||||
for old, new in replace_keys:
|
for old, new in replace_keys:
|
||||||
item[new] = merge_config(item.get(old), item.get(new))
|
if str(new) == str(old):
|
||||||
del item[old]
|
item[new] = item[old]
|
||||||
|
else:
|
||||||
|
item[new] = merge_config(item.get(old), item.get(new))
|
||||||
|
del item[old]
|
||||||
elif isinstance(item, str):
|
elif isinstance(item, str):
|
||||||
sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing)
|
sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing)
|
||||||
if isinstance(sub, JinjaStr) or sub != item:
|
if isinstance(sub, JinjaStr) or sub != item:
|
||||||
|
|||||||
@@ -35,6 +35,27 @@ void VoiceAssistant::setup() {
|
|||||||
temp_ring_buffer->write((void *) data.data(), data.size());
|
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; }
|
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_;
|
msg.wake_word_phrase = this->wake_word_;
|
||||||
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)) {
|
if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
|
||||||
ESP_LOGW(TAG, "Could not request start");
|
ESP_LOGW(TAG, "Could not request start");
|
||||||
this->error_trigger_->trigger("not-connected", "Could not request start");
|
this->error_trigger_->trigger("not-connected", "Could not request start");
|
||||||
@@ -314,17 +342,10 @@ void VoiceAssistant::loop() {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
if (this->media_player_ != nullptr) {
|
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_) {
|
if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) {
|
||||||
// Announcement has started playing, wait for it to finish
|
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
||||||
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;
|
|
||||||
this->cancel_timeout("playing");
|
this->cancel_timeout("playing");
|
||||||
ESP_LOGD(TAG, "Announcement finished playing");
|
ESP_LOGD(TAG, "Announcement finished playing");
|
||||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||||
@@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() {
|
|||||||
break;
|
break;
|
||||||
case State::AWAITING_RESPONSE:
|
case State::AWAITING_RESPONSE:
|
||||||
this->signal_stop_();
|
this->signal_stop_();
|
||||||
// Fallthrough intended to stop a streaming TTS announcement that has potentially started
|
break;
|
||||||
case State::STREAMING_RESPONSE:
|
case State::STREAMING_RESPONSE:
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
// Stop any ongoing media player announcement
|
// Stop any ongoing media player announcement
|
||||||
@@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() {
|
|||||||
.set_announcement(true)
|
.set_announcement(true)
|
||||||
.perform();
|
.perform();
|
||||||
}
|
}
|
||||||
|
if (this->started_streaming_tts_) {
|
||||||
|
// Haven't reached the TTS_END stage, so send the stop signal to HA.
|
||||||
|
this->signal_stop_();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
case State::RESPONSE_FINISHED:
|
case State::RESPONSE_FINISHED:
|
||||||
@@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||||||
if (this->media_player_ != nullptr) {
|
if (this->media_player_ != nullptr) {
|
||||||
for (const auto &arg : msg.data) {
|
for (const auto &arg : msg.data) {
|
||||||
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
|
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_->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->started_streaming_tts_ = true;
|
||||||
|
this->start_playback_timeout_();
|
||||||
|
|
||||||
tts_url_for_trigger = this->tts_response_url_;
|
tts_url_for_trigger = this->tts_response_url_;
|
||||||
this->tts_response_url_.clear(); // Reset streaming 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]() {
|
this->defer([this, url]() {
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
|
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_->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->start_playback_timeout_();
|
||||||
}
|
}
|
||||||
|
this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
|
||||||
#endif
|
#endif
|
||||||
this->tts_end_trigger_->trigger(url);
|
this->tts_end_trigger_->trigger(url);
|
||||||
});
|
});
|
||||||
State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case api::enums::VOICE_ASSISTANT_RUN_END: {
|
case api::enums::VOICE_ASSISTANT_RUN_END: {
|
||||||
@@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
|||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
if (this->media_player_ != nullptr) {
|
if (this->media_player_ != nullptr) {
|
||||||
this->tts_start_trigger_->trigger(msg.text);
|
this->tts_start_trigger_->trigger(msg.text);
|
||||||
|
|
||||||
|
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||||
|
|
||||||
if (!msg.preannounce_media_id.empty()) {
|
if (!msg.preannounce_media_id.empty()) {
|
||||||
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform();
|
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();
|
.perform();
|
||||||
this->continue_conversation_ = msg.start_conversation;
|
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_();
|
this->start_playback_timeout_();
|
||||||
|
|
||||||
if (this->continuous_) {
|
if (this->continuous_) {
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ struct Configuration {
|
|||||||
uint32_t max_active_wake_words;
|
uint32_t max_active_wake_words;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef USE_MEDIA_PLAYER
|
||||||
|
enum class MediaPlayerResponseState {
|
||||||
|
IDLE,
|
||||||
|
URL_SENT,
|
||||||
|
PLAYING,
|
||||||
|
FINISHED,
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
class VoiceAssistant : public Component {
|
class VoiceAssistant : public Component {
|
||||||
public:
|
public:
|
||||||
VoiceAssistant();
|
VoiceAssistant();
|
||||||
@@ -272,8 +281,8 @@ class VoiceAssistant : public Component {
|
|||||||
media_player::MediaPlayer *media_player_{nullptr};
|
media_player::MediaPlayer *media_player_{nullptr};
|
||||||
std::string tts_response_url_{""};
|
std::string tts_response_url_{""};
|
||||||
bool started_streaming_tts_{false};
|
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
|
#endif
|
||||||
|
|
||||||
bool local_output_{false};
|
bool local_output_{false};
|
||||||
|
|||||||
@@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def validate_ota_removed(config: ConfigType) -> ConfigType:
|
def validate_ota(config: ConfigType) -> ConfigType:
|
||||||
# Only raise error if OTA is explicitly enabled (True)
|
# The OTA option only accepts False to explicitly disable OTA for web_server
|
||||||
# If it's False or not specified, we can safely ignore it
|
# IMPORTANT: Setting ota: false ONLY affects the web_server component
|
||||||
if config.get(CONF_OTA):
|
# 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(
|
raise cv.Invalid(
|
||||||
f"The '{CONF_OTA}' option has been removed from 'web_server'. "
|
f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
|
||||||
f"Please use the new OTA platform structure instead:\n\n"
|
f"To enable OTA, please use the new OTA platform structure instead:\n\n"
|
||||||
f"ota:\n"
|
f"ota:\n"
|
||||||
f" - platform: web_server\n\n"
|
f" - platform: web_server\n\n"
|
||||||
f"See https://esphome.io/components/ota for more information."
|
f"See https://esphome.io/components/ota for more information."
|
||||||
@@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
web_server_base.WebServerBase
|
web_server_base.WebServerBase
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
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_LOG, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||||
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
||||||
@@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
default_url,
|
default_url,
|
||||||
validate_local,
|
validate_local,
|
||||||
validate_sorting_groups,
|
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_css_url(config[CONF_CSS_URL]))
|
||||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||||
# OTA is now handled by the web_server OTA platform
|
# 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]))
|
cg.add(var.set_expose_log(config[CONF_LOG]))
|
||||||
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
||||||
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef USE_CAPTIVE_PORTAL
|
||||||
|
#include "esphome/components/captive_portal/captive_portal.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
#include <Updater.h>
|
#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,
|
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||||
bool final) override;
|
bool final) override;
|
||||||
bool canHandle(AsyncWebServerRequest *request) const 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)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
@@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
|||||||
|
|
||||||
// Finalize
|
// Finalize
|
||||||
if (final) {
|
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());
|
this->ota_read_length_, request->contentLength());
|
||||||
|
|
||||||
// For Arduino framework, the Update library tracks expected size from firmware header
|
// 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) {
|
return json::build_json([this](JsonObject root) {
|
||||||
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
||||||
root["comment"] = App.get_comment();
|
root["comment"] = App.get_comment();
|
||||||
#ifdef USE_WEBSERVER_OTA
|
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
||||||
root["ota"] = true; // web_server OTA platform is configured
|
root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||||
#else
|
#else
|
||||||
root["ota"] = false;
|
root["ota"] = true;
|
||||||
#endif
|
#endif
|
||||||
root["log"] = this->expose_log_;
|
root["log"] = this->expose_log_;
|
||||||
root["lang"] = "en";
|
root["lang"] = "en";
|
||||||
@@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
|
|||||||
|
|
||||||
light::LightJSONSchema::dump_json(*obj, root);
|
light::LightJSONSchema::dump_json(*obj, root);
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray opt = root.createNestedArray("effects");
|
JsonArray opt = root["effects"].to<JsonArray>();
|
||||||
opt.add("None");
|
opt.add("None");
|
||||||
for (auto const &option : obj->get_effects()) {
|
for (auto const &option : obj->get_effects()) {
|
||||||
opt.add(option->get_name());
|
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) {
|
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);
|
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray opt = root.createNestedArray("option");
|
JsonArray opt = root["option"].to<JsonArray>();
|
||||||
for (auto &option : obj->traits.get_options()) {
|
for (auto &option : obj->traits.get_options()) {
|
||||||
opt.add(option);
|
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);
|
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
|
||||||
}
|
}
|
||||||
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
|
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) {
|
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||||
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
|
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
|
||||||
const auto traits = obj->get_traits();
|
const auto traits = obj->get_traits();
|
||||||
@@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
|||||||
char buf[16];
|
char buf[16];
|
||||||
|
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray opt = root.createNestedArray("modes");
|
JsonArray opt = root["modes"].to<JsonArray>();
|
||||||
for (climate::ClimateMode m : traits.get_supported_modes())
|
for (climate::ClimateMode m : traits.get_supported_modes())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
||||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
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())
|
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
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())
|
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
|
||||||
opt.add(custom_fan_mode);
|
opt.add(custom_fan_mode);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes()) {
|
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())
|
for (auto swing_mode : traits.get_supported_swing_modes())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && obj->preset.has_value()) {
|
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())
|
for (climate::ClimatePreset m : traits.get_supported_presets())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
|
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())
|
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
||||||
opt.add(custom_preset);
|
opt.add(custom_preset);
|
||||||
}
|
}
|
||||||
@@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
|||||||
root["state"] = root["target_temperature"];
|
root["state"] = root["target_temperature"];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -1618,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
|||||||
request->send(404);
|
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) {
|
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
||||||
auto *event = static_cast<event::Event *>(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;
|
root["event_type"] = event_type;
|
||||||
}
|
}
|
||||||
if (start_config == DETAIL_ALL) {
|
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()) {
|
for (auto const &event_type : obj->get_event_types()) {
|
||||||
event_types.add(event_type);
|
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);
|
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||||
}
|
}
|
||||||
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
|
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) {
|
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||||
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
||||||
root["value"] = obj->update_info.latest_version;
|
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);
|
this->add_sorting_info_(root, obj);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
#endif
|
#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 "
|
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>"));
|
"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 "
|
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>"));
|
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -40,4 +40,4 @@ async def to_code(config):
|
|||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
cg.add_library("ESP8266WiFi", None)
|
cg.add_library("ESP8266WiFi", None)
|
||||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
# 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
|
#ifdef USE_WEBSERVER_SORTING
|
||||||
for (auto &group : ws->sorting_groups_) {
|
for (auto &group : ws->sorting_groups_) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
message = json::build_json([group](JsonObject root) {
|
message = json::build_json([group](JsonObject root) {
|
||||||
root["name"] = group.second.name;
|
root["name"] = group.second.name;
|
||||||
root["sorting_weight"] = group.second.weight;
|
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
|
// 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
|
// 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/dns.h"
|
||||||
#include "lwip/err.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/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.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()) {
|
if (!manual_ip.has_value()) {
|
||||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||||
// https://github.com/esphome/issues/issues/6591
|
// https://github.com/esphome/issues/issues/6591
|
||||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
{
|
||||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
LwIPLock lock;
|
||||||
LOCK_TCPIP_CORE();
|
// 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
|
// No manual IP is set; use DHCP client
|
||||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/time.h"
|
#include "esphome/core/time.h"
|
||||||
#include "esphome/components/network/util.h"
|
#include "esphome/components/network/util.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <esp_wireguard.h>
|
#include <esp_wireguard.h>
|
||||||
#include <esp_wireguard_err.h>
|
#include <esp_wireguard_err.h>
|
||||||
@@ -42,7 +43,10 @@ void Wireguard::setup() {
|
|||||||
|
|
||||||
this->publish_enabled_state();
|
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) {
|
if (this->wg_initialized_ == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "Initialized");
|
ESP_LOGI(TAG, "Initialized");
|
||||||
@@ -249,7 +253,10 @@ void Wireguard::start_connection_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Starting 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) {
|
if (this->wg_connected_ == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "Connection started");
|
ESP_LOGI(TAG, "Connection started");
|
||||||
@@ -280,7 +287,10 @@ void Wireguard::start_connection_() {
|
|||||||
void Wireguard::stop_connection_() {
|
void Wireguard::stop_connection_() {
|
||||||
if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) {
|
if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) {
|
||||||
ESP_LOGD(TAG, "Stopping connection");
|
ESP_LOGD(TAG, "Stopping connection");
|
||||||
esp_wireguard_disconnect(&(this->wg_ctx_));
|
{
|
||||||
|
LwIPLock lock;
|
||||||
|
esp_wireguard_disconnect(&(this->wg_ctx_));
|
||||||
|
}
|
||||||
this->wg_connected_ = ESP_FAIL;
|
this->wg_connected_ = ESP_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.7.0b2"
|
__version__ = "2025.7.2"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
|||||||
@@ -309,6 +309,12 @@ void Application::disable_component_loop_(Component *component) {
|
|||||||
if (this->in_loop_ && i == this->current_loop_index_) {
|
if (this->in_loop_ && i == this->current_loop_index_) {
|
||||||
// Decrement so we'll process the swapped component next
|
// Decrement so we'll process the swapped component next
|
||||||
this->current_loop_index_--;
|
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;
|
return;
|
||||||
|
|||||||
@@ -158,14 +158,14 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
|||||||
void play_complex(Ts... x) override {
|
void play_complex(Ts... x) override {
|
||||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||||
this->num_running_++;
|
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; }
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
void play(Ts... x) override { /* ignore - see play_complex */
|
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...> {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
void Component::mark_failed() {
|
void Component::mark_failed() {
|
||||||
ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source());
|
ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source());
|
||||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
this->component_state_ |= COMPONENT_STATE_FAILED;
|
this->component_state_ |= COMPONENT_STATE_FAILED;
|
||||||
this->status_set_error();
|
this->status_set_error();
|
||||||
@@ -229,7 +229,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
|
|||||||
}
|
}
|
||||||
void Component::reset_to_construction_state() {
|
void Component::reset_to_construction_state() {
|
||||||
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
|
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
|
||||||
ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source());
|
ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source());
|
||||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
||||||
// Clear error status when resetting
|
// Clear error status when resetting
|
||||||
@@ -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));
|
App.scheduler.set_timeout(this, name, 0, std::move(f));
|
||||||
}
|
}
|
||||||
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
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
|
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,
|
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
|
||||||
float backoff_increase_factor) { // NOLINT
|
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_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||||
bool Component::is_ready() const {
|
bool Component::is_ready() const {
|
||||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
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;
|
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||||
}
|
}
|
||||||
bool Component::can_proceed() { return true; }
|
bool Component::can_proceed() { return true; }
|
||||||
@@ -275,14 +276,14 @@ void Component::status_set_warning(const char *message) {
|
|||||||
return;
|
return;
|
||||||
this->component_state_ |= STATUS_LED_WARNING;
|
this->component_state_ |= STATUS_LED_WARNING;
|
||||||
App.app_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) {
|
void Component::status_set_error(const char *message) {
|
||||||
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
||||||
return;
|
return;
|
||||||
this->component_state_ |= STATUS_LED_ERROR;
|
this->component_state_ |= STATUS_LED_ERROR;
|
||||||
App.app_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) {
|
if (strcmp(message, "unspecified") != 0) {
|
||||||
// Lazy allocate the error messages vector if needed
|
// Lazy allocate the error messages vector if needed
|
||||||
if (!component_error_messages) {
|
if (!component_error_messages) {
|
||||||
@@ -303,13 +304,13 @@ void Component::status_clear_warning() {
|
|||||||
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
||||||
return;
|
return;
|
||||||
this->component_state_ &= ~STATUS_LED_WARNING;
|
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() {
|
void Component::status_clear_error() {
|
||||||
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
|
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
|
||||||
return;
|
return;
|
||||||
this->component_state_ &= ~STATUS_LED_ERROR;
|
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) {
|
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
|
||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
@@ -403,7 +404,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
|||||||
}
|
}
|
||||||
if (should_warn) {
|
if (should_warn) {
|
||||||
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
||||||
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
|
ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
|
||||||
ESP_LOGW(TAG, "Components should block for at most 30 ms");
|
ESP_LOGW(TAG, "Components should block for at most 30 ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32)
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@@ -78,4 +78,4 @@ template<class T, uint8_t SIZE> class EventPool {
|
|||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#endif // defined(USE_ESP32)
|
||||||
|
|||||||
@@ -683,6 +683,23 @@ class InterruptLock {
|
|||||||
#endif
|
#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.
|
/** 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
|
* 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) { return this->reallocate(p, n, sizeof(T)); }
|
||||||
|
|
||||||
T *reallocate(T *p, size_t n, size_t manual_size) {
|
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;
|
T *ptr = nullptr;
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32)
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
#if defined(USE_ESP32)
|
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/task.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.
|
* 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
|
} // 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
|
// 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) {
|
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
|
// Early return if name is invalid - no items to cancel
|
||||||
if (name_cstr == nullptr || name_cstr[0] == '\0') {
|
if (name_cstr == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,16 +114,17 @@ class Scheduler {
|
|||||||
name_is_dynamic = false;
|
name_is_dynamic = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name || !name[0]) {
|
if (!name) {
|
||||||
|
// nullptr case - no name provided
|
||||||
name_.static_name = nullptr;
|
name_.static_name = nullptr;
|
||||||
} else if (make_copy) {
|
} else if (make_copy) {
|
||||||
// Make a copy for dynamic strings
|
// Make a copy for dynamic strings (including empty strings)
|
||||||
size_t len = strlen(name);
|
size_t len = strlen(name);
|
||||||
name_.dynamic_name = new char[len + 1];
|
name_.dynamic_name = new char[len + 1];
|
||||||
memcpy(name_.dynamic_name, name, len + 1);
|
memcpy(name_.dynamic_name, name, len + 1);
|
||||||
name_is_dynamic = true;
|
name_is_dynamic = true;
|
||||||
} else {
|
} else {
|
||||||
// Use static string directly
|
// Use static string directly (including empty strings)
|
||||||
name_.static_name = name;
|
name_.static_name = name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
|||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
|
"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)
|
cmd = ["platformio"] + list(args)
|
||||||
|
|
||||||
if not CORE.verbose:
|
if not CORE.verbose:
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ class RedirectText:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
self._write_color_replace(line)
|
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:
|
else:
|
||||||
self._write_color_replace(s)
|
self._write_color_replace(s)
|
||||||
|
|
||||||
@@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]:
|
|||||||
|
|
||||||
result.sort(key=lambda x: x.path)
|
result.sort(key=lambda x: x.path)
|
||||||
return result
|
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
|
# Sort to avoid changing build unflags order
|
||||||
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
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 = "[platformio]\n"
|
||||||
content += f"description = ESPHome {__version__}\n"
|
content += f"description = ESPHome {__version__}\n"
|
||||||
|
|
||||||
@@ -222,6 +225,9 @@ def write_platformio_project():
|
|||||||
write_gitignore()
|
write_gitignore()
|
||||||
write_platformio_ini(content)
|
write_platformio_ini(content)
|
||||||
|
|
||||||
|
# Write extra script for C++ specific flags
|
||||||
|
write_cxx_flags_script()
|
||||||
|
|
||||||
|
|
||||||
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
|
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -394,3 +400,20 @@ def write_gitignore():
|
|||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
with open(file=path, mode="w", encoding="utf-8") as f:
|
with open(file=path, mode="w", encoding="utf-8") as f:
|
||||||
f.write(GITIGNORE_CONTENT)
|
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 =
|
lib_deps =
|
||||||
esphome/noise-c@0.1.10 ; api
|
esphome/noise-c@0.1.10 ; api
|
||||||
improv/Improv@1.2.4 ; improv_serial / esp32_improv
|
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
|
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||||
@@ -138,7 +138,7 @@ lib_deps =
|
|||||||
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
|
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
|
||||||
Update ; ota,web_server_base (Arduino built-in)
|
Update ; ota,web_server_base (Arduino built-in)
|
||||||
${common:arduino.lib_deps}
|
${common:arduino.lib_deps}
|
||||||
ESP32Async/AsyncTCP@3.4.4 ; async_tcp
|
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
|
||||||
NetworkClientSecure ; http_request,nextion (Arduino built-in)
|
NetworkClientSecure ; http_request,nextion (Arduino built-in)
|
||||||
HTTPClient ; http_request,nextion (Arduino built-in)
|
HTTPClient ; http_request,nextion (Arduino built-in)
|
||||||
ESPmDNS ; mdns (Arduino built-in)
|
ESPmDNS ; mdns (Arduino built-in)
|
||||||
@@ -235,7 +235,7 @@ build_flags =
|
|||||||
-DUSE_ZEPHYR
|
-DUSE_ZEPHYR
|
||||||
-DUSE_NRF52
|
-DUSE_NRF52
|
||||||
lib_deps =
|
lib_deps =
|
||||||
bblanchon/ArduinoJson@7.0.0 ; json
|
bblanchon/ArduinoJson@7.4.2 ; json
|
||||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
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:
|
def test_web_server_ota_true_fails_validation() -> None:
|
||||||
"""Test that web_server with ota: true fails validation with helpful message."""
|
"""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 with ota: true should fail
|
||||||
config: ConfigType = {"ota": True}
|
config: ConfigType = {"ota": True}
|
||||||
|
|
||||||
with pytest.raises(cv.Invalid) as exc_info:
|
with pytest.raises(cv.Invalid) as exc_info:
|
||||||
validate_ota_removed(config)
|
validate_ota(config)
|
||||||
|
|
||||||
# Check error message contains migration instructions
|
# Check error message contains migration instructions
|
||||||
error_msg = str(exc_info.value)
|
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 "platform: web_server" in error_msg
|
||||||
assert "ota:" in error_msg
|
assert "ota:" in error_msg
|
||||||
|
|
||||||
|
|
||||||
def test_web_server_ota_false_passes_validation() -> None:
|
def test_web_server_ota_false_passes_validation() -> None:
|
||||||
"""Test that web_server with ota: false passes validation."""
|
"""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 with ota: false should pass
|
||||||
config: ConfigType = {"ota": False}
|
config: ConfigType = {"ota": False}
|
||||||
result = validate_ota_removed(config)
|
result = validate_ota(config)
|
||||||
assert result == config
|
assert result == config
|
||||||
|
|
||||||
# Config without ota should also pass
|
# Config without ota should also pass
|
||||||
config: ConfigType = {}
|
config: ConfigType = {}
|
||||||
result = validate_ota_removed(config)
|
result = validate_ota(config)
|
||||||
assert result == 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
|
- id: ldo_4
|
||||||
channel: 4
|
channel: 4
|
||||||
voltage: 2.0V
|
voltage: 2.0V
|
||||||
|
setup_priority: 900
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
on_boot:
|
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
|
text_color: 0xFFFFFF
|
||||||
scales:
|
scales:
|
||||||
- ticks:
|
- ticks:
|
||||||
width: 1
|
width: !lambda return 1;
|
||||||
count: 61
|
count: 61
|
||||||
length: 20
|
length: 20%
|
||||||
color: 0xFFFFFF
|
color: 0xFFFFFF
|
||||||
range_from: 0
|
range_from: 0
|
||||||
range_to: 60
|
range_to: 60
|
||||||
angle_range: 360
|
angle_range: 360
|
||||||
rotation: 270
|
rotation: !lambda return 2700;
|
||||||
indicators:
|
indicators:
|
||||||
|
- tick_style:
|
||||||
|
start_value: 0
|
||||||
|
end_value: 60
|
||||||
|
color_start: 0x0000bd
|
||||||
|
color_end: 0xbd0000
|
||||||
|
width: !lambda return 1;
|
||||||
- line:
|
- line:
|
||||||
opa: 50%
|
opa: 50%
|
||||||
id: minute_hand
|
id: minute_hand
|
||||||
color: 0xFF0000
|
color: 0xFF0000
|
||||||
r_mod: -1
|
r_mod: !lambda return -1;
|
||||||
width: 3
|
width: !lambda return 3;
|
||||||
-
|
-
|
||||||
angle_range: 330
|
angle_range: 330
|
||||||
rotation: 300
|
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
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
|
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/tests"]
|
CODEOWNERS = ["@esphome/tests"]
|
||||||
|
|
||||||
@@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
|
|||||||
LoopTestISRComponent = loop_test_component_ns.class_(
|
LoopTestISRComponent = loop_test_component_ns.class_(
|
||||||
"LoopTestISRComponent", cg.Component
|
"LoopTestISRComponent", cg.Component
|
||||||
)
|
)
|
||||||
|
LoopTestUpdateComponent = loop_test_component_ns.class_(
|
||||||
|
"LoopTestUpdateComponent", cg.PollingComponent
|
||||||
|
)
|
||||||
|
|
||||||
CONF_DISABLE_AFTER = "disable_after"
|
CONF_DISABLE_AFTER = "disable_after"
|
||||||
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
||||||
CONF_ISR_COMPONENTS = "isr_components"
|
CONF_ISR_COMPONENTS = "isr_components"
|
||||||
|
CONF_UPDATE_COMPONENTS = "update_components"
|
||||||
|
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
|
||||||
|
|
||||||
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
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(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||||
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
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_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)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
@@ -94,3 +111,12 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(isr_config[CONF_ID])
|
var = cg.new_Pvariable(isr_config[CONF_ID])
|
||||||
await cg.register_component(var, isr_config)
|
await cg.register_component(var, isr_config)
|
||||||
cg.add(var.set_name(isr_config[CONF_NAME]))
|
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();
|
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 loop_test_component
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace loop_test_component {
|
namespace loop_test_component {
|
||||||
@@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
|
|||||||
LoopTestComponent *parent_;
|
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 loop_test_component
|
||||||
} // namespace esphome
|
} // 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