esphome 2025.7.0b5__py3-none-any.whl → 2025.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- esphome/components/api/homeassistant_service.h +13 -1
- esphome/components/e131/e131_packet.cpp +7 -1
- esphome/components/esp32/helpers.cpp +40 -0
- esphome/components/esp32_camera/__init__.py +22 -0
- esphome/components/esp8266/helpers.cpp +4 -0
- esphome/components/ethernet/ethernet_component.cpp +9 -2
- esphome/components/gpio/binary_sensor/__init__.py +15 -1
- esphome/components/libretiny/helpers.cpp +4 -0
- esphome/components/logger/__init__.py +2 -2
- esphome/components/lvgl/types.py +2 -2
- esphome/components/lvgl/widgets/meter.py +4 -1
- esphome/components/mqtt/mqtt_client.cpp +8 -4
- esphome/components/rp2040/helpers.cpp +4 -0
- esphome/components/speaker/media_player/audio_pipeline.cpp +14 -2
- esphome/components/speaker/media_player/audio_pipeline.h +1 -0
- esphome/components/voice_assistant/voice_assistant.cpp +52 -20
- esphome/components/voice_assistant/voice_assistant.h +11 -2
- esphome/components/web_server/__init__.py +14 -9
- esphome/components/web_server/ota/ota_web_server.cpp +21 -2
- esphome/components/web_server/web_server.cpp +6 -4
- esphome/components/web_server/web_server_v1.cpp +3 -1
- esphome/components/wifi/wifi_component_esp32_arduino.cpp +9 -22
- esphome/components/wireguard/wireguard.cpp +13 -3
- esphome/const.py +1 -1
- esphome/core/base_automation.h +2 -2
- esphome/core/component.cpp +2 -2
- esphome/core/event_pool.h +2 -2
- esphome/core/helpers.h +17 -0
- esphome/core/lock_free_queue.h +2 -7
- esphome/core/scheduler.cpp +1 -1
- esphome/core/scheduler.h +4 -3
- esphome/util.py +38 -0
- {esphome-2025.7.0b5.dist-info → esphome-2025.7.2.dist-info}/METADATA +1 -1
- {esphome-2025.7.0b5.dist-info → esphome-2025.7.2.dist-info}/RECORD +38 -38
- {esphome-2025.7.0b5.dist-info → esphome-2025.7.2.dist-info}/WHEEL +0 -0
- {esphome-2025.7.0b5.dist-info → esphome-2025.7.2.dist-info}/entry_points.txt +0 -0
- {esphome-2025.7.0b5.dist-info → esphome-2025.7.2.dist-info}/licenses/LICENSE +0 -0
- {esphome-2025.7.0b5.dist-info → esphome-2025.7.2.dist-info}/top_level.txt +0 -0
|
@@ -11,6 +11,18 @@ namespace esphome {
|
|
|
11
11
|
namespace api {
|
|
12
12
|
|
|
13
13
|
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
|
14
|
+
private:
|
|
15
|
+
// Helper to convert value to string - handles the case where value is already a string
|
|
16
|
+
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
|
17
|
+
|
|
18
|
+
// Overloads for string types - needed because std::to_string doesn't support them
|
|
19
|
+
static std::string value_to_string(char *val) {
|
|
20
|
+
return val ? std::string(val) : std::string();
|
|
21
|
+
} // For lambdas returning char* (e.g., itoa)
|
|
22
|
+
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
|
23
|
+
static std::string value_to_string(const std::string &val) { return val; }
|
|
24
|
+
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
|
25
|
+
|
|
14
26
|
public:
|
|
15
27
|
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
|
16
28
|
|
|
@@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|
|
19
31
|
|
|
20
32
|
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
|
21
33
|
TemplatableStringValue(F f)
|
|
22
|
-
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return
|
|
34
|
+
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
|
|
23
35
|
};
|
|
24
36
|
|
|
25
37
|
template<typename... Ts> class TemplatableKeyValuePair {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "esphome/components/network/ip_address.h"
|
|
5
5
|
#include "esphome/core/log.h"
|
|
6
6
|
#include "esphome/core/util.h"
|
|
7
|
+
#include "esphome/core/helpers.h"
|
|
7
8
|
|
|
8
9
|
#include <lwip/igmp.h>
|
|
9
10
|
#include <lwip/init.h>
|
|
@@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
|
|
|
71
72
|
ip4_addr_t multicast_addr =
|
|
72
73
|
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
err_t err;
|
|
76
|
+
{
|
|
77
|
+
LwIPLock lock;
|
|
78
|
+
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
if (err) {
|
|
77
82
|
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) {
|
|
|
104
109
|
if (listen_method_ == E131_MULTICAST) {
|
|
105
110
|
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
|
106
111
|
|
|
112
|
+
LwIPLock lock;
|
|
107
113
|
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "esphome/core/helpers.h"
|
|
2
|
+
#include "esphome/core/defines.h"
|
|
2
3
|
|
|
3
4
|
#ifdef USE_ESP32
|
|
4
5
|
|
|
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
|
|
30
31
|
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
|
31
32
|
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
|
32
33
|
|
|
34
|
+
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
|
35
|
+
#include "lwip/priv/tcpip_priv.h"
|
|
36
|
+
#endif
|
|
37
|
+
|
|
38
|
+
LwIPLock::LwIPLock() {
|
|
39
|
+
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
|
40
|
+
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
|
|
41
|
+
// its internal state. Any thread can take this lock to safely access lwIP APIs.
|
|
42
|
+
//
|
|
43
|
+
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
|
|
44
|
+
// already holds the lwIP core lock. This prevents recursive locking attempts and
|
|
45
|
+
// allows nested LwIPLock instances to work correctly.
|
|
46
|
+
//
|
|
47
|
+
// If we don't already hold the lock, acquire it. This will block until the lock
|
|
48
|
+
// is available if another thread currently holds it.
|
|
49
|
+
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
|
50
|
+
LOCK_TCPIP_CORE();
|
|
51
|
+
}
|
|
52
|
+
#endif
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
LwIPLock::~LwIPLock() {
|
|
56
|
+
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
|
57
|
+
// Only release the lwIP core lock if this thread currently holds it.
|
|
58
|
+
//
|
|
59
|
+
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
|
|
60
|
+
// ownership tracking. It returns true only if the current thread is registered
|
|
61
|
+
// as the lock holder.
|
|
62
|
+
//
|
|
63
|
+
// This check is essential because:
|
|
64
|
+
// 1. We may not have acquired the lock in the constructor (if we already held it)
|
|
65
|
+
// 2. The lock might have been released by other means between constructor and destructor
|
|
66
|
+
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
|
|
67
|
+
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
|
68
|
+
UNLOCK_TCPIP_CORE();
|
|
69
|
+
}
|
|
70
|
+
#endif
|
|
71
|
+
}
|
|
72
|
+
|
|
33
73
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
|
34
74
|
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
|
35
75
|
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from esphome import automation, pins
|
|
2
4
|
import esphome.codegen as cg
|
|
3
5
|
from esphome.components import i2c
|
|
@@ -8,6 +10,7 @@ from esphome.const import (
|
|
|
8
10
|
CONF_CONTRAST,
|
|
9
11
|
CONF_DATA_PINS,
|
|
10
12
|
CONF_FREQUENCY,
|
|
13
|
+
CONF_I2C,
|
|
11
14
|
CONF_I2C_ID,
|
|
12
15
|
CONF_ID,
|
|
13
16
|
CONF_PIN,
|
|
@@ -20,6 +23,9 @@ from esphome.const import (
|
|
|
20
23
|
)
|
|
21
24
|
from esphome.core import CORE
|
|
22
25
|
from esphome.core.entity_helpers import setup_entity
|
|
26
|
+
import esphome.final_validate as fv
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
23
29
|
|
|
24
30
|
DEPENDENCIES = ["esp32"]
|
|
25
31
|
|
|
@@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All(
|
|
|
250
256
|
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
|
|
251
257
|
)
|
|
252
258
|
|
|
259
|
+
|
|
260
|
+
def _final_validate(config):
|
|
261
|
+
if CONF_I2C_PINS not in config:
|
|
262
|
+
return
|
|
263
|
+
fconf = fv.full_config.get()
|
|
264
|
+
if fconf.get(CONF_I2C):
|
|
265
|
+
raise cv.Invalid(
|
|
266
|
+
"The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
|
|
267
|
+
)
|
|
268
|
+
_LOGGER.warning(
|
|
269
|
+
"The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
FINAL_VALIDATE_SCHEMA = _final_validate
|
|
274
|
+
|
|
253
275
|
SETTERS = {
|
|
254
276
|
# pin assignment
|
|
255
277
|
CONF_DATA_PINS: "set_data_pins",
|
|
@@ -22,6 +22,10 @@ void Mutex::unlock() {}
|
|
|
22
22
|
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
|
23
23
|
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
|
24
24
|
|
|
25
|
+
// ESP8266 doesn't support lwIP core locking, so this is a no-op
|
|
26
|
+
LwIPLock::LwIPLock() {}
|
|
27
|
+
LwIPLock::~LwIPLock() {}
|
|
28
|
+
|
|
25
29
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
|
26
30
|
wifi_get_macaddr(STATION_IF, mac);
|
|
27
31
|
}
|
|
@@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
|
423
|
+
LwIPLock lock;
|
|
423
424
|
const ip_addr_t *dns_ip = dns_getserver(num);
|
|
424
425
|
return dns_ip;
|
|
425
426
|
}
|
|
@@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
|
|
|
527
528
|
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
|
528
529
|
|
|
529
530
|
if (this->manual_ip_.has_value()) {
|
|
531
|
+
LwIPLock lock;
|
|
530
532
|
if (this->manual_ip_->dns1.is_set()) {
|
|
531
533
|
ip_addr_t d;
|
|
532
534
|
d = this->manual_ip_->dns1;
|
|
@@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
|
|
|
559
561
|
void EthernetComponent::dump_connect_params_() {
|
|
560
562
|
esp_netif_ip_info_t ip;
|
|
561
563
|
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
|
562
|
-
const ip_addr_t *dns_ip1
|
|
563
|
-
const ip_addr_t *dns_ip2
|
|
564
|
+
const ip_addr_t *dns_ip1;
|
|
565
|
+
const ip_addr_t *dns_ip2;
|
|
566
|
+
{
|
|
567
|
+
LwIPLock lock;
|
|
568
|
+
dns_ip1 = dns_getserver(0);
|
|
569
|
+
dns_ip2 = dns_getserver(1);
|
|
570
|
+
}
|
|
564
571
|
|
|
565
572
|
ESP_LOGCONFIG(TAG,
|
|
566
573
|
" IP Address: %s\n"
|
|
@@ -29,7 +29,21 @@ CONFIG_SCHEMA = (
|
|
|
29
29
|
.extend(
|
|
30
30
|
{
|
|
31
31
|
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
|
32
|
-
|
|
32
|
+
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
|
|
33
|
+
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
|
34
|
+
# stable operation on these platforms. Future maintainers should verify platform
|
|
35
|
+
# capabilities before changing this default behavior.
|
|
36
|
+
cv.SplitDefault(
|
|
37
|
+
CONF_USE_INTERRUPT,
|
|
38
|
+
bk72xx=False,
|
|
39
|
+
esp32=True,
|
|
40
|
+
esp8266=True,
|
|
41
|
+
host=True,
|
|
42
|
+
ln882x=False,
|
|
43
|
+
nrf52=True,
|
|
44
|
+
rp2040=True,
|
|
45
|
+
rtl87xx=False,
|
|
46
|
+
): cv.boolean,
|
|
33
47
|
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
|
|
34
48
|
INTERRUPT_TYPES, upper=True
|
|
35
49
|
),
|
|
@@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
|
|
26
26
|
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
|
27
27
|
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
|
28
28
|
|
|
29
|
+
// LibreTiny doesn't support lwIP core locking, so this is a no-op
|
|
30
|
+
LwIPLock::LwIPLock() {}
|
|
31
|
+
LwIPLock::~LwIPLock() {}
|
|
32
|
+
|
|
29
33
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
|
30
34
|
WiFi.macAddress(mac);
|
|
31
35
|
}
|
|
@@ -183,7 +183,7 @@ def validate_local_no_higher_than_global(value):
|
|
|
183
183
|
Logger = logger_ns.class_("Logger", cg.Component)
|
|
184
184
|
LoggerMessageTrigger = logger_ns.class_(
|
|
185
185
|
"LoggerMessageTrigger",
|
|
186
|
-
automation.Trigger.template(cg.
|
|
186
|
+
automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr),
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
|
|
@@ -368,7 +368,7 @@ async def to_code(config):
|
|
|
368
368
|
await automation.build_automation(
|
|
369
369
|
trigger,
|
|
370
370
|
[
|
|
371
|
-
(cg.
|
|
371
|
+
(cg.uint8, "level"),
|
|
372
372
|
(cg.const_char_ptr, "tag"),
|
|
373
373
|
(cg.const_char_ptr, "message"),
|
|
374
374
|
],
|
esphome/components/lvgl/types.py
CHANGED
|
@@ -192,7 +192,7 @@ class WidgetType:
|
|
|
192
192
|
|
|
193
193
|
class NumberType(WidgetType):
|
|
194
194
|
def get_max(self, config: dict):
|
|
195
|
-
return int(config
|
|
195
|
+
return int(config.get(CONF_MAX_VALUE, 100))
|
|
196
196
|
|
|
197
197
|
def get_min(self, config: dict):
|
|
198
|
-
return int(config
|
|
198
|
+
return int(config.get(CONF_MIN_VALUE, 0))
|
|
@@ -14,6 +14,7 @@ from esphome.const import (
|
|
|
14
14
|
CONF_VALUE,
|
|
15
15
|
CONF_WIDTH,
|
|
16
16
|
)
|
|
17
|
+
from esphome.cpp_generator import IntLiteral
|
|
17
18
|
|
|
18
19
|
from ..automation import action_to_code
|
|
19
20
|
from ..defines import (
|
|
@@ -188,6 +189,8 @@ class MeterType(WidgetType):
|
|
|
188
189
|
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
|
189
190
|
if CONF_ROTATION in scale_conf:
|
|
190
191
|
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
|
|
192
|
+
if isinstance(rotation, IntLiteral):
|
|
193
|
+
rotation = int(str(rotation)) // 10
|
|
191
194
|
with LocalVariable(
|
|
192
195
|
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
|
193
196
|
) as meter_var:
|
|
@@ -264,7 +267,7 @@ class MeterType(WidgetType):
|
|
|
264
267
|
color_start,
|
|
265
268
|
color_end,
|
|
266
269
|
v[CONF_LOCAL],
|
|
267
|
-
size.process(v[CONF_WIDTH]),
|
|
270
|
+
await size.process(v[CONF_WIDTH]),
|
|
268
271
|
),
|
|
269
272
|
)
|
|
270
273
|
if t == CONF_IMAGE:
|
|
@@ -193,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
|
|
|
193
193
|
this->dns_resolve_error_ = false;
|
|
194
194
|
this->dns_resolved_ = false;
|
|
195
195
|
ip_addr_t addr;
|
|
196
|
+
err_t err;
|
|
197
|
+
{
|
|
198
|
+
LwIPLock lock;
|
|
196
199
|
#if USE_NETWORK_IPV6
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
|
201
|
+
this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
|
199
202
|
#else
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
|
204
|
+
this, LWIP_DNS_ADDRTYPE_IPV4);
|
|
202
205
|
#endif /* USE_NETWORK_IPV6 */
|
|
206
|
+
}
|
|
203
207
|
switch (err) {
|
|
204
208
|
case ERR_OK: {
|
|
205
209
|
// Got IP immediately
|
|
@@ -44,6 +44,10 @@ void Mutex::unlock() {}
|
|
|
44
44
|
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
|
|
45
45
|
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
|
46
46
|
|
|
47
|
+
// RP2040 doesn't support lwIP core locking, so this is a no-op
|
|
48
|
+
LwIPLock::LwIPLock() {}
|
|
49
|
+
LwIPLock::~LwIPLock() {}
|
|
50
|
+
|
|
47
51
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
|
48
52
|
#ifdef USE_WIFI
|
|
49
53
|
WiFi.macAddress(mac);
|
|
@@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() {
|
|
|
200
200
|
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
|
|
201
201
|
this->delete_tasks_();
|
|
202
202
|
if (this->hard_stop_) {
|
|
203
|
-
// Stop command was sent, so immediately end
|
|
203
|
+
// Stop command was sent, so immediately end the playback
|
|
204
204
|
this->speaker_->stop();
|
|
205
205
|
this->hard_stop_ = false;
|
|
206
206
|
} else {
|
|
@@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
this->is_playing_ = false;
|
|
213
|
-
|
|
213
|
+
if (!this->speaker_->is_running()) {
|
|
214
|
+
return AudioPipelineState::STOPPED;
|
|
215
|
+
} else {
|
|
216
|
+
this->is_finishing_ = true;
|
|
217
|
+
}
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
if (this->pause_state_) {
|
|
217
221
|
return AudioPipelineState::PAUSED;
|
|
218
222
|
}
|
|
219
223
|
|
|
224
|
+
if (this->is_finishing_) {
|
|
225
|
+
if (!this->speaker_->is_running()) {
|
|
226
|
+
this->is_finishing_ = false;
|
|
227
|
+
} else {
|
|
228
|
+
return AudioPipelineState::PLAYING;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
220
232
|
if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
|
|
221
233
|
// No tasks are running, so the pipeline is stopped.
|
|
222
234
|
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
|
|
@@ -35,6 +35,27 @@ void VoiceAssistant::setup() {
|
|
|
35
35
|
temp_ring_buffer->write((void *) data.data(), data.size());
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
#ifdef USE_MEDIA_PLAYER
|
|
40
|
+
if (this->media_player_ != nullptr) {
|
|
41
|
+
this->media_player_->add_on_state_callback([this]() {
|
|
42
|
+
switch (this->media_player_->state) {
|
|
43
|
+
case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING:
|
|
44
|
+
if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) {
|
|
45
|
+
// State changed to announcing after receiving the url
|
|
46
|
+
this->media_player_response_state_ = MediaPlayerResponseState::PLAYING;
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
if (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING) {
|
|
51
|
+
// No longer announcing the TTS response
|
|
52
|
+
this->media_player_response_state_ = MediaPlayerResponseState::FINISHED;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
#endif
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
|
@@ -223,6 +244,13 @@ void VoiceAssistant::loop() {
|
|
|
223
244
|
msg.wake_word_phrase = this->wake_word_;
|
|
224
245
|
this->wake_word_ = "";
|
|
225
246
|
|
|
247
|
+
// Reset media player state tracking
|
|
248
|
+
#ifdef USE_MEDIA_PLAYER
|
|
249
|
+
if (this->media_player_ != nullptr) {
|
|
250
|
+
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
|
251
|
+
}
|
|
252
|
+
#endif
|
|
253
|
+
|
|
226
254
|
if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
|
|
227
255
|
ESP_LOGW(TAG, "Could not request start");
|
|
228
256
|
this->error_trigger_->trigger("not-connected", "Could not request start");
|
|
@@ -314,17 +342,10 @@ void VoiceAssistant::loop() {
|
|
|
314
342
|
#endif
|
|
315
343
|
#ifdef USE_MEDIA_PLAYER
|
|
316
344
|
if (this->media_player_ != nullptr) {
|
|
317
|
-
playing = (this->
|
|
318
|
-
|
|
319
|
-
if (playing && this->media_player_wait_for_announcement_start_) {
|
|
320
|
-
// Announcement has started playing, wait for it to finish
|
|
321
|
-
this->media_player_wait_for_announcement_start_ = false;
|
|
322
|
-
this->media_player_wait_for_announcement_end_ = true;
|
|
323
|
-
}
|
|
345
|
+
playing = (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING);
|
|
324
346
|
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
this->media_player_wait_for_announcement_end_ = false;
|
|
347
|
+
if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) {
|
|
348
|
+
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
|
328
349
|
this->cancel_timeout("playing");
|
|
329
350
|
ESP_LOGD(TAG, "Announcement finished playing");
|
|
330
351
|
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
|
@@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() {
|
|
|
555
576
|
break;
|
|
556
577
|
case State::AWAITING_RESPONSE:
|
|
557
578
|
this->signal_stop_();
|
|
558
|
-
|
|
579
|
+
break;
|
|
559
580
|
case State::STREAMING_RESPONSE:
|
|
560
581
|
#ifdef USE_MEDIA_PLAYER
|
|
561
582
|
// Stop any ongoing media player announcement
|
|
@@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() {
|
|
|
565
586
|
.set_announcement(true)
|
|
566
587
|
.perform();
|
|
567
588
|
}
|
|
589
|
+
if (this->started_streaming_tts_) {
|
|
590
|
+
// Haven't reached the TTS_END stage, so send the stop signal to HA.
|
|
591
|
+
this->signal_stop_();
|
|
592
|
+
}
|
|
568
593
|
#endif
|
|
569
594
|
break;
|
|
570
595
|
case State::RESPONSE_FINISHED:
|
|
@@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|
|
648
673
|
if (this->media_player_ != nullptr) {
|
|
649
674
|
for (const auto &arg : msg.data) {
|
|
650
675
|
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
|
|
676
|
+
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
|
677
|
+
|
|
651
678
|
this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform();
|
|
652
679
|
|
|
653
|
-
this->media_player_wait_for_announcement_start_ = true;
|
|
654
|
-
this->media_player_wait_for_announcement_end_ = false;
|
|
655
680
|
this->started_streaming_tts_ = true;
|
|
681
|
+
this->start_playback_timeout_();
|
|
682
|
+
|
|
656
683
|
tts_url_for_trigger = this->tts_response_url_;
|
|
657
684
|
this->tts_response_url_.clear(); // Reset streaming URL
|
|
685
|
+
this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE);
|
|
658
686
|
}
|
|
659
687
|
}
|
|
660
688
|
}
|
|
@@ -713,18 +741,22 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|
|
713
741
|
this->defer([this, url]() {
|
|
714
742
|
#ifdef USE_MEDIA_PLAYER
|
|
715
743
|
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
|
|
744
|
+
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
|
745
|
+
|
|
716
746
|
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
|
|
717
747
|
|
|
718
|
-
this->media_player_wait_for_announcement_start_ = true;
|
|
719
|
-
this->media_player_wait_for_announcement_end_ = false;
|
|
720
|
-
// Start the playback timeout, as the media player state isn't immediately updated
|
|
721
748
|
this->start_playback_timeout_();
|
|
722
749
|
}
|
|
750
|
+
this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
|
|
723
751
|
#endif
|
|
724
752
|
this->tts_end_trigger_->trigger(url);
|
|
725
753
|
});
|
|
726
754
|
State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE;
|
|
727
|
-
|
|
755
|
+
if (new_state != this->state_) {
|
|
756
|
+
// Don't needlessly change the state. The intent progress stage may have already changed the state to streaming
|
|
757
|
+
// response.
|
|
758
|
+
this->set_state_(new_state, new_state);
|
|
759
|
+
}
|
|
728
760
|
break;
|
|
729
761
|
}
|
|
730
762
|
case api::enums::VOICE_ASSISTANT_RUN_END: {
|
|
@@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
|
|
875
907
|
#ifdef USE_MEDIA_PLAYER
|
|
876
908
|
if (this->media_player_ != nullptr) {
|
|
877
909
|
this->tts_start_trigger_->trigger(msg.text);
|
|
910
|
+
|
|
911
|
+
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
|
912
|
+
|
|
878
913
|
if (!msg.preannounce_media_id.empty()) {
|
|
879
914
|
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform();
|
|
880
915
|
}
|
|
@@ -886,9 +921,6 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
|
|
886
921
|
.perform();
|
|
887
922
|
this->continue_conversation_ = msg.start_conversation;
|
|
888
923
|
|
|
889
|
-
this->media_player_wait_for_announcement_start_ = true;
|
|
890
|
-
this->media_player_wait_for_announcement_end_ = false;
|
|
891
|
-
// Start the playback timeout, as the media player state isn't immediately updated
|
|
892
924
|
this->start_playback_timeout_();
|
|
893
925
|
|
|
894
926
|
if (this->continuous_) {
|
|
@@ -90,6 +90,15 @@ struct Configuration {
|
|
|
90
90
|
uint32_t max_active_wake_words;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
#ifdef USE_MEDIA_PLAYER
|
|
94
|
+
enum class MediaPlayerResponseState {
|
|
95
|
+
IDLE,
|
|
96
|
+
URL_SENT,
|
|
97
|
+
PLAYING,
|
|
98
|
+
FINISHED,
|
|
99
|
+
};
|
|
100
|
+
#endif
|
|
101
|
+
|
|
93
102
|
class VoiceAssistant : public Component {
|
|
94
103
|
public:
|
|
95
104
|
VoiceAssistant();
|
|
@@ -272,8 +281,8 @@ class VoiceAssistant : public Component {
|
|
|
272
281
|
media_player::MediaPlayer *media_player_{nullptr};
|
|
273
282
|
std::string tts_response_url_{""};
|
|
274
283
|
bool started_streaming_tts_{false};
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
|
|
285
|
+
MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE};
|
|
277
286
|
#endif
|
|
278
287
|
|
|
279
288
|
bool local_output_{false};
|
|
@@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
|
|
|
74
74
|
return config
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
77
|
+
def validate_ota(config: ConfigType) -> ConfigType:
|
|
78
|
+
# The OTA option only accepts False to explicitly disable OTA for web_server
|
|
79
|
+
# IMPORTANT: Setting ota: false ONLY affects the web_server component
|
|
80
|
+
# The captive_portal component will still be able to perform OTA updates
|
|
81
|
+
if CONF_OTA in config and config[CONF_OTA] is not False:
|
|
81
82
|
raise cv.Invalid(
|
|
82
|
-
f"The '{CONF_OTA}' option
|
|
83
|
-
f"
|
|
83
|
+
f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
|
|
84
|
+
f"To enable OTA, please use the new OTA platform structure instead:\n\n"
|
|
84
85
|
f"ota:\n"
|
|
85
86
|
f" - platform: web_server\n\n"
|
|
86
87
|
f"See https://esphome.io/components/ota for more information."
|
|
@@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
|
|
185
186
|
web_server_base.WebServerBase
|
|
186
187
|
),
|
|
187
188
|
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
|
188
|
-
cv.Optional(CONF_OTA
|
|
189
|
+
cv.Optional(CONF_OTA): cv.boolean,
|
|
189
190
|
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
|
190
191
|
cv.Optional(CONF_LOCAL): cv.boolean,
|
|
191
192
|
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
|
@@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
|
|
|
203
204
|
default_url,
|
|
204
205
|
validate_local,
|
|
205
206
|
validate_sorting_groups,
|
|
206
|
-
|
|
207
|
+
validate_ota,
|
|
207
208
|
)
|
|
208
209
|
|
|
209
210
|
|
|
@@ -288,7 +289,11 @@ async def to_code(config):
|
|
|
288
289
|
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
|
289
290
|
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
|
290
291
|
# OTA is now handled by the web_server OTA platform
|
|
291
|
-
# The CONF_OTA option is kept
|
|
292
|
+
# The CONF_OTA option is kept to allow explicitly disabling OTA for web_server
|
|
293
|
+
# IMPORTANT: This ONLY affects the web_server component, NOT captive_portal
|
|
294
|
+
# Captive portal will still be able to perform OTA updates even when this is set
|
|
295
|
+
if config.get(CONF_OTA) is False:
|
|
296
|
+
cg.add_define("USE_WEBSERVER_OTA_DISABLED")
|
|
292
297
|
cg.add(var.set_expose_log(config[CONF_LOG]))
|
|
293
298
|
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
|
294
299
|
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
#include "esphome/core/application.h"
|
|
6
6
|
#include "esphome/core/log.h"
|
|
7
7
|
|
|
8
|
+
#ifdef USE_CAPTIVE_PORTAL
|
|
9
|
+
#include "esphome/components/captive_portal/captive_portal.h"
|
|
10
|
+
#endif
|
|
11
|
+
|
|
8
12
|
#ifdef USE_ARDUINO
|
|
9
13
|
#ifdef USE_ESP8266
|
|
10
14
|
#include <Updater.h>
|
|
@@ -25,7 +29,22 @@ class OTARequestHandler : public AsyncWebHandler {
|
|
|
25
29
|
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
|
26
30
|
bool final) override;
|
|
27
31
|
bool canHandle(AsyncWebServerRequest *request) const override {
|
|
28
|
-
|
|
32
|
+
// Check if this is an OTA update request
|
|
33
|
+
bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST;
|
|
34
|
+
|
|
35
|
+
#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
|
|
36
|
+
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
|
|
37
|
+
// Captive portal can still perform OTA updates - check if request is from active captive portal
|
|
38
|
+
// Note: global_captive_portal is the standard way components communicate in ESPHome
|
|
39
|
+
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
|
|
40
|
+
captive_portal::global_captive_portal->is_active();
|
|
41
|
+
#elif defined(USE_WEBSERVER_OTA_DISABLED)
|
|
42
|
+
// OTA disabled for web_server and no captive portal compiled in
|
|
43
|
+
return false;
|
|
44
|
+
#else
|
|
45
|
+
// OTA enabled for web_server
|
|
46
|
+
return is_ota_request;
|
|
47
|
+
#endif
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
|
@@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
|
|
152
171
|
|
|
153
172
|
// Finalize
|
|
154
173
|
if (final) {
|
|
155
|
-
ESP_LOGD(TAG, "OTA final chunk: index=%
|
|
174
|
+
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len,
|
|
156
175
|
this->ota_read_length_, request->contentLength());
|
|
157
176
|
|
|
158
177
|
// For Arduino framework, the Update library tracks expected size from firmware header
|