mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 07:17:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85203e048f | ||
|
|
c05b7362cf | ||
|
|
3e3822e554 | ||
|
|
7564f5ff1a | ||
|
|
a807a8f945 |
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -153,13 +153,14 @@ bool ES7210::configure_mic_gain_() {
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
|
||||
|
||||
// Configure mic 3
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
|
||||
|
||||
// Configure mic 4
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
|
||||
|
||||
@@ -6,12 +6,17 @@ from esphome.components import esp32, network, psram, socket, wifi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_ESPHOME,
|
||||
CONF_FORMAT,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
CONF_PROJECT,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_SOURCE,
|
||||
CONF_TASK_STACK_IN_PSRAM,
|
||||
CONF_VERSION,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
@@ -27,6 +32,14 @@ DOMAIN = "sendspin"
|
||||
CONF_DISPLAY_OFFSET = "display_offset"
|
||||
CONF_SENDSPIN_ID = "sendspin_id"
|
||||
|
||||
CONF_FIRMWARE_VERSION = "firmware_version"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
|
||||
# An empty device information string would be sent to the server as an empty value rather than
|
||||
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
|
||||
# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`.
|
||||
DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127))
|
||||
|
||||
CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
|
||||
CONF_FIXED_DELAY = "fixed_delay"
|
||||
CONF_DECODE_MEMORY = "decode_memory"
|
||||
@@ -198,6 +211,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SendspinHub),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
@@ -248,6 +264,22 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
psram.request_external_task_stack()
|
||||
|
||||
# Device information for the server's client/hello message. Falls back to the project
|
||||
# information, which is written as `manufacturer.model`. Anything still unset keeps the
|
||||
# default the hub itself applies: the ESPHome name and version.
|
||||
project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {})
|
||||
project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".")
|
||||
for value, setter in (
|
||||
(config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer),
|
||||
(config.get(CONF_MODEL) or project_model, var.set_model),
|
||||
(
|
||||
config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION),
|
||||
var.set_firmware_version,
|
||||
),
|
||||
):
|
||||
if value:
|
||||
cg.add(setter(value))
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
|
||||
|
||||
|
||||
@@ -76,8 +76,12 @@ void SendspinHub::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Sendspin Hub:\n"
|
||||
" Client ID: %s\n"
|
||||
" Manufacturer: %s\n"
|
||||
" Model: %s\n"
|
||||
" Firmware version: %s\n"
|
||||
" Task stack in PSRAM: %s",
|
||||
get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
|
||||
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
|
||||
this->firmware_version_, YESNO(this->task_stack_in_psram_));
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
// Slot indices come from the order the image platform entries were declared, so the log is the
|
||||
@@ -127,15 +131,19 @@ const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_P
|
||||
return get_mac_address_pretty_into_buffer(buf);
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_product_name_() const {
|
||||
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
|
||||
}
|
||||
|
||||
sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
|
||||
sendspin::SendspinClientConfig config;
|
||||
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
|
||||
config.name = App.get_friendly_name();
|
||||
config.product_name = App.get_name();
|
||||
config.manufacturer = "ESPHome";
|
||||
config.software_version = ESPHOME_VERSION;
|
||||
config.product_name = this->get_product_name_();
|
||||
config.manufacturer = this->manufacturer_;
|
||||
config.software_version = this->firmware_version_;
|
||||
config.httpd_psram_stack = this->task_stack_in_psram_;
|
||||
|
||||
return config;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#include <sendspin/client.h>
|
||||
#include <sendspin/config.h>
|
||||
@@ -125,6 +126,15 @@ class SendspinHub final : public Component,
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
/// @brief Sets the device information reported to the server in the `client/hello` message.
|
||||
///
|
||||
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
|
||||
/// lifetime of the hub. Only called for values the configuration overrides; anything left alone
|
||||
/// keeps the default described on the member below.
|
||||
void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; }
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -187,6 +197,9 @@ class SendspinHub final : public Component,
|
||||
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
|
||||
sendspin::SendspinClientConfig build_client_config_();
|
||||
|
||||
/// @brief Returns the product name reported to the server: the configured model, or the device name.
|
||||
const char *get_product_name_() const;
|
||||
|
||||
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
|
||||
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
|
||||
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
@@ -268,6 +281,12 @@ class SendspinHub final : public Component,
|
||||
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
|
||||
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
// Device information sent in the `client/hello` message. Defaults apply when neither the
|
||||
// sendspin configuration nor the project information supplies a value.
|
||||
const char *manufacturer_{"ESPHome"};
|
||||
const char *model_{nullptr}; // nullptr reports the device name instead
|
||||
const char *firmware_version_{ESPHOME_VERSION};
|
||||
};
|
||||
|
||||
/// @brief Base class for all sendspin subcomponents.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Speaker Media Player Setup."""
|
||||
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import (
|
||||
@@ -33,9 +31,6 @@ from esphome.const import (
|
||||
CONF_TASK_STACK_IN_PSRAM,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
@@ -44,7 +39,7 @@ DOMAIN = "media_player"
|
||||
|
||||
CONF_ANNOUNCEMENT = "announcement"
|
||||
CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline"
|
||||
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2026.10.0
|
||||
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2027.4.0
|
||||
CONF_ENQUEUE = "enqueue"
|
||||
CONF_MEDIA_FILE = "media_file"
|
||||
CONF_MEDIA_PIPELINE = "media_pipeline"
|
||||
@@ -103,15 +98,6 @@ def _validate_repeated_speaker(config):
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
# Remove before 2026.10.0
|
||||
if CONF_CODEC_SUPPORT_ENABLED in config:
|
||||
_LOGGER.warning(
|
||||
"'%s' is deprecated and will be removed in 2026.10.0. "
|
||||
"Codec support is now automatically determined from the pipeline "
|
||||
"'format' setting. Set format to 'NONE' to enable all codecs.",
|
||||
CONF_CODEC_SUPPORT_ENABLED,
|
||||
)
|
||||
|
||||
# Request codecs based on pipeline formats. Codecs needed by local files are
|
||||
# already requested during CONFIG_SCHEMA validation (via audio_files_schema).
|
||||
media_player.request_codecs_for_format_configs(
|
||||
@@ -151,8 +137,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
|
||||
min=4000, max=4000000
|
||||
),
|
||||
# Remove before 2026.10.0
|
||||
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string),
|
||||
# Removed in 2026.10.0 - kept to provide helpful error message
|
||||
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.invalid(
|
||||
"The 'codec_support_enabled' option has been removed in ESPHome 2026.10.0.\n"
|
||||
"Codec support is now determined from the pipeline 'format' setting.\n"
|
||||
"Set 'format: NONE' on the pipeline to enable all codecs."
|
||||
),
|
||||
cv.Optional(CONF_FILES): audio_file.audio_files_schema(),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
project:
|
||||
name: project_manufacturer.project_model
|
||||
version: 9.9.9
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
manufacturer: Explicit Manufacturer
|
||||
model: Explicit Model
|
||||
firmware_version: 1.2.3
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
project:
|
||||
name: project_manufacturer.project_model
|
||||
version: 9.9.9
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests for the device information the sendspin hub reports to the server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sendspin import (
|
||||
CONF_FIRMWARE_VERSION,
|
||||
CONF_MANUFACTURER,
|
||||
CONFIG_SCHEMA,
|
||||
)
|
||||
from esphome.const import CONF_MODEL, PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_explicit_device_info_wins_over_project(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Configured values take precedence over the project information."""
|
||||
main_cpp = generate_main(component_config_path("device_info_explicit.yaml"))
|
||||
|
||||
assert 'set_manufacturer("Explicit Manufacturer")' in main_cpp
|
||||
assert 'set_model("Explicit Model")' in main_cpp
|
||||
assert 'set_firmware_version("1.2.3")' in main_cpp
|
||||
|
||||
|
||||
def test_project_supplies_device_info(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without configured values, the project name splits into manufacturer and model."""
|
||||
main_cpp = generate_main(component_config_path("device_info_project.yaml"))
|
||||
|
||||
assert 'set_manufacturer("project_manufacturer")' in main_cpp
|
||||
assert 'set_model("project_model")' in main_cpp
|
||||
assert 'set_firmware_version("9.9.9")' in main_cpp
|
||||
|
||||
|
||||
def test_no_device_info_leaves_hub_defaults(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""With neither source, nothing is emitted and the hub keeps its own defaults."""
|
||||
main_cpp = generate_main(component_config_path("device_info_default.yaml"))
|
||||
|
||||
assert "set_manufacturer(" not in main_cpp
|
||||
assert "set_model(" not in main_cpp
|
||||
assert "set_firmware_version(" not in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
|
||||
)
|
||||
def test_empty_device_info_rejected(
|
||||
set_core_config: SetCoreConfigCallable, conf_key: str
|
||||
) -> None:
|
||||
"""An empty string would be sent to the server as an empty value, so it is not accepted."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: ""})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
|
||||
)
|
||||
def test_device_info_capped_at_127_bytes(
|
||||
set_core_config: SetCoreConfigCallable, conf_key: str
|
||||
) -> None:
|
||||
"""The cap is in bytes so the protobuf length prefix stays a single byte."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
CONFIG_SCHEMA({conf_key: "a" * 127})
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: "a" * 128})
|
||||
# 64 two-byte characters is 128 bytes.
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: "é" * 64})
|
||||
@@ -4,3 +4,6 @@ psram:
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
task_stack_in_psram: true
|
||||
manufacturer: Test Manufacturer
|
||||
model: Test Model
|
||||
firmware_version: 1.2.3
|
||||
|
||||
Reference in New Issue
Block a user