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

This commit is contained in:
J. Nick Koston
2026-03-03 08:23:42 -10:00
17 changed files with 145 additions and 16 deletions
+3
View File
@@ -8,14 +8,17 @@ BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_PARITY = "parity"
CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_STOP_BITS = "stop_bits"
CONF_USE_PSRAM = "use_psram"
ICON_CURRENT_DC = "mdi:current-dc"
+29 -1
View File
@@ -4,13 +4,20 @@ from esphome.components.esp32 import (
VARIANT_ESP32C6,
VARIANT_ESP32H2,
add_idf_sdkconfig_option,
get_esp32_variant,
include_builtin_idf_component,
only_on_variant,
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS
from esphome.const import (
CONF_CHANNEL,
CONF_ENABLE_IPV6,
CONF_ID,
CONF_OUTPUT_POWER,
CONF_USE_ADDRESS,
)
from esphome.core import CORE, TimePeriodMilliseconds
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -45,6 +52,20 @@ CONF_DEVICE_TYPES = [
]
def _validate_txpower(value):
if CORE.is_esp32:
variant = get_esp32_variant()
# HW limits: Datasheet section "802.15.4 RF Transmitter (TX) Characteristics"
# Further regulatory/soft limit may apply, e.g. by region
if variant in (VARIANT_ESP32C6, VARIANT_ESP32C5):
return cv.int_range(min=-15, max=20)(value)
if variant == VARIANT_ESP32H2:
return cv.int_range(min=-24, max=20)(value)
return value # Unsupported, fail later with clear error
def set_sdkconfig_options(config):
# and expose options for using SPI/UART RCPs
add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True)
@@ -155,6 +176,10 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_TLV): cv.string_strict,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OUTPUT_POWER): cv.All(
cv.decibel,
_validate_txpower,
),
}
).extend(_CONNECTION_SCHEMA),
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
@@ -197,4 +222,7 @@ async def to_code(config):
cg.add(srp.set_mdns(mdns_component))
await cg.register_component(srp, config)
if (output_power := config.get(CONF_OUTPUT_POWER)) is not None:
cg.add(ot.set_output_power(output_power))
set_sdkconfig_options(config)
@@ -43,6 +43,9 @@ void OpenThreadComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)");
}
#endif
if (this->output_power_.has_value()) {
ESP_LOGCONFIG(TAG, " Output power: %" PRId8 "dBm", *this->output_power_);
}
}
bool OpenThreadComponent::is_connected() {
@@ -38,6 +38,7 @@ class OpenThreadComponent : public Component {
#if CONFIG_OPENTHREAD_MTD
void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; }
#endif
void set_output_power(int8_t output_power) { this->output_power_ = output_power; }
protected:
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
@@ -45,6 +46,7 @@ class OpenThreadComponent : public Component {
#if CONFIG_OPENTHREAD_MTD
uint32_t poll_period_{0};
#endif
std::optional<int8_t> output_power_{};
bool teardown_started_{false};
bool teardown_complete_{false};
@@ -135,6 +135,12 @@ void OpenThreadComponent::ot_main() {
TRUEFALSE(link_mode_config.mRxOnWhenIdle));
#endif
if (this->output_power_.has_value()) {
if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) {
ESP_LOGE(TAG, "Failed to set power: %s", otThreadErrorToString(err));
}
}
// Run the main loop
#if CONFIG_OPENTHREAD_CLI
esp_openthread_cli_create_task();
@@ -15,6 +15,8 @@ from esphome.const import (
CONF_FORMAT,
CONF_ID,
CONF_NUM_CHANNELS,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_SAMPLE_RATE,
@@ -401,6 +403,9 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config):
if CONF_ON_TURN_OFF in config or CONF_ON_TURN_ON in config:
cg.add_define("USE_SPEAKER_MEDIA_PLAYER_ON_OFF", True)
var = await media_player.new_media_player(config)
await cg.register_component(var, config)
@@ -51,7 +51,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
static const char *const TAG = "speaker_media_player";
void SpeakerMediaPlayer::setup() {
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
state = media_player::MEDIA_PLAYER_STATE_OFF;
#else
state = media_player::MEDIA_PLAYER_STATE_IDLE;
#endif
this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
@@ -128,6 +132,12 @@ void SpeakerMediaPlayer::watch_media_commands_() {
bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
if (media_command.url.has_value() || media_command.file.has_value()) {
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) {
this->state = media_player::MEDIA_PLAYER_STATE_ON;
publish_state();
}
#endif
PlaylistItem playlist_item;
if (media_command.url.has_value()) {
playlist_item.url = *media_command.url.value();
@@ -184,6 +194,12 @@ void SpeakerMediaPlayer::watch_media_commands_() {
if (media_command.command.has_value()) {
switch (media_command.command.value()) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) {
this->state = media_player::MEDIA_PLAYER_STATE_ON;
publish_state();
}
#endif
if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
this->media_pipeline_->set_pause_state(false);
}
@@ -195,10 +211,26 @@ void SpeakerMediaPlayer::watch_media_commands_() {
}
this->is_paused_ = true;
break;
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
case media_player::MEDIA_PLAYER_COMMAND_TURN_ON:
if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) {
this->state = media_player::MEDIA_PLAYER_STATE_ON;
this->publish_state();
}
break;
case media_player::MEDIA_PLAYER_COMMAND_TURN_OFF:
this->is_turn_off_ = true;
// Intentional Fall-through
#endif
case media_player::MEDIA_PLAYER_COMMAND_STOP:
// Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing.
// This avoids an audible short segment playing after receiving the stop command in a paused state.
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value()) ||
(this->is_turn_off_ && this->announcement_pipeline_state_ != AudioPipelineState::STOPPED)) {
#else
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
#endif
if (this->announcement_pipeline_ != nullptr) {
this->cancel_timeout("next_ann");
this->announcement_playlist_.clear();
@@ -366,7 +398,13 @@ void SpeakerMediaPlayer::loop() {
}
} else {
if (this->is_paused_) {
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
}
#else
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
#endif
} else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
@@ -399,7 +437,13 @@ void SpeakerMediaPlayer::loop() {
}
}
} else {
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) {
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
}
#else
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
#endif
}
}
}
@@ -409,6 +453,20 @@ void SpeakerMediaPlayer::loop() {
this->publish_state();
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
}
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
if (this->is_turn_off_ && (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED ||
this->state == media_player::MEDIA_PLAYER_STATE_IDLE)) {
this->is_turn_off_ = false;
if (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED) {
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
this->publish_state();
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
}
this->state = media_player::MEDIA_PLAYER_STATE_OFF;
this->publish_state();
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
}
#endif
}
void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
@@ -481,6 +539,9 @@ media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() {
if (!this->single_pipeline_()) {
traits.set_supports_pause(true);
}
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
traits.set_supports_turn_off_on(true);
#endif
if (this->announcement_format_.has_value()) {
traits.get_supported_formats().push_back(this->announcement_format_.value());
@@ -144,6 +144,9 @@ class SpeakerMediaPlayer : public Component,
bool is_paused_{false};
bool is_muted_{false};
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
bool is_turn_off_{false};
#endif
uint8_t unpause_media_remaining_{0};
uint8_t unpause_announcement_remaining_{0};
+1 -3
View File
@@ -4,6 +4,7 @@ import re
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -11,7 +12,6 @@ from esphome.const import (
CONF_BAUD_RATE,
CONF_BYTES,
CONF_DATA,
CONF_DATA_BITS,
CONF_DEBUG,
CONF_DELIMITER,
CONF_DIRECTION,
@@ -21,12 +21,10 @@ from esphome.const import (
CONF_ID,
CONF_LAMBDA,
CONF_NUMBER,
CONF_PARITY,
CONF_PORT,
CONF_RX_BUFFER_SIZE,
CONF_RX_PIN,
CONF_SEQUENCE,
CONF_STOP_BITS,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_TX_PIN,
+1 -3
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import socket
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
from esphome.components.uart import UARTComponent
from esphome.components.usb_host import register_usb_client, usb_device_schema
import esphome.config_validation as cv
@@ -7,12 +8,9 @@ from esphome.const import (
CONF_BAUD_RATE,
CONF_BUFFER_SIZE,
CONF_CHANNELS,
CONF_DATA_BITS,
CONF_DEBUG,
CONF_DUMMY_RECEIVER,
CONF_ID,
CONF_PARITY,
CONF_STOP_BITS,
)
from esphome.cpp_types import Component
+1 -3
View File
@@ -1,18 +1,16 @@
import esphome.codegen as cg
from esphome.components import uart
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_CHANNEL,
CONF_DATA_BITS,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PARITY,
CONF_STOP_BITS,
)
CODEOWNERS = ["@DrCoolZic"]
-3
View File
@@ -280,7 +280,6 @@ CONF_CUSTOM_PRESETS = "custom_presets"
CONF_CYCLE = "cycle"
CONF_DALLAS_ID = "dallas_id"
CONF_DATA = "data"
CONF_DATA_BITS = "data_bits"
CONF_DATA_PIN = "data_pin"
CONF_DATA_PINS = "data_pins"
CONF_DATA_RATE = "data_rate"
@@ -760,7 +759,6 @@ CONF_PAGE_ID = "page_id"
CONF_PAGES = "pages"
CONF_PANASONIC = "panasonic"
CONF_PARAMETERS = "parameters"
CONF_PARITY = "parity"
CONF_PASSWORD = "password"
CONF_PATH = "path"
CONF_PATTERN = "pattern"
@@ -963,7 +961,6 @@ CONF_STEP_PIN = "step_pin"
CONF_STILL_THRESHOLD = "still_threshold"
CONF_STOP = "stop"
CONF_STOP_ACTION = "stop_action"
CONF_STOP_BITS = "stop_bits"
CONF_STORE_BASELINE = "store_baseline"
CONF_SUBNET = "subnet"
CONF_SUBSCRIBE_QOS = "subscribe_qos"
+1
View File
@@ -236,6 +236,7 @@
#define USE_LWIP_FAST_SELECT
#define USE_WAKE_LOOP_THREADSAFE
#define USE_SPEAKER
#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF
#define USE_SPI
#define USE_VOICE_ASSISTANT
#define USE_WEBSERVER
@@ -13,3 +13,4 @@ openthread:
force_dataset: true
use_address: open-thread-test.local
poll_period: 20sec
output_power: 1dBm
@@ -0,0 +1,18 @@
<<: !include common.yaml
media_player:
- platform: speaker
id: speaker_media_player_id
announcement_pipeline:
speaker: speaker_id
buffer_size: 1000000
volume_increment: 0.02
volume_max: 0.95
volume_min: 0.0
task_stack_in_psram: true
on_turn_on:
then:
- logger.log: "Turn On Media Player"
on_turn_off:
then:
- logger.log: "Turn Off Media Player"
@@ -0,0 +1,9 @@
substitutions:
scl_pin: GPIO16
sda_pin: GPIO17
i2s_bclk_pin: GPIO27
i2s_lrclk_pin: GPIO26
i2s_mclk_pin: GPIO25
i2s_dout_pin: GPIO23
<<: !include common-media_player_off_on.yaml
@@ -1,6 +1,7 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
from esphome.components.uart import (
CONF_RX_FULL_THRESHOLD,
CONF_RX_TIMEOUT,
@@ -12,14 +13,11 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_DATA,
CONF_DATA_BITS,
CONF_DEBUG,
CONF_DELAY,
CONF_ID,
CONF_INTERVAL,
CONF_PARITY,
CONF_RX_BUFFER_SIZE,
CONF_STOP_BITS,
CONF_TRIGGER_ID,
)
from esphome.core import ID