esphome 2025.7.1__py3-none-any.whl → 2025.7.3__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 +3 -0
- esphome/components/bme680_bsec/__init__.py +10 -2
- esphome/components/esp32_camera/__init__.py +22 -0
- esphome/components/esp32_touch/esp32_touch_v1.cpp +16 -10
- esphome/components/fastled_clockless/light.py +17 -2
- esphome/components/fastled_spi/light.py +10 -0
- esphome/components/gpio/binary_sensor/__init__.py +34 -2
- esphome/components/logger/__init__.py +2 -2
- esphome/components/lvgl/types.py +2 -2
- esphome/components/lvgl/widgets/meter.py +3 -0
- esphome/components/neopixelbus/light.py +10 -1
- esphome/components/sdl/sdl_esphome.cpp +3 -0
- esphome/components/speaker/media_player/audio_pipeline.cpp +14 -2
- esphome/components/speaker/media_player/audio_pipeline.h +1 -0
- esphome/components/tuya/fan/__init__.py +6 -5
- esphome/components/voice_assistant/voice_assistant.cpp +52 -20
- esphome/components/voice_assistant/voice_assistant.h +11 -2
- esphome/components/web_server/ota/ota_web_server.cpp +2 -2
- esphome/components/web_server/web_server.cpp +3 -1
- esphome/components/wireguard/wireguard.cpp +13 -3
- esphome/config_validation.py +58 -26
- esphome/const.py +1 -1
- esphome/core/application.cpp +39 -24
- esphome/core/application.h +2 -0
- esphome/core/base_automation.h +2 -2
- esphome/core/component.cpp +2 -2
- esphome/core/event_pool.h +2 -2
- esphome/core/lock_free_queue.h +2 -7
- esphome/core/scheduler.cpp +1 -1
- esphome/core/scheduler.h +4 -3
- {esphome-2025.7.1.dist-info → esphome-2025.7.3.dist-info}/METADATA +1 -1
- {esphome-2025.7.1.dist-info → esphome-2025.7.3.dist-info}/RECORD +36 -36
- {esphome-2025.7.1.dist-info → esphome-2025.7.3.dist-info}/WHEEL +0 -0
- {esphome-2025.7.1.dist-info → esphome-2025.7.3.dist-info}/entry_points.txt +0 -0
- {esphome-2025.7.1.dist-info → esphome-2025.7.3.dist-info}/licenses/LICENSE +0 -0
- {esphome-2025.7.1.dist-info → esphome-2025.7.3.dist-info}/top_level.txt +0 -0
|
@@ -16,6 +16,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|
|
16
16
|
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
|
17
17
|
|
|
18
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)
|
|
19
22
|
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
|
20
23
|
static std::string value_to_string(const std::string &val) { return val; }
|
|
21
24
|
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import esphome.codegen as cg
|
|
2
2
|
from esphome.components import esp32, i2c
|
|
3
3
|
import esphome.config_validation as cv
|
|
4
|
-
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET
|
|
4
|
+
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
|
|
5
5
|
|
|
6
6
|
CODEOWNERS = ["@trvrnrth"]
|
|
7
7
|
DEPENDENCIES = ["i2c"]
|
|
@@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
|
|
|
56
56
|
): cv.positive_time_period_minutes,
|
|
57
57
|
}
|
|
58
58
|
).extend(i2c.i2c_device_schema(0x76)),
|
|
59
|
-
cv.
|
|
59
|
+
cv.only_with_framework(
|
|
60
|
+
frameworks=Framework.ARDUINO,
|
|
61
|
+
suggestions={
|
|
62
|
+
Framework.ESP_IDF: (
|
|
63
|
+
"bme68x_bsec2_i2c",
|
|
64
|
+
"sensor/bme68x_bsec2",
|
|
65
|
+
)
|
|
66
|
+
},
|
|
67
|
+
),
|
|
60
68
|
cv.Any(
|
|
61
69
|
cv.only_on_esp8266,
|
|
62
70
|
cv.All(
|
|
@@ -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",
|
|
@@ -16,6 +16,8 @@ namespace esp32_touch {
|
|
|
16
16
|
|
|
17
17
|
static const char *const TAG = "esp32_touch";
|
|
18
18
|
|
|
19
|
+
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
|
|
20
|
+
|
|
19
21
|
void ESP32TouchComponent::setup() {
|
|
20
22
|
// Create queue for touch events
|
|
21
23
|
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
|
@@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() {
|
|
|
44
46
|
|
|
45
47
|
// Configure each touch pad
|
|
46
48
|
for (auto *child : this->children_) {
|
|
47
|
-
|
|
49
|
+
if (this->setup_mode_) {
|
|
50
|
+
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
|
|
51
|
+
} else {
|
|
52
|
+
touch_pad_config(child->get_touch_pad(), child->get_threshold());
|
|
53
|
+
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
// Register ISR handler
|
|
@@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() {
|
|
|
114
120
|
child->publish_state(new_state);
|
|
115
121
|
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
|
116
122
|
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
|
|
117
|
-
ESP_LOGV(TAG, "Touch Pad '%s' state:
|
|
118
|
-
child->get_name().c_str(), event.value, child->get_threshold());
|
|
123
|
+
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
|
|
124
|
+
child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
|
|
119
125
|
}
|
|
120
126
|
break; // Exit inner loop after processing matching pad
|
|
121
127
|
}
|
|
@@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
|
|
188
194
|
// as any pad remains touched. This allows us to detect both new touches and
|
|
189
195
|
// continued touches, but releases must be detected by timeout in the main loop.
|
|
190
196
|
|
|
191
|
-
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
|
192
|
-
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
|
193
|
-
// Therefore: touched = (value < threshold)
|
|
194
|
-
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
|
195
|
-
|
|
196
197
|
// Process all configured pads to check their current state
|
|
197
198
|
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
|
198
199
|
// so we must scan all configured pads to find which ones were touched
|
|
@@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
// Skip pads that aren’t in the trigger mask
|
|
214
|
-
|
|
215
|
-
if (!is_touched) {
|
|
215
|
+
if (((mask >> pad) & 1) == 0) {
|
|
216
216
|
continue;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
|
220
|
+
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
|
221
|
+
// Therefore: touched = (value < threshold)
|
|
222
|
+
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
|
223
|
+
bool is_touched = value < child->get_threshold();
|
|
224
|
+
|
|
219
225
|
// Always send the current state - the main loop will filter for changes
|
|
220
226
|
// We send both touched and untouched states because the ISR doesn't
|
|
221
227
|
// track previous state (to keep ISR fast and simple)
|
|
@@ -2,7 +2,13 @@ from esphome import pins
|
|
|
2
2
|
import esphome.codegen as cg
|
|
3
3
|
from esphome.components import fastled_base
|
|
4
4
|
import esphome.config_validation as cv
|
|
5
|
-
from esphome.const import
|
|
5
|
+
from esphome.const import (
|
|
6
|
+
CONF_CHIPSET,
|
|
7
|
+
CONF_NUM_LEDS,
|
|
8
|
+
CONF_PIN,
|
|
9
|
+
CONF_RGB_ORDER,
|
|
10
|
+
Framework,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
AUTO_LOAD = ["fastled_base"]
|
|
8
14
|
|
|
@@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All(
|
|
|
48
54
|
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
|
|
49
55
|
}
|
|
50
56
|
),
|
|
51
|
-
|
|
57
|
+
cv.only_with_framework(
|
|
58
|
+
frameworks=Framework.ARDUINO,
|
|
59
|
+
suggestions={
|
|
60
|
+
Framework.ESP_IDF: (
|
|
61
|
+
"esp32_rmt_led_strip",
|
|
62
|
+
"light/esp32_rmt_led_strip",
|
|
63
|
+
)
|
|
64
|
+
},
|
|
65
|
+
),
|
|
52
66
|
cv.require_framework_version(
|
|
53
67
|
esp8266_arduino=cv.Version(2, 7, 4),
|
|
54
68
|
esp32_arduino=cv.Version(99, 0, 0),
|
|
55
69
|
max_version=True,
|
|
56
70
|
extra_message="Please see note on documentation for FastLED",
|
|
57
71
|
),
|
|
72
|
+
_validate,
|
|
58
73
|
)
|
|
59
74
|
|
|
60
75
|
|
|
@@ -9,6 +9,7 @@ from esphome.const import (
|
|
|
9
9
|
CONF_DATA_RATE,
|
|
10
10
|
CONF_NUM_LEDS,
|
|
11
11
|
CONF_RGB_ORDER,
|
|
12
|
+
Framework,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
AUTO_LOAD = ["fastled_base"]
|
|
@@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All(
|
|
|
33
34
|
cv.Optional(CONF_DATA_RATE): cv.frequency,
|
|
34
35
|
}
|
|
35
36
|
),
|
|
37
|
+
cv.only_with_framework(
|
|
38
|
+
frameworks=Framework.ARDUINO,
|
|
39
|
+
suggestions={
|
|
40
|
+
Framework.ESP_IDF: (
|
|
41
|
+
"spi_led_strip",
|
|
42
|
+
"light/spi_led_strip",
|
|
43
|
+
)
|
|
44
|
+
},
|
|
45
|
+
),
|
|
36
46
|
cv.require_framework_version(
|
|
37
47
|
esp8266_arduino=cv.Version(2, 7, 4),
|
|
38
48
|
esp32_arduino=cv.Version(99, 0, 0),
|
|
@@ -4,7 +4,13 @@ from esphome import pins
|
|
|
4
4
|
import esphome.codegen as cg
|
|
5
5
|
from esphome.components import binary_sensor
|
|
6
6
|
import esphome.config_validation as cv
|
|
7
|
-
from esphome.const import
|
|
7
|
+
from esphome.const import (
|
|
8
|
+
CONF_ALLOW_OTHER_USES,
|
|
9
|
+
CONF_ID,
|
|
10
|
+
CONF_NAME,
|
|
11
|
+
CONF_NUMBER,
|
|
12
|
+
CONF_PIN,
|
|
13
|
+
)
|
|
8
14
|
from esphome.core import CORE
|
|
9
15
|
|
|
10
16
|
from .. import gpio_ns
|
|
@@ -29,7 +35,21 @@ CONFIG_SCHEMA = (
|
|
|
29
35
|
.extend(
|
|
30
36
|
{
|
|
31
37
|
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
|
32
|
-
|
|
38
|
+
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
|
|
39
|
+
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
|
40
|
+
# stable operation on these platforms. Future maintainers should verify platform
|
|
41
|
+
# capabilities before changing this default behavior.
|
|
42
|
+
cv.SplitDefault(
|
|
43
|
+
CONF_USE_INTERRUPT,
|
|
44
|
+
bk72xx=False,
|
|
45
|
+
esp32=True,
|
|
46
|
+
esp8266=True,
|
|
47
|
+
host=True,
|
|
48
|
+
ln882x=False,
|
|
49
|
+
nrf52=True,
|
|
50
|
+
rp2040=True,
|
|
51
|
+
rtl87xx=False,
|
|
52
|
+
): cv.boolean,
|
|
33
53
|
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
|
|
34
54
|
INTERRUPT_TYPES, upper=True
|
|
35
55
|
),
|
|
@@ -62,6 +82,18 @@ async def to_code(config):
|
|
|
62
82
|
)
|
|
63
83
|
use_interrupt = False
|
|
64
84
|
|
|
85
|
+
# Check if pin is shared with other components (allow_other_uses)
|
|
86
|
+
# When a pin is shared, interrupts can interfere with other components
|
|
87
|
+
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
|
|
88
|
+
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
|
89
|
+
_LOGGER.info(
|
|
90
|
+
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
|
|
91
|
+
"The sensor will use polling mode for compatibility with other pin uses.",
|
|
92
|
+
config.get(CONF_NAME, config[CONF_ID]),
|
|
93
|
+
config[CONF_PIN][CONF_NUMBER],
|
|
94
|
+
)
|
|
95
|
+
use_interrupt = False
|
|
96
|
+
|
|
65
97
|
cg.add(var.set_use_interrupt(use_interrupt))
|
|
66
98
|
if use_interrupt:
|
|
67
99
|
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
|
@@ -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:
|
|
@@ -15,6 +15,7 @@ from esphome.const import (
|
|
|
15
15
|
CONF_PIN,
|
|
16
16
|
CONF_TYPE,
|
|
17
17
|
CONF_VARIANT,
|
|
18
|
+
Framework,
|
|
18
19
|
)
|
|
19
20
|
from esphome.core import CORE
|
|
20
21
|
|
|
@@ -162,7 +163,15 @@ def _validate_method(value):
|
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
CONFIG_SCHEMA = cv.All(
|
|
165
|
-
cv.
|
|
166
|
+
cv.only_with_framework(
|
|
167
|
+
frameworks=Framework.ARDUINO,
|
|
168
|
+
suggestions={
|
|
169
|
+
Framework.ESP_IDF: (
|
|
170
|
+
"esp32_rmt_led_strip",
|
|
171
|
+
"light/esp32_rmt_led_strip",
|
|
172
|
+
)
|
|
173
|
+
},
|
|
174
|
+
),
|
|
166
175
|
cv.require_framework_version(
|
|
167
176
|
esp8266_arduino=cv.Version(2, 4, 0),
|
|
168
177
|
esp32_arduino=cv.Version(0, 0, 0),
|
|
@@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
|
51
|
+
if (!this->get_clipping().inside(x, y))
|
|
52
|
+
return;
|
|
53
|
+
|
|
51
54
|
SDL_Rect rect{x, y, 1, 1};
|
|
52
55
|
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
|
|
53
56
|
SDL_UpdateTexture(this->texture_, &rect, &data, 2);
|
|
@@ -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);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import esphome.codegen as cg
|
|
2
2
|
from esphome.components import fan
|
|
3
3
|
import esphome.config_validation as cv
|
|
4
|
-
from esphome.const import
|
|
4
|
+
from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
|
|
5
5
|
|
|
6
6
|
from .. import CONF_TUYA_ID, Tuya, tuya_ns
|
|
7
7
|
|
|
@@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint"
|
|
|
14
14
|
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan)
|
|
15
15
|
|
|
16
16
|
CONFIG_SCHEMA = cv.All(
|
|
17
|
-
fan.
|
|
17
|
+
fan.fan_schema(TuyaFan)
|
|
18
|
+
.extend(
|
|
18
19
|
{
|
|
19
|
-
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
|
|
20
20
|
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
|
|
21
21
|
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
|
|
22
22
|
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
|
|
@@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All(
|
|
|
24
24
|
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
|
|
25
25
|
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
|
|
26
26
|
}
|
|
27
|
-
)
|
|
27
|
+
)
|
|
28
|
+
.extend(cv.COMPONENT_SCHEMA),
|
|
28
29
|
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
|
29
30
|
)
|
|
30
31
|
|
|
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
|
|
|
32
33
|
async def to_code(config):
|
|
33
34
|
parent = await cg.get_variable(config[CONF_TUYA_ID])
|
|
34
35
|
|
|
35
|
-
var = cg.new_Pvariable(config[
|
|
36
|
+
var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT])
|
|
36
37
|
await cg.register_component(var, config)
|
|
37
38
|
await fan.register_fan(var, config)
|
|
38
39
|
|
|
@@ -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};
|
|
@@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
|
|
76
76
|
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
|
77
77
|
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
|
78
78
|
} else {
|
|
79
|
-
ESP_LOGD(TAG, "OTA in progress: %
|
|
79
|
+
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
|
|
80
80
|
}
|
|
81
81
|
#ifdef USE_OTA_STATE_CALLBACK
|
|
82
82
|
// Report progress - use call_deferred since we're in web server task
|
|
@@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
|
|
171
171
|
|
|
172
172
|
// Finalize
|
|
173
173
|
if (final) {
|
|
174
|
-
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%
|
|
174
|
+
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
|
|
175
175
|
this->ota_read_length_, request->contentLength());
|
|
176
176
|
|
|
177
177
|
// For Arduino framework, the Update library tracks expected size from firmware header
|
|
@@ -1620,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
|
|
1620
1620
|
request->send(404);
|
|
1621
1621
|
}
|
|
1622
1622
|
|
|
1623
|
-
static std::string get_event_type(event::Event *event) {
|
|
1623
|
+
static std::string get_event_type(event::Event *event) {
|
|
1624
|
+
return (event && event->last_event_type) ? *event->last_event_type : "";
|
|
1625
|
+
}
|
|
1624
1626
|
|
|
1625
1627
|
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
|
1626
1628
|
auto *event = static_cast<event::Event *>(source);
|