From 84836f15db00e8eecc73fa06b169fa7265ed653b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 10 Feb 2025 13:00:23 -0600 Subject: [PATCH 01/31] [speaker] Media Player Components PR9 (#8171) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/media_player/__init__.py | 59 +- esphome/components/media_player/automation.h | 13 +- .../components/media_player/media_player.cpp | 8 + .../components/media_player/media_player.h | 12 +- .../speaker/media_player/__init__.py | 458 ++++++++++++++ .../speaker/media_player/audio_pipeline.cpp | 560 +++++++++++++++++ .../speaker/media_player/audio_pipeline.h | 158 +++++ .../speaker/media_player/automation.h | 26 + .../media_player/speaker_media_player.cpp | 577 ++++++++++++++++++ .../media_player/speaker_media_player.h | 160 +++++ tests/components/media_player/common.yaml | 2 + .../speaker/common-media_player.yaml | 12 + .../speaker/media_player.esp32-idf.yaml | 9 + .../speaker/media_player.esp32-s3-idf.yaml | 9 + 15 files changed, 2043 insertions(+), 21 deletions(-) create mode 100644 esphome/components/speaker/media_player/__init__.py create mode 100644 esphome/components/speaker/media_player/audio_pipeline.cpp create mode 100644 esphome/components/speaker/media_player/audio_pipeline.h create mode 100644 esphome/components/speaker/media_player/automation.h create mode 100644 esphome/components/speaker/media_player/speaker_media_player.cpp create mode 100644 esphome/components/speaker/media_player/speaker_media_player.h create mode 100644 tests/components/speaker/common-media_player.yaml create mode 100644 tests/components/speaker/media_player.esp32-idf.yaml create mode 100644 tests/components/speaker/media_player.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d4b3d7eff9..26e36befe5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -390,6 +390,7 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/speaker/* @jesserockz @kahrendt +esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index a46b30db29..b2543ac05f 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -1,5 +1,4 @@ from esphome import automation -from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -21,6 +20,16 @@ media_player_ns = cg.esphome_ns.namespace("media_player") MediaPlayer = media_player_ns.class_("MediaPlayer") +MediaPlayerSupportedFormat = media_player_ns.struct("MediaPlayerSupportedFormat") + +MediaPlayerFormatPurpose = media_player_ns.enum( + "MediaPlayerFormatPurpose", is_class=True +) +MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { + "default": MediaPlayerFormatPurpose.PURPOSE_DEFAULT, + "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, +} + PlayAction = media_player_ns.class_( "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) @@ -47,7 +56,7 @@ VolumeSetAction = media_player_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) - +CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" CONF_ON_ANNOUNCEMENT = "on_announcement" @@ -125,7 +134,16 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ) -MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)}) +MEDIA_PLAYER_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(MediaPlayer), + cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), + } +) + +MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( + {cv.GenerateID(): cv.use_id(MediaPlayer)} +) @automation.register_action( @@ -135,6 +153,7 @@ MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPl { cv.GenerateID(): cv.use_id(MediaPlayer), cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), + cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), }, key=CONF_MEDIA_URL, ), @@ -143,7 +162,9 @@ async def media_player_play_media_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) cg.add(var.set_media_url(media_url)) + cg.add(var.set_announcement(announcement)) return var @@ -161,19 +182,27 @@ async def media_player_play_media_action(config, action_id, template_arg, args): @automation.register_action( "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA ) -@automation.register_condition( - "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_condition( - "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_condition( - "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_condition( - "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_ACTION_SCHEMA -) async def media_player_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) + cg.add(var.set_announcement(announcement)) + return var + + +@automation.register_condition( + "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA +) +@automation.register_condition( + "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA +) +@automation.register_condition( + "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA +) +@automation.register_condition( + "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA +) +async def media_player_condition(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 7b9220c4a5..422c224a85 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -10,7 +10,10 @@ namespace media_player { template class MediaPlayerCommandAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->make_call().set_command(Command).perform(); } + TEMPLATABLE_VALUE(bool, announcement); + void play(Ts... x) override { + this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform(); + } }; template @@ -28,7 +31,13 @@ using VolumeDownAction = MediaPlayerCommandAction class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) - void play(Ts... x) override { this->parent_->make_call().set_media_url(this->media_url_.value(x...)).perform(); } + TEMPLATABLE_VALUE(bool, announcement) + void play(Ts... x) override { + this->parent_->make_call() + .set_media_url(this->media_url_.value(x...)) + .set_announcement(this->announcement_.value(x...)) + .perform(); + } }; template class VolumeSetAction : public Action, public Parented { diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index b5190d8573..01304d9135 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -41,6 +41,14 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "VOLUME_UP"; case MEDIA_PLAYER_COMMAND_VOLUME_DOWN: return "VOLUME_DOWN"; + case MEDIA_PLAYER_COMMAND_ENQUEUE: + return "ENQUEUE"; + case MEDIA_PLAYER_COMMAND_REPEAT_ONE: + return "REPEAT_ONE"; + case MEDIA_PLAYER_COMMAND_REPEAT_OFF: + return "REPEAT_OFF"; + case MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: + return "CLEAR_PLAYLIST"; default: return "UNKNOWN"; } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 78b3ed6216..ee5889901c 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -24,6 +24,10 @@ enum MediaPlayerCommand : uint8_t { MEDIA_PLAYER_COMMAND_TOGGLE = 5, MEDIA_PLAYER_COMMAND_VOLUME_UP = 6, MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7, + MEDIA_PLAYER_COMMAND_ENQUEUE = 8, + MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9, + MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10, + MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, }; const char *media_player_command_to_string(MediaPlayerCommand command); @@ -72,10 +76,10 @@ class MediaPlayerCall { void perform(); - const optional &get_command() const { return command_; } - const optional &get_media_url() const { return media_url_; } - const optional &get_volume() const { return volume_; } - const optional &get_announcement() const { return announcement_; } + const optional &get_command() const { return this->command_; } + const optional &get_media_url() const { return this->media_url_; } + const optional &get_volume() const { return this->volume_; } + const optional &get_announcement() const { return this->announcement_; } protected: void validate_(); diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py new file mode 100644 index 0000000000..14b72cacc0 --- /dev/null +++ b/esphome/components/speaker/media_player/__init__.py @@ -0,0 +1,458 @@ +"""Speaker Media Player Setup.""" + +import hashlib +import logging +from pathlib import Path + +from esphome import automation, external_files +import esphome.codegen as cg +from esphome.components import audio, esp32, media_player, speaker +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_FILE, + CONF_FILES, + CONF_FORMAT, + CONF_ID, + CONF_NUM_CHANNELS, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_SAMPLE_RATE, + CONF_SPEAKER, + CONF_TASK_STACK_IN_PSRAM, + CONF_TYPE, + CONF_URL, +) +from esphome.core import CORE, HexInt +from esphome.core.entity_helpers import inherit_property_from +from esphome.external_files import download_content + +_LOGGER = logging.getLogger(__name__) + +AUTO_LOAD = ["audio", "psram"] + +CODEOWNERS = ["@kahrendt", "@synesthesiam"] +DOMAIN = "media_player" + +TYPE_LOCAL = "local" +TYPE_WEB = "web" + +CONF_ANNOUNCEMENT = "announcement" +CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline" +CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" +CONF_ENQUEUE = "enqueue" +CONF_MEDIA_FILE = "media_file" +CONF_MEDIA_PIPELINE = "media_pipeline" +CONF_ON_MUTE = "on_mute" +CONF_ON_UNMUTE = "on_unmute" +CONF_ON_VOLUME = "on_volume" +CONF_STREAM = "stream" +CONF_VOLUME_INCREMENT = "volume_increment" +CONF_VOLUME_MIN = "volume_min" +CONF_VOLUME_MAX = "volume_max" + + +speaker_ns = cg.esphome_ns.namespace("speaker") +SpeakerMediaPlayer = speaker_ns.class_( + "SpeakerMediaPlayer", + media_player.MediaPlayer, + cg.Component, +) + +AudioPipeline = speaker_ns.class_("AudioPipeline") +AudioPipelineType = speaker_ns.enum("AudioPipelineType", is_class=True) +AUDIO_PIPELINE_TYPE_ENUM = { + "MEDIA": AudioPipelineType.MEDIA, + "ANNOUNCEMENT": AudioPipelineType.ANNOUNCEMENT, +} + +PlayOnDeviceMediaAction = speaker_ns.class_( + "PlayOnDeviceMediaAction", + automation.Action, + cg.Parented.template(SpeakerMediaPlayer), +) +StopStreamAction = speaker_ns.class_( + "StopStreamAction", automation.Action, cg.Parented.template(SpeakerMediaPlayer) +) + + +def _compute_local_file_path(value: dict) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def _download_web_file(value): + url = value[CONF_URL] + path = _compute_local_file_path(value) + + download_content(url, path) + _LOGGER.debug("download_web_file: path=%s", path) + return value + + +# Returns a media_player.MediaPlayerSupportedFormat struct with the configured +# format, sample rate, number of channels, purpose, and bytes per sample +def _get_supported_format_struct(pipeline, type): + args = [ + media_player.MediaPlayerSupportedFormat, + ] + + if pipeline[CONF_FORMAT] == "FLAC": + args.append(("format", "flac")) + elif pipeline[CONF_FORMAT] == "MP3": + args.append(("format", "mp3")) + elif pipeline[CONF_FORMAT] == "WAV": + args.append(("format", "wav")) + + args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) + args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) + + if type == "MEDIA": + args.append( + ( + "purpose", + media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], + ) + ) + elif type == "ANNOUNCEMENT": + args.append( + ( + "purpose", + media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], + ) + ) + if pipeline[CONF_FORMAT] != "MP3": + args.append(("sample_bytes", 2)) + + return cg.StructInitializer(*args) + + +def _file_schema(value): + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +def _read_audio_file_and_type(file_config): + conf_file = file_config[CONF_FILE] + file_source = conf_file[CONF_TYPE] + if file_source == TYPE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif file_source == TYPE_WEB: + path = _compute_local_file_path(conf_file) + else: + raise cv.Invalid("Unsupported file source.") + + with open(path, "rb") as f: + data = f.read() + + import puremagic + + file_type: str = puremagic.from_string(data) + if file_type.startswith("."): + file_type = file_type[1:] + + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] + if file_type in ("wav"): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"] + elif file_type in ("mp3", "mpeg", "mpga"): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] + elif file_type in ("flac"): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + + return data, media_file_type + + +def _validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +def _validate_pipeline(config): + # Inherit transcoder settings from speaker if not manually set + inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) + inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + + # Validate the transcoder settings is compatible with the speaker + audio.final_validate_audio_schema( + "speaker media_player", + audio_device=CONF_SPEAKER, + bits_per_sample=16, + channels=config.get(CONF_NUM_CHANNELS), + sample_rate=config.get(CONF_SAMPLE_RATE), + )(config) + + return config + + +def _validate_repeated_speaker(config): + if (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) and ( + media_config := config.get(CONF_MEDIA_PIPELINE) + ): + if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]: + raise cv.Invalid( + "The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers." + ) + + return config + + +def _validate_supported_local_file(config): + for file_config in config.get(CONF_FILES, []): + _, media_file_type = _read_audio_file_and_type(file_config) + if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): + raise cv.Invalid("Unsupported local media file.") + if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str( + audio.AUDIO_FILE_TYPE_ENUM["WAV"] + ): + # Only wav files are supported + raise cv.Invalid( + f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav" + ) + + return config + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _download_web_file, +) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +MEDIA_FILE_TYPE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(audio.AudioFile), + cv.Required(CONF_FILE): _file_schema, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + +PIPELINE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(AudioPipeline), + cv.Required(CONF_SPEAKER): cv.use_id(speaker.Speaker), + cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM), + cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1), + cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2), + } +) + +CONFIG_SCHEMA = cv.All( + media_player.MEDIA_PLAYER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SpeakerMediaPlayer), + cv.Required(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( + min=4000, max=4000000 + ), + cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, + cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, + cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, + cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), + } + ), + cv.only_with_esp_idf, + _validate_repeated_speaker, +) + + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_ANNOUNCEMENT_PIPELINE): _validate_pipeline, + cv.Optional(CONF_MEDIA_PIPELINE): _validate_pipeline, + }, + extra=cv.ALLOW_EXTRA, + ), + _validate_supported_local_file, +) + + +async def to_code(config): + if config[CONF_CODEC_SUPPORT_ENABLED]: + # Compile all supported audio codecs and optimize the wifi settings + + cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) + cg.add_define("USE_AUDIO_MP3_SUPPORT", True) + + # Wifi settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM", 16) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER", True) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BUFFER_TYPE", 0) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM", 8) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM", 32) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED", True) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BA_WIN", 16) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED", True) + esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_RX_BA_WIN", 32) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_MAXRTX", 12) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_SYNMAXRTX", 6) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSS", 1436) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSL", 60000) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_SND_BUF_DEFAULT", 65535) + esp32.add_idf_sdkconfig_option( + "CONFIG_TCP_WND_DEFAULT", 65535 + ) # Adjusted from referenced settings to avoid compilation error + esp32.add_idf_sdkconfig_option("CONFIG_TCP_RECVMBOX_SIZE", 512) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_QUEUE_OOSEQ", True) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_OVERSIZE_MSS", True) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) + esp32.add_idf_sdkconfig_option("CONFIG_TCP_RCV_SCALE", 3) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) + + # Allocate wifi buffers in PSRAM + esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_player.register_media_player(var, config) + + cg.add_define("USE_OTA_STATE_CALLBACK") + + cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) + + cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) + if config[CONF_TASK_STACK_IN_PSRAM]: + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) + + cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT])) + cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) + cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) + + announcement_pipeline_config = config[CONF_ANNOUNCEMENT_PIPELINE] + spkr = await cg.get_variable(announcement_pipeline_config[CONF_SPEAKER]) + cg.add(var.set_announcement_speaker(spkr)) + if announcement_pipeline_config[CONF_FORMAT] != "NONE": + cg.add( + var.set_announcement_format( + _get_supported_format_struct( + announcement_pipeline_config, "ANNOUNCEMENT" + ) + ) + ) + + if media_pipeline_config := config.get(CONF_MEDIA_PIPELINE): + spkr = await cg.get_variable(media_pipeline_config[CONF_SPEAKER]) + cg.add(var.set_media_speaker(spkr)) + if media_pipeline_config[CONF_FORMAT] != "NONE": + cg.add( + var.set_media_format( + _get_supported_format_struct(media_pipeline_config, "MEDIA") + ) + ) + + if on_mute := config.get(CONF_ON_MUTE): + await automation.build_automation( + var.get_mute_trigger(), + [], + on_mute, + ) + if on_unmute := config.get(CONF_ON_UNMUTE): + await automation.build_automation( + var.get_unmute_trigger(), + [], + on_unmute, + ) + if on_volume := config.get(CONF_ON_VOLUME): + await automation.build_automation( + var.get_volume_trigger(), + [(cg.float_, "x")], + on_volume, + ) + + for file_config in config.get(CONF_FILES, []): + data, media_file_type = _read_audio_file_and_type(file_config) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) + + media_files_struct = cg.StructInitializer( + audio.AudioFile, + ( + "data", + prog_arr, + ), + ( + "length", + len(rhs), + ), + ( + "file_type", + media_file_type, + ), + ) + + cg.new_Pvariable( + file_config[CONF_ID], + media_files_struct, + ) + + +@automation.register_action( + "media_player.speaker.play_on_device_media_file", + PlayOnDeviceMediaAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(SpeakerMediaPlayer), + cv.Required(CONF_MEDIA_FILE): cv.use_id(audio.AudioFile), + cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), + cv.Optional(CONF_ENQUEUE, default=False): cv.templatable(cv.boolean), + }, + key=CONF_MEDIA_FILE, + ), +) +async def play_on_device_media_media_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + media_file = await cg.get_variable(config[CONF_MEDIA_FILE]) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) + enqueue = await cg.templatable(config[CONF_ENQUEUE], args, cg.bool_) + + cg.add(var.set_audio_file(media_file)) + cg.add(var.set_announcement(announcement)) + cg.add(var.set_enqueue(enqueue)) + return var diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp new file mode 100644 index 0000000000..73ec5a3334 --- /dev/null +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -0,0 +1,560 @@ +#include "audio_pipeline.h" + +#ifdef USE_ESP_IDF + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace speaker { + +static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file + +static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024; +static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; + +static const uint32_t INFO_ERROR_QUEUE_COUNT = 5; + +static const char *const TAG = "speaker_media_player.pipeline"; + +enum EventGroupBits : uint32_t { + // MESSAGE_* bits are only set by their respective tasks + + // Stops all activity in the pipeline elements; cleared by process_state() and set by stop() or by each task + PIPELINE_COMMAND_STOP = (1 << 0), + + // Read audio from an HTTP source; cleared by reader task and set by start_url + READER_COMMAND_INIT_HTTP = (1 << 4), + // Read audio from an audio file from the flash; cleared by reader task and set by start_file + READER_COMMAND_INIT_FILE = (1 << 5), + + // Audio file type is read after checking it is supported; cleared by decoder task + READER_MESSAGE_LOADED_MEDIA_TYPE = (1 << 6), + // Reader is done (either through a failure or just end of the stream); cleared by reader task + READER_MESSAGE_FINISHED = (1 << 7), + // Error reading the file; cleared by process_state() + READER_MESSAGE_ERROR = (1 << 8), + + // Decoder is done (either through a faiilure or the end of the stream); cleared by decoder task + DECODER_MESSAGE_FINISHED = (1 << 12), + // Error decoding the file; cleared by process_state() by decoder task + DECODER_MESSAGE_ERROR = (1 << 13), +}; + +AudioPipeline::AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram, + std::string base_name, UBaseType_t priority) + : base_name_(std::move(base_name)), + priority_(priority), + task_stack_in_psram_(task_stack_in_psram), + speaker_(speaker), + buffer_size_(buffer_size) { + this->allocate_communications_(); + this->transfer_buffer_size_ = std::min(buffer_size_ / 4, DEFAULT_TRANSFER_BUFFER_SIZE); +} + +void AudioPipeline::start_url(const std::string &uri) { + if (this->is_playing_) { + xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP); + } + this->current_uri_ = uri; + this->pending_url_ = true; +} + +void AudioPipeline::start_file(audio::AudioFile *audio_file) { + if (this->is_playing_) { + xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP); + } + this->current_audio_file_ = audio_file; + this->pending_file_ = true; +} + +esp_err_t AudioPipeline::stop() { + xEventGroupSetBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); + + return ESP_OK; +} +void AudioPipeline::set_pause_state(bool pause_state) { + this->speaker_->set_pause_state(pause_state); + + this->pause_state_ = pause_state; +} + +void AudioPipeline::suspend_tasks() { + if (this->read_task_handle_ != nullptr) { + vTaskSuspend(this->read_task_handle_); + } + if (this->decode_task_handle_ != nullptr) { + vTaskSuspend(this->decode_task_handle_); + } +} + +void AudioPipeline::resume_tasks() { + if (this->read_task_handle_ != nullptr) { + vTaskResume(this->read_task_handle_); + } + if (this->decode_task_handle_ != nullptr) { + vTaskResume(this->decode_task_handle_); + } +} + +AudioPipelineState AudioPipeline::process_state() { + /* + * Log items from info error queue + */ + InfoErrorEvent event; + if (this->info_error_queue_ != nullptr) { + while (xQueueReceive(this->info_error_queue_, &event, 0)) { + switch (event.source) { + case InfoErrorSource::READER: + if (event.err.has_value()) { + ESP_LOGE(TAG, "Media reader encountered an error: %s", esp_err_to_name(event.err.value())); + } else if (event.file_type.has_value()) { + ESP_LOGD(TAG, "Reading %s file type", audio_file_type_to_string(event.file_type.value())); + } + + break; + case InfoErrorSource::DECODER: + if (event.err.has_value()) { + ESP_LOGE(TAG, "Decoder encountered an error: %s", esp_err_to_name(event.err.value())); + } + + if (event.audio_stream_info.has_value()) { + ESP_LOGD(TAG, "Decoded audio has %d channels, %" PRId32 " Hz sample rate, and %d bits per sample", + event.audio_stream_info.value().get_channels(), event.audio_stream_info.value().get_sample_rate(), + event.audio_stream_info.value().get_bits_per_sample()); + } + + if (event.decoding_err.has_value()) { + switch (event.decoding_err.value()) { + case DecodingError::FAILED_HEADER: + ESP_LOGE(TAG, "Failed to parse the file's header."); + break; + case DecodingError::INCOMPATIBLE_BITS_PER_SAMPLE: + ESP_LOGE(TAG, "Incompatible bits per sample. Only 16 bits per sample is supported"); + break; + case DecodingError::INCOMPATIBLE_CHANNELS: + ESP_LOGE(TAG, "Incompatible number of channels. Only 1 or 2 channel audio is supported."); + break; + } + } + break; + } + } + } + + /* + * Determine the current state based on the event group bits and tasks' status + */ + + EventBits_t event_bits = xEventGroupGetBits(this->event_group_); + + if (this->pending_url_ || this->pending_file_) { + // Init command pending + if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { + // Only start if there is no pending stop command + if ((this->read_task_handle_ == nullptr) || (this->decode_task_handle_ == nullptr)) { + // At least one task isn't running + this->start_tasks_(); + } + + if (this->pending_url_) { + xEventGroupSetBits(this->event_group_, EventGroupBits::READER_COMMAND_INIT_HTTP); + this->playback_ms_ = 0; + this->pending_url_ = false; + } else if (this->pending_file_) { + xEventGroupSetBits(this->event_group_, EventGroupBits::READER_COMMAND_INIT_FILE); + this->playback_ms_ = 0; + this->pending_file_ = false; + } + + this->is_playing_ = true; + return AudioPipelineState::PLAYING; + } + } + + if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) && + (!(event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) && + (event_bits & EventGroupBits::DECODER_MESSAGE_FINISHED))) { + // Tasks are finished and there's no media in between the reader and decoder + + if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) { + // Stop command is fully processed, so clear the command bit + xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); + } + + if (!this->is_playing_) { + // The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks + if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) { + this->delete_tasks_(); + this->speaker_->stop(); + } + } + this->is_playing_ = false; + return AudioPipelineState::STOPPED; + } + + if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_READING; + } + + if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_DECODING; + } + + if (this->pause_state_) { + return AudioPipelineState::PAUSED; + } + + if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) { + // No tasks are running, so the pipeline is stopped. + xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); + return AudioPipelineState::STOPPED; + } + + this->is_playing_ = true; + return AudioPipelineState::PLAYING; +} + +esp_err_t AudioPipeline::allocate_communications_() { + if (this->event_group_ == nullptr) + this->event_group_ = xEventGroupCreate(); + + if (this->event_group_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + if (this->info_error_queue_ == nullptr) + this->info_error_queue_ = xQueueCreate(INFO_ERROR_QUEUE_COUNT, sizeof(InfoErrorEvent)); + + if (this->info_error_queue_ == nullptr) + return ESP_ERR_NO_MEM; + + return ESP_OK; +} + +esp_err_t AudioPipeline::start_tasks_() { + if (this->read_task_handle_ == nullptr) { + if (this->read_task_stack_buffer_ == nullptr) { + if (this->task_stack_in_psram_) { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); + this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); + } else { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); + } + } + + if (this->read_task_stack_buffer_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + if (this->read_task_handle_ == nullptr) { + this->read_task_handle_ = + xTaskCreateStatic(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this, + this->priority_, this->read_task_stack_buffer_, &this->read_task_stack_); + } + + if (this->read_task_handle_ == nullptr) { + return ESP_ERR_INVALID_STATE; + } + } + + if (this->decode_task_handle_ == nullptr) { + if (this->decode_task_stack_buffer_ == nullptr) { + if (this->task_stack_in_psram_) { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); + this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); + } else { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); + } + } + + if (this->decode_task_stack_buffer_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + if (this->decode_task_handle_ == nullptr) { + this->decode_task_handle_ = + xTaskCreateStatic(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE, (void *) this, + this->priority_, this->decode_task_stack_buffer_, &this->decode_task_stack_); + } + + if (this->decode_task_handle_ == nullptr) { + return ESP_ERR_INVALID_STATE; + } + } + + return ESP_OK; +} + +void AudioPipeline::delete_tasks_() { + if (this->read_task_handle_ != nullptr) { + vTaskDelete(this->read_task_handle_); + + if (this->read_task_stack_buffer_ != nullptr) { + if (this->task_stack_in_psram_) { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); + stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); + } else { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); + } + + this->read_task_stack_buffer_ = nullptr; + this->read_task_handle_ = nullptr; + } + } + + if (this->decode_task_handle_ != nullptr) { + vTaskDelete(this->decode_task_handle_); + + if (this->decode_task_stack_buffer_ != nullptr) { + if (this->task_stack_in_psram_) { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); + stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); + } else { + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); + } + + this->decode_task_stack_buffer_ = nullptr; + this->decode_task_handle_ = nullptr; + } + } +} + +void AudioPipeline::read_task(void *params) { + AudioPipeline *this_pipeline = (AudioPipeline *) params; + + while (true) { + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); + + // Wait until the pipeline notifies us the source of the media file + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | + EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set + + if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { + xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED | + EventGroupBits::READER_COMMAND_INIT_FILE | + EventGroupBits::READER_COMMAND_INIT_HTTP); + InfoErrorEvent event; + event.source = InfoErrorSource::READER; + esp_err_t err = ESP_OK; + + std::unique_ptr reader = + make_unique(this_pipeline->transfer_buffer_size_); + + if (event_bits & EventGroupBits::READER_COMMAND_INIT_FILE) { + err = reader->start(this_pipeline->current_audio_file_, this_pipeline->current_audio_file_type_); + } else { + err = reader->start(this_pipeline->current_uri_, this_pipeline->current_audio_file_type_); + } + + if (err == ESP_OK) { + size_t file_ring_buffer_size = this_pipeline->buffer_size_; + + std::shared_ptr temp_ring_buffer; + + if (!this_pipeline->raw_file_ring_buffer_.use_count()) { + temp_ring_buffer = RingBuffer::create(file_ring_buffer_size); + this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer; + } + + if (!this_pipeline->raw_file_ring_buffer_.use_count()) { + err = ESP_ERR_NO_MEM; + } else { + reader->add_sink(this_pipeline->raw_file_ring_buffer_); + } + } + + if (err != ESP_OK) { + // Send specific error message + event.err = err; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + // Setting up the reader failed, stop the pipeline + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } else { + // Send the file type to the pipeline + event.file_type = this_pipeline->current_audio_file_type_; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); + } + + while (true) { + event_bits = xEventGroupGetBits(this_pipeline->event_group_); + + if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) { + break; + } + + audio::AudioReaderState reader_state = reader->read(); + + if (reader_state == audio::AudioReaderState::FINISHED) { + break; + } else if (reader_state == audio::AudioReaderState::FAILED) { + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + break; + } + } + event_bits = xEventGroupGetBits(this_pipeline->event_group_); + if ((event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) || + (this_pipeline->raw_file_ring_buffer_.use_count() == 1)) { + // Decoder task hasn't started yet, so delay a bit before releasing ownership of the ring buffer + delay(10); + } + } + } +} + +void AudioPipeline::decode_task(void *params) { + AudioPipeline *this_pipeline = (AudioPipeline *) params; + + while (true) { + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); + + // Wait until the reader notifies us that the media type is available + EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | + EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set + + if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { + xEventGroupClearBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); + InfoErrorEvent event; + event.source = InfoErrorSource::DECODER; + + std::unique_ptr decoder = + make_unique(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_); + + esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_); + decoder->add_source(this_pipeline->raw_file_ring_buffer_); + + if (err != ESP_OK) { + // Send specific error message + event.err = err; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + // Setting up the decoder failed, stop the pipeline + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } + + bool has_stream_info = false; + bool started_playback = false; + + size_t initial_bytes_to_buffer = 0; + + while (true) { + event_bits = xEventGroupGetBits(this_pipeline->event_group_); + + if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) { + break; + } + + // Update pause state + if (!started_playback) { + if (!(event_bits & EventGroupBits::READER_MESSAGE_FINISHED)) { + decoder->set_pause_output_state(true); + } else { + started_playback = true; + } + } else { + decoder->set_pause_output_state(this_pipeline->pause_state_); + } + + // Stop gracefully if the reader has finished + audio::AudioDecoderState decoder_state = decoder->decode(event_bits & EventGroupBits::READER_MESSAGE_FINISHED); + + if ((decoder_state == audio::AudioDecoderState::DECODING) || + (decoder_state == audio::AudioDecoderState::FINISHED)) { + this_pipeline->playback_ms_ = decoder->get_playback_ms(); + } + + if (decoder_state == audio::AudioDecoderState::FINISHED) { + break; + } else if (decoder_state == audio::AudioDecoderState::FAILED) { + if (!has_stream_info) { + event.decoding_err = DecodingError::FAILED_HEADER; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + } + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + break; + } + + if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { + has_stream_info = true; + + this_pipeline->current_audio_stream_info_ = decoder->get_audio_stream_info().value(); + + // Send the stream information to the pipeline + event.audio_stream_info = this_pipeline->current_audio_stream_info_; + + if (this_pipeline->current_audio_stream_info_.get_bits_per_sample() != 16) { + // Error state, incompatible bits per sample + event.decoding_err = DecodingError::INCOMPATIBLE_BITS_PER_SAMPLE; + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } else if ((this_pipeline->current_audio_stream_info_.get_channels() > 2)) { + // Error state, incompatible number of channels + event.decoding_err = DecodingError::INCOMPATIBLE_CHANNELS; + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } else { + // Send audio directly to the speaker + this_pipeline->speaker_->set_audio_stream_info(this_pipeline->current_audio_stream_info_); + decoder->add_sink(this_pipeline->speaker_); + } + + initial_bytes_to_buffer = std::min(this_pipeline->current_audio_stream_info_.ms_to_bytes(INITIAL_BUFFER_MS), + this_pipeline->buffer_size_ * 3 / 4); + + switch (this_pipeline->current_audio_file_type_) { +#ifdef USE_AUDIO_MP3_SUPPORT + case audio::AudioFileType::MP3: + initial_bytes_to_buffer /= 8; // Estimate the MP3 compression factor is 8 + break; +#endif +#ifdef USE_AUDIO_FLAC_SUPPORT + case audio::AudioFileType::FLAC: + initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2 + break; +#endif + default: + break; + } + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + } + + if (!started_playback && has_stream_info) { + // Verify enough data is available before starting playback + std::shared_ptr temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); + if (temp_ring_buffer->available() >= initial_bytes_to_buffer) { + started_playback = true; + } + } + } + } + } +} + +} // namespace speaker +} // namespace esphome + +#endif diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h new file mode 100644 index 0000000000..c382e1eebe --- /dev/null +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -0,0 +1,158 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio/audio_reader.h" +#include "esphome/components/audio/audio_decoder.h" +#include "esphome/components/speaker/speaker.h" + +#include "esphome/core/ring_buffer.h" + +#include "esp_err.h" + +#include +#include +#include + +namespace esphome { +namespace speaker { + +// Internal sink/source buffers for reader and decoder +static const size_t DEFAULT_TRANSFER_BUFFER_SIZE = 24 * 1024; + +enum class AudioPipelineType : uint8_t { + MEDIA, + ANNOUNCEMENT, +}; + +enum class AudioPipelineState : uint8_t { + STARTING_FILE, + STARTING_URL, + PLAYING, + STOPPING, + STOPPED, + PAUSED, + ERROR_READING, + ERROR_DECODING, +}; + +enum class InfoErrorSource : uint8_t { + READER = 0, + DECODER, +}; + +enum class DecodingError : uint8_t { + FAILED_HEADER = 0, + INCOMPATIBLE_BITS_PER_SAMPLE, + INCOMPATIBLE_CHANNELS, +}; + +// Used to pass information from each task. +struct InfoErrorEvent { + InfoErrorSource source; + optional err; + optional file_type; + optional audio_stream_info; + optional decoding_err; +}; + +class AudioPipeline { + public: + /// @param speaker ESPHome speaker component for pipeline's audio output + /// @param buffer_size Size of the buffer in bytes between the reader and decoder + /// @param task_stack_in_psram True if the task stack should be allocated in PSRAM, false otherwise + /// @param task_name FreeRTOS task base name + /// @param priority FreeRTOS task priority + AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram, std::string base_name, + UBaseType_t priority); + + /// @brief Starts an audio pipeline given a media url + /// @param uri media file url + /// @return ESP_OK if successful or an appropriate error if not + void start_url(const std::string &uri); + + /// @brief Starts an audio pipeline given a AudioFile pointer + /// @param audio_file pointer to an AudioFile object + /// @return ESP_OK if successful or an appropriate error if not + void start_file(audio::AudioFile *audio_file); + + /// @brief Stops the pipeline. Sends a stop signal to each task (if running) and clears the ring buffers. + /// @return ESP_OK if successful or ESP_ERR_TIMEOUT if the tasks did not indicate they stopped + esp_err_t stop(); + + /// @brief Processes the state of the audio pipeline based on the info_error_queue_ and event_group_. Handles creating + /// and stopping the pipeline tasks. Needs to be regularly called to update the internal pipeline state. + /// @return AudioPipelineState + AudioPipelineState process_state(); + + /// @brief Suspends any running tasks + void suspend_tasks(); + /// @brief Resumes any running tasks + void resume_tasks(); + + uint32_t get_playback_ms() { return this->playback_ms_; } + + void set_pause_state(bool pause_state); + + protected: + /// @brief Allocates the event group and info error queue. + /// @return ESP_OK if successful or ESP_ERR_NO_MEM if it is unable to allocate all parts + esp_err_t allocate_communications_(); + + /// @brief Common start code for the pipeline, regardless if the source is a file or url. + /// @return ESP_OK if successful or an appropriate error if not + esp_err_t start_tasks_(); + + /// @brief Resets the task related pointers and deallocates their stacks. + void delete_tasks_(); + + std::string base_name_; + UBaseType_t priority_; + + uint32_t playback_ms_{0}; + + bool is_playing_{false}; + bool pause_state_{false}; + bool task_stack_in_psram_; + + // Pending file start state used to ensure the pipeline fully stops before attempting to start the next file + bool pending_url_{false}; + bool pending_file_{false}; + + speaker::Speaker *speaker_{nullptr}; + + std::string current_uri_{}; + audio::AudioFile *current_audio_file_{nullptr}; + + audio::AudioFileType current_audio_file_type_; + audio::AudioStreamInfo current_audio_stream_info_; + + size_t buffer_size_; // Ring buffer between reader and decoder + size_t transfer_buffer_size_; // Internal source/sink buffers for the audio reader and decoder + + std::weak_ptr raw_file_ring_buffer_; + + // Handles basic control/state of the three tasks + EventGroupHandle_t event_group_{nullptr}; + + // Receives detailed info (file type, stream info, resampling info) or specific errors from the three tasks + QueueHandle_t info_error_queue_{nullptr}; + + // Handles reading the media file from flash or a url + static void read_task(void *params); + TaskHandle_t read_task_handle_{nullptr}; + StaticTask_t read_task_stack_; + StackType_t *read_task_stack_buffer_{nullptr}; + + // Decodes the media file into PCM audio + static void decode_task(void *params); + TaskHandle_t decode_task_handle_{nullptr}; + StaticTask_t decode_task_stack_; + StackType_t *decode_task_stack_buffer_{nullptr}; +}; + +} // namespace speaker +} // namespace esphome + +#endif diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h new file mode 100644 index 0000000000..d1a01aabc4 --- /dev/null +++ b/esphome/components/speaker/media_player/automation.h @@ -0,0 +1,26 @@ +#pragma once + +#include "speaker_media_player.h" + +#ifdef USE_ESP_IDF + +#include "esphome/components/audio/audio.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace speaker { + +template class PlayOnDeviceMediaAction : public Action, public Parented { + TEMPLATABLE_VALUE(audio::AudioFile *, audio_file) + TEMPLATABLE_VALUE(bool, announcement) + TEMPLATABLE_VALUE(bool, enqueue) + void play(Ts... x) override { + this->parent_->play_file(this->audio_file_.value(x...), this->announcement_.value(x...), + this->enqueue_.value(x...)); + } +}; + +} // namespace speaker +} // namespace esphome + +#endif diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp new file mode 100644 index 0000000000..0a2585ce60 --- /dev/null +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -0,0 +1,577 @@ +#include "speaker_media_player.h" + +#ifdef USE_ESP_IDF + +#include "esphome/core/log.h" + +#include "esphome/components/audio/audio.h" +#ifdef USE_OTA +#include "esphome/components/ota/ota_backend.h" +#endif + +namespace esphome { +namespace speaker { + +// Framework: +// - Media player that can handle two streams: one for media and one for announcements +// - Each stream has an individual speaker component for output +// - Each stream is handled by an ``AudioPipeline`` object with two parts/tasks +// - ``AudioReader`` handles reading from an HTTP source or from a PROGMEM flash set at compile time +// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per sample +// - FLAC +// - MP3 (based on the libhelix decoder) +// - WAV +// - Each task runs until it is done processing the file or it receives a stop command +// - Inter-task communication uses a FreeRTOS Event Group +// - The ``AudioPipeline`` sets up a ring buffer between the reader and decoder tasks. The decoder task outputs audio +// directly to a speaker component. +// - The pipelines internal state needs to be processed by regularly calling ``process_state``. +// - Generic media player commands are received by the ``control`` function. The commands are added to the +// ``media_control_command_queue_`` to be processed in the component's loop +// - Local file play back is initiatied with ``play_file`` and adds it to the ``media_control_command_queue_`` +// - Starting a stream intializes the appropriate pipeline or stops it if it is already running +// - Volume and mute commands are achieved by the ``mute``, ``unmute``, ``set_volume`` functions. +// - Volume commands are ignored if the media control queue is full to avoid crashing with rapid volume +// increases/decreases. +// - These functions all send the appropriate information to the speakers to implement. +// - Pausing is implemented in the decoder task and is also sent directly to the media speaker component to decrease +// latency. +// - The components main loop performs housekeeping: +// - It reads the media control queue and processes it directly +// - It determines the overall state of the media player by considering the state of each pipeline +// - announcement playback takes highest priority +// - Handles playlists and repeating by starting the appropriate file when a previous file is finished +// - Logging only happens in the main loop task to reduce task stack memory usage. + +static const uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20; + +static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1; +static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; + +static const float FIRST_BOOT_DEFAULT_VOLUME = 0.5f; + +static const char *const TAG = "speaker_media_player"; + +void SpeakerMediaPlayer::setup() { + state = media_player::MEDIA_PLAYER_STATE_IDLE; + + this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); + + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + + VolumeRestoreState volume_restore_state; + if (this->pref_.load(&volume_restore_state)) { + this->set_volume_(volume_restore_state.volume); + this->set_mute_state_(volume_restore_state.is_muted); + } else { + this->set_volume_(FIRST_BOOT_DEFAULT_VOLUME); + this->set_mute_state_(false); + } + +#ifdef USE_OTA + ota::get_global_ota_callback()->add_on_state_callback( + [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->suspend_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->suspend_tasks(); + } + } else if (state == ota::OTA_ERROR) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->resume_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->resume_tasks(); + } + } + }); +#endif + + this->announcement_pipeline_ = + make_unique(this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann", + ANNOUNCEMENT_PIPELINE_TASK_PRIORITY); + + if (this->announcement_pipeline_ == nullptr) { + ESP_LOGE(TAG, "Failed to create announcement pipeline"); + this->mark_failed(); + } + + if (!this->single_pipeline_()) { + this->media_pipeline_ = make_unique(this->media_speaker_, this->buffer_size_, + this->task_stack_in_psram_, "ann", MEDIA_PIPELINE_TASK_PRIORITY); + + if (this->media_pipeline_ == nullptr) { + ESP_LOGE(TAG, "Failed to create media pipeline"); + this->mark_failed(); + } + + // Setup callback to track the duration of audio played by the media pipeline + this->media_speaker_->add_audio_output_callback( + [this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) { + this->playback_ms_ += new_playback_ms; + this->remainder_us_ = remainder_us; + this->pending_ms_ = pending_ms; + this->last_audio_write_timestamp_ = write_timestamp; + this->playback_us_ = this->playback_ms_ * 1000 + this->remainder_us_; + }); + } + + ESP_LOGI(TAG, "Set up speaker media player"); +} + +void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms) { + switch (pipeline_type) { + case AudioPipelineType::ANNOUNCEMENT: + this->announcement_playlist_delay_ms_ = delay_ms; + break; + case AudioPipelineType::MEDIA: + this->media_playlist_delay_ms_ = delay_ms; + break; + } +} + +void SpeakerMediaPlayer::watch_media_commands_() { + if (!this->is_ready()) { + return; + } + + MediaCallCommand media_command; + esp_err_t err = ESP_OK; + + if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) { + bool new_url = media_command.new_url.has_value() && media_command.new_url.value(); + bool new_file = media_command.new_file.has_value() && media_command.new_file.value(); + + if (new_url || new_file) { + bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value(); + + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { + // Announcement playlist/pipeline + + if (!enqueue) { + // Clear the queue and ensure the loaded next item doesn't start playing + this->cancel_timeout("next_ann"); + this->announcement_playlist_.clear(); + } + + PlaylistItem playlist_item; + if (new_url) { + playlist_item.url = this->announcement_url_; + if (!enqueue) { + // Not adding to the queue, so directly start playback and internally unpause the pipeline + this->announcement_pipeline_->start_url(playlist_item.url.value()); + this->announcement_pipeline_->set_pause_state(false); + } + } else { + playlist_item.file = this->announcement_file_; + if (!enqueue) { + // Not adding to the queue, so directly start playback and internally unpause the pipeline + this->announcement_pipeline_->start_file(playlist_item.file.value()); + this->announcement_pipeline_->set_pause_state(false); + } + } + this->announcement_playlist_.push_back(playlist_item); + } else { + // Media playlist/pipeline + + if (!enqueue) { + // Clear the queue and ensure the loaded next item doesn't start playing + this->cancel_timeout("next_media"); + this->media_playlist_.clear(); + } + + this->is_paused_ = false; + PlaylistItem playlist_item; + if (new_url) { + playlist_item.url = this->media_url_; + if (!enqueue) { + // Not adding to the queue, so directly start playback and internally unpause the pipeline + this->media_pipeline_->start_url(playlist_item.url.value()); + this->media_pipeline_->set_pause_state(false); + } + } else { + playlist_item.file = this->media_file_; + if (!enqueue) { + // Not adding to the queue, so directly start playback and internally unpause the pipeline + this->media_pipeline_->start_file(playlist_item.file.value()); + this->media_pipeline_->set_pause_state(false); + } + } + this->media_playlist_.push_back(playlist_item); + } + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error starting the audio pipeline: %s", esp_err_to_name(err)); + this->status_set_error(); + } else { + this->status_clear_error(); + } + + return; // Don't process the new file play command further + } + + if (media_command.volume.has_value()) { + this->set_volume_(media_command.volume.value()); + this->publish_state(); + } + + if (media_command.command.has_value()) { + switch (media_command.command.value()) { + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) { + this->media_pipeline_->set_pause_state(false); + } + this->is_paused_ = false; + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + if ((this->media_pipeline_ != nullptr) && (!this->is_paused_)) { + this->media_pipeline_->set_pause_state(true); + } + this->is_paused_ = true; + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { + if (this->announcement_pipeline_ != nullptr) { + this->cancel_timeout("next_ann"); + this->announcement_playlist_.clear(); + this->announcement_pipeline_->stop(); + } + } else { + if (this->media_pipeline_ != nullptr) { + this->cancel_timeout("next_media"); + this->media_playlist_.clear(); + this->media_pipeline_->stop(); + } + } + break; + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: + if (this->media_pipeline_ != nullptr) { + if (this->is_paused_) { + this->media_pipeline_->set_pause_state(false); + this->is_paused_ = false; + } else { + this->media_pipeline_->set_pause_state(true); + this->is_paused_ = true; + } + } + break; + case media_player::MEDIA_PLAYER_COMMAND_MUTE: { + this->set_mute_state_(true); + + this->publish_state(); + break; + } + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->set_mute_state_(false); + this->publish_state(); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: + this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_)); + this->publish_state(); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_)); + this->publish_state(); + break; + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ONE: + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { + this->announcement_repeat_one_ = true; + } else { + this->media_repeat_one_ = true; + } + break; + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_OFF: + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { + this->announcement_repeat_one_ = false; + } else { + this->media_repeat_one_ = false; + } + break; + case media_player::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { + if (this->announcement_playlist_.empty()) { + this->announcement_playlist_.resize(1); + } + } else { + if (this->media_playlist_.empty()) { + this->media_playlist_.resize(1); + } + } + break; + default: + break; + } + } + } +} + +void SpeakerMediaPlayer::loop() { + this->watch_media_commands_(); + + // Determine state of the media player + media_player::MediaPlayerState old_state = this->state; + + AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_; + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_state_ = this->media_pipeline_->process_state(); + this->decoded_playback_ms_ = this->media_pipeline_->get_playback_ms(); + } + + if (this->media_pipeline_state_ == AudioPipelineState::ERROR_READING) { + ESP_LOGE(TAG, "The media pipeline's file reader encountered an error."); + } else if (this->media_pipeline_state_ == AudioPipelineState::ERROR_DECODING) { + ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error."); + } + + AudioPipelineState old_announcement_pipeline_state = this->announcement_pipeline_state_; + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_state_ = this->announcement_pipeline_->process_state(); + } + + if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_READING) { + ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error."); + } else if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_DECODING) { + ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error."); + } + + if (this->announcement_pipeline_state_ != AudioPipelineState::STOPPED) { + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + } else { + if (!this->announcement_playlist_.empty()) { + uint32_t timeout_ms = 0; + if (old_announcement_pipeline_state == AudioPipelineState::PLAYING) { + // Finished the current announcement file + if (!this->announcement_repeat_one_) { + // Pop item off the playlist if repeat is disabled + this->announcement_playlist_.pop_front(); + } + // Only delay starting playback if moving on the next playlist item or repeating the current item + timeout_ms = this->announcement_playlist_delay_ms_; + } + + if (!this->announcement_playlist_.empty()) { + // Start the next announcement file + PlaylistItem playlist_item = this->announcement_playlist_.front(); + if (playlist_item.url.has_value()) { + this->announcement_pipeline_->start_url(playlist_item.url.value()); + } else if (playlist_item.file.has_value()) { + this->announcement_pipeline_->start_file(playlist_item.file.value()); + } + + if (timeout_ms > 0) { + // Pause pipeline internally to facilitiate delay between items + this->announcement_pipeline_->set_pause_state(true); + // Internally unpause the pipeline after the delay between playlist items + this->set_timeout("next_ann", timeout_ms, + [this]() { this->announcement_pipeline_->set_pause_state(this->is_paused_); }); + } + } + } else { + if (this->is_paused_) { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) { + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + } else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { + // Reset playback durations + this->decoded_playback_ms_ = 0; + this->playback_us_ = 0; + this->playback_ms_ = 0; + this->remainder_us_ = 0; + this->pending_ms_ = 0; + + if (!media_playlist_.empty()) { + uint32_t timeout_ms = 0; + if (old_media_pipeline_state == AudioPipelineState::PLAYING) { + // Finished the current media file + if (!this->media_repeat_one_) { + // Pop item off the playlist if repeat is disabled + this->media_playlist_.pop_front(); + } + // Only delay starting playback if moving on the next playlist item or repeating the current item + timeout_ms = this->announcement_playlist_delay_ms_; + } + if (!this->media_playlist_.empty()) { + PlaylistItem playlist_item = this->media_playlist_.front(); + if (playlist_item.url.has_value()) { + this->media_pipeline_->start_url(playlist_item.url.value()); + } else if (playlist_item.file.has_value()) { + this->media_pipeline_->start_file(playlist_item.file.value()); + } + + if (timeout_ms > 0) { + // Pause pipeline internally to facilitiate delay between items + this->media_pipeline_->set_pause_state(true); + // Internally unpause the pipeline after the delay between playlist items + this->set_timeout("next_media", timeout_ms, + [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); }); + } + } + } else { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + } + } + } + } + + if (this->state != old_state) { + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } +} + +void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) { + if (!this->is_ready()) { + // Ignore any commands sent before the media player is setup + return; + } + + MediaCallCommand media_command; + + media_command.new_file = true; + if (this->single_pipeline_() || announcement) { + this->announcement_file_ = media_file; + media_command.announce = true; + } else { + this->media_file_ = media_file; + media_command.announce = false; + } + media_command.enqueue = enqueue; + xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY); +} + +void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (!this->is_ready()) { + // Ignore any commands sent before the media player is setup + return; + } + + MediaCallCommand media_command; + + if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) { + media_command.announce = true; + } else { + media_command.announce = false; + } + + if (call.get_media_url().has_value()) { + std::string new_uri = call.get_media_url().value(); + + media_command.new_url = true; + if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) { + this->announcement_url_ = new_uri; + } else { + this->media_url_ = new_uri; + } + + if (call.get_command().has_value()) { + if (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) { + media_command.enqueue = true; + } + } + + xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY); + return; + } + + if (call.get_volume().has_value()) { + media_command.volume = call.get_volume().value(); + // Wait 0 ticks for queue to be free, volume sets aren't that important! + xQueueSend(this->media_control_command_queue_, &media_command, 0); + return; + } + + if (call.get_command().has_value()) { + media_command.command = call.get_command().value(); + TickType_t ticks_to_wait = portMAX_DELAY; + if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || + (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { + ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important! + } + xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait); + return; + } +} + +media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() { + auto traits = media_player::MediaPlayerTraits(); + if (!this->single_pipeline_()) { + traits.set_supports_pause(true); + } + + if (this->announcement_format_.has_value()) { + traits.get_supported_formats().push_back(this->announcement_format_.value()); + } + if (this->media_format_.has_value()) { + traits.get_supported_formats().push_back(this->media_format_.value()); + } else if (this->single_pipeline_() && this->announcement_format_.has_value()) { + // Only one pipeline is defined, so use the announcement format (if configured) for the default purpose + media_player::MediaPlayerSupportedFormat media_format = this->announcement_format_.value(); + media_format.purpose = media_player::MediaPlayerFormatPurpose::PURPOSE_DEFAULT; + traits.get_supported_formats().push_back(media_format); + } + + return traits; +}; + +void SpeakerMediaPlayer::save_volume_restore_state_() { + VolumeRestoreState volume_restore_state; + volume_restore_state.volume = this->volume; + volume_restore_state.is_muted = this->is_muted_; + this->pref_.save(&volume_restore_state); +} + +void SpeakerMediaPlayer::set_mute_state_(bool mute_state) { + if (this->media_speaker_ != nullptr) { + this->media_speaker_->set_mute_state(mute_state); + } + if (this->announcement_speaker_ != nullptr) { + this->announcement_speaker_->set_mute_state(mute_state); + } + + bool old_mute_state = this->is_muted_; + this->is_muted_ = mute_state; + + this->save_volume_restore_state_(); + + if (old_mute_state != mute_state) { + if (mute_state) { + this->defer([this]() { this->mute_trigger_->trigger(); }); + } else { + this->defer([this]() { this->unmute_trigger_->trigger(); }); + } + } +} + +void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { + // Remap the volume to fit with in the configured limits + float bounded_volume = remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); + + if (this->media_speaker_ != nullptr) { + this->media_speaker_->set_volume(bounded_volume); + } + + if (this->announcement_speaker_ != nullptr) { + this->announcement_speaker_->set_volume(bounded_volume); + } + + if (publish) { + this->volume = volume; + this->save_volume_restore_state_(); + } + + // Turn on the mute state if the volume is effectively zero, off otherwise + if (volume < 0.001) { + this->set_mute_state_(true); + } else { + this->set_mute_state_(false); + } + + this->defer([this, volume]() { this->volume_trigger_->trigger(volume); }); +} + +} // namespace speaker +} // namespace esphome + +#endif diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h new file mode 100644 index 0000000000..6cbce91866 --- /dev/null +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -0,0 +1,160 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "audio_pipeline.h" + +#include "esphome/components/audio/audio.h" + +#include "esphome/components/media_player/media_player.h" +#include "esphome/components/speaker/speaker.h" + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +#include +#include +#include + +namespace esphome { +namespace speaker { + +struct MediaCallCommand { + optional command; + optional volume; + optional announce; + optional new_url; + optional new_file; + optional enqueue; +}; + +struct PlaylistItem { + optional url; + optional file; +}; + +struct VolumeRestoreState { + float volume; + bool is_muted; +}; + +class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer { + public: + float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } + void setup() override; + void loop() override; + + // MediaPlayer implementations + media_player::MediaPlayerTraits get_traits() override; + bool is_muted() const override { return this->is_muted_; } + + void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; } + void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + + // Percentage to increase or decrease the volume for volume up or volume down commands + void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; } + + void set_volume_max(float volume_max) { this->volume_max_ = volume_max; } + void set_volume_min(float volume_min) { this->volume_min_ = volume_min; } + + void set_announcement_speaker(Speaker *announcement_speaker) { this->announcement_speaker_ = announcement_speaker; } + void set_announcement_format(const media_player::MediaPlayerSupportedFormat &announcement_format) { + this->announcement_format_ = announcement_format; + } + void set_media_speaker(Speaker *media_speaker) { this->media_speaker_ = media_speaker; } + void set_media_format(const media_player::MediaPlayerSupportedFormat &media_format) { + this->media_format_ = media_format; + } + + Trigger<> *get_mute_trigger() const { return this->mute_trigger_; } + Trigger<> *get_unmute_trigger() const { return this->unmute_trigger_; } + Trigger *get_volume_trigger() const { return this->volume_trigger_; } + + void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue); + + uint32_t get_playback_ms() const { return this->playback_ms_; } + uint32_t get_playback_us() const { return this->playback_us_; } + uint32_t get_decoded_playback_ms() const { return this->decoded_playback_ms_; } + + void set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms); + + protected: + // Receives commands from HA or from the voice assistant component + // Sends commands to the media_control_commanda_queue_ + void control(const media_player::MediaPlayerCall &call) override; + + /// @brief Updates this->volume and saves volume/mute state to flash for restortation if publish is true. + void set_volume_(float volume, bool publish = true); + + /// @brief Sets the mute state. Restores previous volume if unmuting. Always saves volume/mute state to flash for + /// restoration. + /// @param mute_state If true, audio will be muted. If false, audio will be unmuted + void set_mute_state_(bool mute_state); + + /// @brief Saves the current volume and mute state to the flash for restoration. + void save_volume_restore_state_(); + + /// Returns true if the media player has only the announcement pipeline defined, false if both the announcement and + /// media pipelines are defined. + inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); } + + // Processes commands from media_control_command_queue_. + void watch_media_commands_(); + + std::unique_ptr announcement_pipeline_; + std::unique_ptr media_pipeline_; + Speaker *media_speaker_{nullptr}; + Speaker *announcement_speaker_{nullptr}; + + optional media_format_; + AudioPipelineState media_pipeline_state_{AudioPipelineState::STOPPED}; + std::string media_url_{}; // only modified by control function + audio::AudioFile *media_file_{}; // only modified by play_file function + bool media_repeat_one_{false}; + uint32_t media_playlist_delay_ms_{0}; + + optional announcement_format_; + AudioPipelineState announcement_pipeline_state_{AudioPipelineState::STOPPED}; + std::string announcement_url_{}; // only modified by control function + audio::AudioFile *announcement_file_{}; // only modified by play_file function + bool announcement_repeat_one_{false}; + uint32_t announcement_playlist_delay_ms_{0}; + + QueueHandle_t media_control_command_queue_; + + std::deque announcement_playlist_; + std::deque media_playlist_; + + size_t buffer_size_; + + bool task_stack_in_psram_; + + bool is_paused_{false}; + bool is_muted_{false}; + + // The amount to change the volume on volume up/down commands + float volume_increment_; + + float volume_max_; + float volume_min_; + + // Used to save volume/mute state for restoration on reboot + ESPPreferenceObject pref_; + + Trigger<> *mute_trigger_ = new Trigger<>(); + Trigger<> *unmute_trigger_ = new Trigger<>(); + Trigger *volume_trigger_ = new Trigger(); + + uint32_t decoded_playback_ms_{0}; + uint32_t playback_us_{0}; + uint32_t playback_ms_{0}; + uint32_t remainder_us_{0}; + uint32_t pending_ms_{0}; + uint32_t last_audio_write_timestamp_{0}; +}; + +} // namespace speaker +} // namespace esphome + +#endif diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index af0d5c7765..763bc231c0 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -21,6 +21,8 @@ media_player: - media_player.pause: on_play: - media_player.stop: + - media_player.stop: + announcement: true on_pause: - media_player.toggle: - wait_until: diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml new file mode 100644 index 0000000000..edc9f670fc --- /dev/null +++ b/tests/components/speaker/common-media_player.yaml @@ -0,0 +1,12 @@ +<<: !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 diff --git a/tests/components/speaker/media_player.esp32-idf.yaml b/tests/components/speaker/media_player.esp32-idf.yaml new file mode 100644 index 0000000000..4712e4bae8 --- /dev/null +++ b/tests/components/speaker/media_player.esp32-idf.yaml @@ -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.yaml diff --git a/tests/components/speaker/media_player.esp32-s3-idf.yaml b/tests/components/speaker/media_player.esp32-s3-idf.yaml new file mode 100644 index 0000000000..b3eec04d23 --- /dev/null +++ b/tests/components/speaker/media_player.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + scl_pin: GPIO2 + sda_pin: GPIO3 + i2s_bclk_pin: GPIO4 + i2s_lrclk_pin: GPIO5 + i2s_mclk_pin: GPIO6 + i2s_dout_pin: GPIO7 + +<<: !include common-media_player.yaml From abdf215d3a54eff4b33b5d76207372245b5d1031 Mon Sep 17 00:00:00 2001 From: mystster Date: Tue, 11 Feb 2025 05:29:27 +0900 Subject: [PATCH 02/31] Add partial update of GDEW029T5 e-paper display (#8162) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 2 +- .../waveshare_epaper/waveshare_epaper.cpp | 242 ++++++++++++++++-- .../waveshare_epaper/waveshare_epaper.h | 21 +- tests/components/waveshare_epaper/common.yaml | 1 + 4 files changed, 236 insertions(+), 30 deletions(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 3a5151682f..81afc159a5 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -120,7 +120,7 @@ MODELS = { "2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74), "2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), - "gdew029t5": ("b", GDEW029T5), + "gdew029t5": ("c", GDEW029T5), "2.70in": ("b", WaveshareEPaper2P7In), "2.70in-b": ("b", WaveshareEPaper2P7InB), "2.70in-bv2": ("b", WaveshareEPaper2P7InBV2), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index fb9e8ff6e5..2ff271f999 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1596,15 +1596,108 @@ void WaveshareEPaper2P9InV2R2::set_full_update_every(uint32_t full_update_every) // Datasheet: // - https://v4.cecdn.yun300.cn/100001_1909185148/SSD1680.pdf // - https://github.com/adafruit/Adafruit_EPD/blob/master/src/panels/ThinkInk_290_Grayscale4_T5.h +// - https://github.com/ZinggJM/GxEPD2/blob/master/src/epd/GxEPD2_290_T5.cpp +// - http://www.e-paper-display.com/GDEW029T5%20V3.1%20Specification5c22.pdf? // ======================================================== -void GDEW029T5::initialize() { - // from https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf, page 37 - // EPD hardware init start - this->reset_(); +// full screen update LUT +static const uint8_t LUT_20_VCOMDC_29_5[] = { + 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x60, 0x28, 0x28, 0x00, 0x00, 0x01, 0x00, 0x14, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x12, 0x12, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_21_WW_29_5[] = { + 0x40, 0x08, 0x00, 0x00, 0x00, 0x02, 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, 0x40, 0x14, + 0x00, 0x00, 0x00, 0x01, 0xA0, 0x12, 0x12, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_22_BW_29_5[] = { + 0x40, 0x08, 0x00, 0x00, 0x00, 0x02, 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, 0x40, 0x14, + 0x00, 0x00, 0x00, 0x01, 0xA0, 0x12, 0x12, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_23_WB_29_5[] = { + 0x80, 0x08, 0x00, 0x00, 0x00, 0x02, 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, 0x80, 0x14, + 0x00, 0x00, 0x00, 0x01, 0x50, 0x12, 0x12, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_24_BB_29_5[] = { + 0x80, 0x08, 0x00, 0x00, 0x00, 0x02, 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, 0x80, 0x14, + 0x00, 0x00, 0x00, 0x01, 0x50, 0x12, 0x12, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +// partial screen update LUT +static const uint8_t LUT_20_VCOMDC_PARTIAL_29_5[] = { + 0x00, 0x20, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_21_WW_PARTIAL_29_5[] = { + 0x00, 0x20, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_22_BW_PARTIAL_29_5[] = { + 0x80, 0x20, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_23_WB_PARTIAL_29_5[] = { + 0x40, 0x20, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t LUT_24_BB_PARTIAL_29_5[] = { + 0x00, 0x20, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +void GDEW029T5::power_on_() { + if (!this->power_is_on_) { + this->command(0x04); + this->wait_until_idle_(); + } + this->power_is_on_ = true; +} + +void GDEW029T5::power_off_() { + this->command(0x02); + this->wait_until_idle_(); + this->power_is_on_ = false; +} + +void GDEW029T5::deep_sleep() { + this->power_off_(); + if (this->deep_sleep_between_updates_) { + this->command(0x07); // deep sleep + this->data(0xA5); // check code + ESP_LOGD(TAG, "go to deep sleep"); + this->is_deep_sleep_ = true; + } +} + +void GDEW029T5::init_display_() { + // from https://github.com/ZinggJM/GxEPD2/blob/master/src/epd/GxEPD2_290_T5.cpp + + // Hardware Initialization + if (this->deep_sleep_between_updates_ && this->is_deep_sleep_) { + ESP_LOGI(TAG, "wake up from deep sleep"); + this->reset_(); + this->is_deep_sleep_ = false; + } // COMMAND POWER SETTINGS - this->command(0x00); + this->command(0x01); this->data(0x03); this->data(0x00); this->data(0x2b); @@ -1617,40 +1710,122 @@ void GDEW029T5::initialize() { this->data(0x17); this->data(0x17); - // COMMAND POWER ON - this->command(0x04); - this->wait_until_idle_(); - - // Not sure what this does but it's in the Adafruit EPD library - this->command(0xFF); - this->wait_until_idle_(); + this->power_on_(); // COMMAND PANEL SETTING this->command(0x00); // 128x296 resolution: 10 - // LUT from OTP: 0 + // LUT from register: 1 // B/W mode (doesn't work): 1 // scan-up: 1 // shift-right: 1 // booster ON: 1 // no soft reset: 1 - this->data(0b10011111); + this->data(0b10111111); + this->data(0x0d); // VCOM to 0V fast + this->command(0x30); // PLL setting + this->data(0x3a); // 3a 100HZ 29 150Hz 39 200HZ 31 171HZ + this->command(0x61); // resolution setting + this->data(this->get_width_internal()); + this->data(this->get_height_internal() >> 8); + this->data(this->get_height_internal() & 0xFF); - // COMMAND RESOLUTION SETTING - // set to 128x296 by COMMAND PANEL SETTING - - // COMMAND VCOM AND DATA INTERVAL SETTING - // use defaults for white border and ESPHome image polarity - - // EPD hardware init end + ESP_LOGD(TAG, "panel setting done"); } + +void GDEW029T5::initialize() { + // from https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf, page 37 + if (this->reset_pin_ != nullptr) + this->deep_sleep_between_updates_ = true; + + // old buffer for partial update + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->old_buffer_ = allocator.allocate(this->get_buffer_length_()); + if (this->old_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate old buffer for display!"); + return; + } + for (size_t i = 0; i < this->get_buffer_length_(); i++) { + this->old_buffer_[i] = 0xFF; + } +} + +// initialize for full(normal) update +void GDEW029T5::init_full_() { + this->init_display_(); + this->command(0x82); // vcom_DC setting + this->data(0x08); + this->command(0X50); // VCOM AND DATA INTERVAL SETTING + this->data(0x97); // WBmode:VBDF 17|D7 VBDW 97 VBDB 57 WBRmode:VBDF F7 VBDW 77 VBDB 37 VBDR B7 + this->command(0x20); + this->write_lut_(LUT_20_VCOMDC_29_5, sizeof(LUT_20_VCOMDC_29_5)); + this->command(0x21); + this->write_lut_(LUT_21_WW_29_5, sizeof(LUT_21_WW_29_5)); + this->command(0x22); + this->write_lut_(LUT_22_BW_29_5, sizeof(LUT_22_BW_29_5)); + this->command(0x23); + this->write_lut_(LUT_23_WB_29_5, sizeof(LUT_23_WB_29_5)); + this->command(0x24); + this->write_lut_(LUT_24_BB_29_5, sizeof(LUT_24_BB_29_5)); + ESP_LOGD(TAG, "initialized full update"); +} + +// initialzie for partial update +void GDEW029T5::init_partial_() { + this->init_display_(); + this->command(0x82); // vcom_DC setting + this->data(0x08); + this->command(0X50); // VCOM AND DATA INTERVAL SETTING + this->data(0x17); // WBmode:VBDF 17|D7 VBDW 97 VBDB 57 WBRmode:VBDF F7 VBDW 77 VBDB 37 VBDR B7 + this->command(0x20); + this->write_lut_(LUT_20_VCOMDC_PARTIAL_29_5, sizeof(LUT_20_VCOMDC_PARTIAL_29_5)); + this->command(0x21); + this->write_lut_(LUT_21_WW_PARTIAL_29_5, sizeof(LUT_21_WW_PARTIAL_29_5)); + this->command(0x22); + this->write_lut_(LUT_22_BW_PARTIAL_29_5, sizeof(LUT_22_BW_PARTIAL_29_5)); + this->command(0x23); + this->write_lut_(LUT_23_WB_PARTIAL_29_5, sizeof(LUT_23_WB_PARTIAL_29_5)); + this->command(0x24); + this->write_lut_(LUT_24_BB_PARTIAL_29_5, sizeof(LUT_24_BB_PARTIAL_29_5)); + ESP_LOGD(TAG, "initialized partial update"); +} + void HOT GDEW029T5::display() { + bool full_update = this->at_update_ == 0; + if (full_update) { + this->init_full_(); + } else { + this->init_partial_(); + this->command(0x91); // partial in + // set partial window + this->command(0x90); + // this->data(0); + this->data(0); + // this->data(0); + this->data((this->get_width_internal() - 1) % 256); + this->data(0); + this->data(0); + this->data(((this->get_height_internal() - 1)) / 256); + this->data(((this->get_height_internal() - 1)) % 256); + this->data(0x01); + } + // input old buffer data + this->command(0x10); + delay(2); + this->start_data_(); + for (size_t i = 0; i < this->get_buffer_length_(); i++) { + this->write_byte(this->old_buffer_[i]); + } + this->end_data_(); + delay(2); + // COMMAND DATA START TRANSMISSION 2 (B/W only) this->command(0x13); delay(2); this->start_data_(); for (size_t i = 0; i < this->get_buffer_length_(); i++) { this->write_byte(this->buffer_[i]); + this->old_buffer_[i] = this->buffer_[i]; } this->end_data_(); delay(2); @@ -1660,10 +1835,28 @@ void HOT GDEW029T5::display() { delay(2); this->wait_until_idle_(); - // COMMAND POWER OFF - // NOTE: power off < deep sleep - this->command(0x02); + if (full_update) { + ESP_LOGD(TAG, "full update done"); + } else { + this->command(0x92); // partial out + ESP_LOGD(TAG, "partial update done"); + } + + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + // COMMAND deep sleep + this->deep_sleep(); } + +void GDEW029T5::write_lut_(const uint8_t *lut, const uint8_t size) { + // COMMAND WRITE LUT REGISTER + this->start_data_(); + for (uint8_t i = 0; i < size; i++) + this->write_byte(lut[i]); + this->end_data_(); +} + +void GDEW029T5::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } + int GDEW029T5::get_width_internal() { return 128; } int GDEW029T5::get_height_internal() { return 296; } void GDEW029T5::dump_config() { @@ -1672,6 +1865,7 @@ void GDEW029T5::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 0fc1051268..e0c8001a50 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -254,16 +254,27 @@ class GDEW029T5 : public WaveshareEPaper { void dump_config() override; - void deep_sleep() override { - // COMMAND DEEP SLEEP - this->command(0x07); - this->data(0xA5); // check byte - } + void deep_sleep() override; + void set_full_update_every(uint32_t full_update_every); protected: + void init_display_(); + void init_full_(); + void init_partial_(); + void write_lut_(const uint8_t *lut, uint8_t size); + void power_off_(); + void power_on_(); int get_width_internal() override; int get_height_internal() override; + + private: + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + bool deep_sleep_between_updates_{false}; + bool power_is_on_{false}; + bool is_deep_sleep_{false}; + uint8_t *old_buffer_{nullptr}; }; class WaveshareEPaper2P7InV2 : public WaveshareEPaper { diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index ff9ddb955f..1d77e0cf38 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -459,6 +459,7 @@ display: reset_pin: allow_other_uses: true number: ${reset_pin} + full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); From b667ceacedbbc5f248801460aef6432b62c6097c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=A1ta=20GEEK?= <63705187+TataGEEK@users.noreply.github.com> Date: Tue, 11 Feb 2025 01:35:56 +0100 Subject: [PATCH 03/31] Add waveshare 2.9inch e-Paper HAT (D) (#7906) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_epaper.cpp | 87 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 20 +++++ tests/components/waveshare_epaper/common.yaml | 19 ++++ 4 files changed, 130 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 81afc159a5..f0064c6a4d 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -55,6 +55,9 @@ GDEW029T5 = waveshare_epaper_ns.class_("GDEW029T5", WaveshareEPaper) WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InDKE", WaveshareEPaper ) +WaveshareEPaper2P9InD = waveshare_epaper_ns.class_( + "WaveshareEPaper2P9InD", WaveshareEPaper +) WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper ) @@ -128,6 +131,7 @@ MODELS = { "2.90in-b": ("b", WaveshareEPaper2P9InB), "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), + "2.90in-d": ("b", WaveshareEPaper2P9InD), "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 2ff271f999..f240c98096 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1211,6 +1211,93 @@ void WaveshareEPaper2P9InB::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// Waveshare 2.9-inch E-Paper (Type D) +// Waveshare WIKI: https://www.waveshare.com/wiki/Pico-ePaper-2.9-D +// Datasheet: https://www.waveshare.com/w/upload/b/b5/2.9inch_e-Paper_(D)_Specification.pdf +// ======================================================== + +void WaveshareEPaper2P9InD::initialize() { + // EPD hardware init start + this->reset_(); + + // Booster Soft Start + this->command(0x06); // Command: BTST + this->data(0x17); // Soft start configuration Phase A + this->data(0x17); // Soft start configuration Phase B + this->data(0x17); // Soft start configuration Phase C + + // Power Setting + this->command(0x01); // Command: PWR + this->data(0x03); // Intern DC/DC for VDH/VDL and VGH/VGL + this->data(0x00); // Default configuration VCOM_HV and VGHL_LV + this->data(0x2b); // VDH = 10.8 V + this->data(0x2b); // VDL = -10.8 V + + // Power ON + this->command(0x04); // Command: PON + this->wait_until_idle_(); + + // Panel settings + this->command(0x00); // Command: PSR + this->data(0x1F); // LUT from OTP, black and white mode, default scan + + // PLL Control + this->command(0x30); // Command: PLL + this->data(0x3A); // Default PLL frequency + + // Resolution settings + this->command(0x61); // Command: TRES + this->data(0x80); // Width: 128 + this->data(0x01); // Height MSB: 296 + this->data(0x28); // Height LSB: 296 + + // VCOM and data interval settings + this->command(0x50); // Command: CDI + this->data(0x77); + + // VCOM_DC settings + this->command(0x82); // Command: VDCS + this->data(0x12); // Dafault VCOM_DC +} + +void WaveshareEPaper2P9InD::display() { + // Start transmitting old data (clearing buffer) + this->command(0x10); // Command: DTM1 (OLD frame data) + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // Start transmitting new data (updated content) + this->command(0x13); // Command: DTM2 (NEW frame data) + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // Refresh Display + this->command(0x12); // Command: DRF + this->wait_until_idle_(); + + // Enter Power Off + this->command(0x02); // Command: POF + this->wait_until_idle_(); + + // Enter Deep Sleep + this->command(0x07); // Command: DSLP + this->data(0xA5); +} + +int WaveshareEPaper2P9InD::get_width_internal() { return 128; } +int WaveshareEPaper2P9InD::get_height_internal() { return 296; } +void WaveshareEPaper2P9InD::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in (D)"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + // DKE 2.9 // https://www.badge.team/docs/badges/sha2017/hardware/#e-ink-display-the-dke-group-depg0290b1 // https://www.badge.team/docs/badges/sha2017/hardware/DEPG0290B01V3.0.pdf diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index e0c8001a50..c2277f2962 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -427,6 +427,26 @@ class WaveshareEPaper2P9InDKE : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P9InD : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index 1d77e0cf38..4a67ccd67d 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -714,6 +714,25 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + model: 2.90in-d + spi_id: spi_waveshare_epaper + cs_pin: + allow_other_uses: true + number: ${cs_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + busy_pin: + allow_other_uses: true + number: ${busy_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} + reset_duration: 200ms + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper model: 2.90in spi_id: spi_waveshare_epaper From 8b7aa4c1102f2102d38a2d189e678b6761ee7d51 Mon Sep 17 00:00:00 2001 From: guillempages Date: Tue, 11 Feb 2025 01:39:03 +0100 Subject: [PATCH 04/31] [http_request]Use std::string for headers (#8225) --- esphome/components/http_request/http_request.h | 4 ++-- esphome/components/http_request/http_request_arduino.cpp | 2 +- esphome/components/http_request/http_request_idf.cpp | 2 +- esphome/components/online_image/online_image.cpp | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index b2ce718ec4..e98fd1a475 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -18,8 +18,8 @@ namespace esphome { namespace http_request { struct Header { - const char *name; - const char *value; + std::string name; + std::string value; }; // Some common HTTP status codes diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 85a1312aaa..b0067e7839 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -96,7 +96,7 @@ std::shared_ptr HttpRequestArduino::start(std::string url, std::s container->client_.setUserAgent(this->useragent_); } for (const auto &header : headers) { - container->client_.addHeader(header.name, header.value, false, true); + container->client_.addHeader(header.name.c_str(), header.value.c_str(), false, true); } // returned needed headers must be collected before the requests diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 66f064c2ce..78c37403f5 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -84,7 +84,7 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin container->set_secure(secure); for (const auto &header : headers) { - esp_http_client_set_header(client, header.name, header.value); + esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); } const int body_len = body.length(); diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index da0e88a904..a1c530c2ab 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -124,7 +124,7 @@ void OnlineImage::update() { default: accept_mime_type = "image/*"; } - accept_header.value = (accept_mime_type + ",*/*;q=0.8").c_str(); + accept_header.value = accept_mime_type + ",*/*;q=0.8"; headers.push_back(accept_header); From c9e7562aff0b3a94c47baf3dc946646564432a3e Mon Sep 17 00:00:00 2001 From: guillempages Date: Tue, 11 Feb 2025 12:12:13 +0100 Subject: [PATCH 05/31] [online_image] Improve error handling (#8212) --- esphome/components/online_image/image_decoder.cpp | 12 +++++++++++- esphome/components/online_image/image_decoder.h | 11 ++++++----- esphome/components/online_image/jpeg_image.cpp | 7 ++++--- esphome/components/online_image/jpeg_image.h | 2 +- esphome/components/online_image/online_image.cpp | 7 ++++++- esphome/components/online_image/png_image.cpp | 7 ++++++- esphome/components/online_image/png_image.h | 2 +- 7 files changed, 35 insertions(+), 13 deletions(-) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp index 2958d8671d..d8c0cc33c4 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/image_decoder.cpp @@ -25,6 +25,15 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { } } +DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { + this->buffer_ = this->allocator_.allocate(size); + this->reset(); + if (!this->buffer_) { + ESP_LOGE(TAG, "Initial allocation of download buffer failed!"); + this->size_ = 0; + } +} + uint8_t *DownloadBuffer::data(size_t offset) { if (offset > this->size_) { ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); @@ -46,12 +55,13 @@ size_t DownloadBuffer::resize(size_t size) { return size; } this->allocator_.deallocate(this->buffer_, this->size_); - this->size_ = size; this->buffer_ = this->allocator_.allocate(size); this->reset(); if (this->buffer_) { + this->size_ = size; return size; } else { + this->size_ = 0; return 0; } } diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 957af49ac9..d11b8b46d3 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -29,8 +29,12 @@ class ImageDecoder { * @brief Initialize the decoder. * * @param download_size The total number of bytes that need to be downloaded for the image. + * @return int Returns 0 on success, a {@see DecodeError} value in case of an error. */ - virtual void prepare(size_t download_size) { this->download_size_ = download_size; } + virtual int prepare(size_t download_size) { + this->download_size_ = download_size; + return 0; + } /** * @brief Decode a part of the image. It will try reading from the buffer. @@ -83,10 +87,7 @@ class ImageDecoder { class DownloadBuffer { public: - DownloadBuffer(size_t size) : size_(size) { - this->buffer_ = this->allocator_.allocate(size); - this->reset(); - } + DownloadBuffer(size_t size); virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/online_image/jpeg_image.cpp index 773b85a2c4..0aff576da8 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/online_image/jpeg_image.cpp @@ -41,13 +41,14 @@ static int draw_callback(JPEGDRAW *jpeg) { return 1; } -void JpegDecoder::prepare(size_t download_size) { +int JpegDecoder::prepare(size_t download_size) { ImageDecoder::prepare(download_size); auto size = this->image_->resize_download_buffer(download_size); if (size < download_size) { - ESP_LOGE(TAG, "Resize failed!"); - // TODO: return an error code; + ESP_LOGE(TAG, "Download buffer resize failed!"); + return DECODE_ERROR_OUT_OF_MEMORY; } + return 0; } int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { diff --git a/esphome/components/online_image/jpeg_image.h b/esphome/components/online_image/jpeg_image.h index f04a35655a..fd488d6138 100644 --- a/esphome/components/online_image/jpeg_image.h +++ b/esphome/components/online_image/jpeg_image.h @@ -21,7 +21,7 @@ class JpegDecoder : public ImageDecoder { JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} ~JpegDecoder() override {} - void prepare(size_t download_size) override; + int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index a1c530c2ab..3b3d00a044 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -178,7 +178,12 @@ void OnlineImage::update() { this->download_error_callback_.call(); return; } - this->decoder_->prepare(total_size); + auto prepare_result = this->decoder_->prepare(total_size); + if (prepare_result < 0) { + this->end_connection_(); + this->download_error_callback_.call(); + return; + } ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size); this->start_time_ = ::time(nullptr); } diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 6bc968c7ba..fc5fb554bf 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -40,11 +40,16 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui decoder->draw(x, y, w, h, color); } -void PngDecoder::prepare(size_t download_size) { +int PngDecoder::prepare(size_t download_size) { ImageDecoder::prepare(download_size); + if (!this->pngle_) { + ESP_LOGE(TAG, "PNG decoder engine not initialized!"); + return DECODE_ERROR_OUT_OF_MEMORY; + } pngle_set_user_data(this->pngle_, this); pngle_set_init_callback(this->pngle_, init_callback); pngle_set_draw_callback(this->pngle_, draw_callback); + return 0; } int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index c137227907..39f445c588 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -21,7 +21,7 @@ class PngDecoder : public ImageDecoder { PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} ~PngDecoder() override { pngle_destroy(this->pngle_); } - void prepare(size_t download_size) override; + int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: From 46d19d82c2df3885690ed3f67a78e95728bafb77 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 11 Feb 2025 12:14:59 -0600 Subject: [PATCH 06/31] [speaker] Bugfix: Ensure all audio is played after completely decoding a file (#8231) --- .../components/speaker/media_player/audio_pipeline.cpp | 10 +++++++++- .../components/speaker/media_player/audio_pipeline.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 73ec5a3334..b49cf3ddda 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -182,13 +182,21 @@ AudioPipelineState AudioPipeline::process_state() { if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) { // Stop command is fully processed, so clear the command bit xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); + this->hard_stop_ = true; } if (!this->is_playing_) { // The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) { this->delete_tasks_(); - this->speaker_->stop(); + if (this->hard_stop_) { + // Stop command was sent, so immediately end of the playback + this->speaker_->stop(); + this->hard_stop_ = false; + } else { + // Decoded all the audio, so let the speaker finish playing before stopping + this->speaker_->finish(); + } } } this->is_playing_ = false; diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index c382e1eebe..722d9cbb2a 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -112,6 +112,7 @@ class AudioPipeline { uint32_t playback_ms_{0}; + bool hard_stop_{false}; bool is_playing_{false}; bool pause_state_{false}; bool task_stack_in_psram_; From 33f9d66e8188ce68c7083aac909857cdd5281069 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 11 Feb 2025 12:20:39 -0600 Subject: [PATCH 07/31] [voice_assistant] Add announce support (#8232) --- .../voice_assistant/voice_assistant.cpp | 38 ++++++++++++------- .../voice_assistant/voice_assistant.h | 8 ++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 6f164f69d3..4b02867967 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -1,4 +1,5 @@ #include "voice_assistant.h" +#include "esphome/core/defines.h" #ifdef USE_VOICE_ASSISTANT @@ -127,7 +128,7 @@ void VoiceAssistant::clear_buffers_() { } #ifdef USE_SPEAKER - if (this->speaker_buffer_ != nullptr) { + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); this->speaker_buffer_size_ = 0; @@ -159,7 +160,7 @@ void VoiceAssistant::deallocate_buffers_() { this->input_buffer_ = nullptr; #ifdef USE_SPEAKER - if (this->speaker_buffer_ != nullptr) { + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { ExternalRAMAllocator speaker_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE); this->speaker_buffer_ = nullptr; @@ -389,14 +390,7 @@ void VoiceAssistant::loop() { } #endif if (playing) { - this->set_timeout("playing", 2000, [this]() { - this->cancel_timeout("speaker-timeout"); - this->set_state_(State::IDLE, State::IDLE); - - api::VoiceAssistantAnnounceFinished msg; - msg.success = true; - this->api_client_->send_voice_assistant_announce_finished(msg); - }); + this->start_playback_timeout_(); } break; } @@ -614,6 +608,8 @@ void VoiceAssistant::request_stop() { this->desired_state_ = State::IDLE; break; case State::AWAITING_RESPONSE: + this->signal_stop_(); + break; case State::STREAMING_RESPONSE: case State::RESPONSE_FINISHED: break; // Let the incoming audio stream finish then it will go to idle. @@ -631,6 +627,17 @@ void VoiceAssistant::signal_stop_() { this->api_client_->send_voice_assistant_request(msg); } +void VoiceAssistant::start_playback_timeout_() { + this->set_timeout("playing", 100, [this]() { + this->cancel_timeout("speaker-timeout"); + this->set_state_(State::IDLE, State::IDLE); + + api::VoiceAssistantAnnounceFinished msg; + msg.success = true; + this->api_client_->send_voice_assistant_announce_finished(msg); + }); +} + void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Event Type: %" PRId32, msg.event_type); switch (msg.event_type) { @@ -715,6 +722,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); + // Start the playback timeout, as the media player state isn't immediately updated + this->start_playback_timeout_(); } #endif this->tts_end_trigger_->trigger(url); @@ -725,7 +734,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_RUN_END: { ESP_LOGD(TAG, "Assist Pipeline ended"); - if (this->state_ == State::STREAMING_MICROPHONE) { + if ((this->state_ == State::STARTING_PIPELINE) || (this->state_ == State::AWAITING_RESPONSE)) { + // Pipeline ended before starting microphone + // Or there wasn't a TTS start event ("nevermind") + this->set_state_(State::IDLE, State::IDLE); + } else if (this->state_ == State::STREAMING_MICROPHONE) { this->ring_buffer_->reset(); #ifdef USE_ESP_ADF if (this->use_wake_word_) { @@ -736,9 +749,6 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { { this->set_state_(State::IDLE, State::IDLE); } - } else if (this->state_ == State::AWAITING_RESPONSE) { - // No TTS start event ("nevermind") - this->set_state_(State::IDLE, State::IDLE); } this->defer([this]() { this->end_trigger_->trigger(); }); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 0016d3157c..12124c1486 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -40,6 +40,7 @@ enum VoiceAssistantFeature : uint32_t { FEATURE_SPEAKER = 1 << 1, FEATURE_API_AUDIO = 1 << 2, FEATURE_TIMERS = 1 << 3, + FEATURE_ANNOUNCE = 1 << 4, }; enum class State { @@ -136,6 +137,12 @@ class VoiceAssistant : public Component { flags |= VoiceAssistantFeature::FEATURE_TIMERS; } +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + flags |= VoiceAssistantFeature::FEATURE_ANNOUNCE; + } +#endif + return flags; } @@ -209,6 +216,7 @@ class VoiceAssistant : public Component { void set_state_(State state); void set_state_(State state, State desired_state); void signal_stop_(); + void start_playback_timeout_(); std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; From 6b3f3e1da6ed7725eaa5bacc65b069190f2787e3 Mon Sep 17 00:00:00 2001 From: Jordan Zucker Date: Tue, 11 Feb 2025 11:51:55 -0800 Subject: [PATCH 08/31] [prometheus] Adding valve entity metrics (#8223) --- .../prometheus/prometheus_handler.cpp | 54 +++++++++++++++++++ .../prometheus/prometheus_handler.h | 8 +++ tests/components/prometheus/common.yaml | 8 +++ 3 files changed, 70 insertions(+) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index c31d34f000..794df299a1 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -83,6 +83,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->update_entity_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_VALVE + this->valve_type_(stream); + for (auto *obj : App.get_valves()) + this->valve_row_(stream, obj, area, node, friendly_name); +#endif + req->send(stream); } @@ -770,6 +776,54 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update:: } #endif +#ifdef USE_VALVE +void PrometheusHandler::valve_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_valve_operation gauge\n")); + stream->print(F("#TYPE esphome_valve_failed gauge\n")); + stream->print(F("#TYPE esphome_valve_position gauge\n")); +} + +void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + stream->print(F("esphome_valve_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_valve_operation{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",operation=\"")); + stream->print(valve::valve_operation_to_str(obj->current_operation)); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); + // Now see if position is supported + if (obj->get_traits().get_supports_position()) { + stream->print(F("esphome_valve_position{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} ")); + stream->print(obj->position); + stream->print(F("\n")); + } +} +#endif + } // namespace prometheus } // namespace esphome #endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 08a6e8dc8a..b77dbc462b 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -161,6 +161,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void handle_update_state_(AsyncResponseStream *stream, update::UpdateState state); #endif +#ifdef USE_VALVE + /// Return the type for prometheus + void valve_type_(AsyncResponseStream *stream); + /// Return the valve state as prometheus data point + void valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + web_server_base::WebServerBase *base_; bool include_internal_{false}; std::map relabel_map_id_; diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 9205c27f2a..7c226b6782 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -121,6 +121,14 @@ number: max_value: 100 step: 1 +valve: + - platform: template + name: "Template Valve" + lambda: |- + return VALVE_OPEN; + optimistic: true + has_position: true + prometheus: include_internal: true relabel: From 14d7931bd6b12d4b91846af1a83196a9b06651e2 Mon Sep 17 00:00:00 2001 From: tmpeh <41875356+tmpeh@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:41:52 +0100 Subject: [PATCH 09/31] Added Waveshare e-paper display model "7.50inv2p" to the waveshare_epaper component. (#7751) Co-authored-by: Tim Pehla Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_epaper.cpp | 203 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 37 ++++ tests/components/waveshare_epaper/common.yaml | 20 ++ 4 files changed, 264 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index f0064c6a4d..2d8619bc5c 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -91,6 +91,9 @@ WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( WaveshareEPaper7P5InV2alt = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2alt", WaveshareEPaper ) +WaveshareEPaper7P5InV2P = waveshare_epaper_ns.class_( + "WaveshareEPaper7P5InV2P", WaveshareEPaper +) WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InHDB", WaveshareEPaper ) @@ -144,6 +147,7 @@ MODELS = { "7.50in-bc": ("b", WaveshareEPaper7P5InBC), "7.50inv2": ("b", WaveshareEPaper7P5InV2), "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), + "7.50inv2p": ("c", WaveshareEPaper7P5InV2P), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), "2.13inv3": ("c", WaveshareEPaper2P13InV3), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index f240c98096..9f8c32721d 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -3100,6 +3100,209 @@ void WaveshareEPaper7P5InV2alt::dump_config() { LOG_UPDATE_INTERVAL(this); } +/* 7.50inV2 with partial and fast refresh */ +bool WaveshareEPaper7P5InV2P::wait_until_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + + const uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + this->command(0x71); + if (millis() - start > this->idle_timeout_()) { + ESP_LOGE(TAG, "Timeout while displaying image!"); + return false; + } + App.feed_wdt(); + delay(10); + } + return true; +} + +void WaveshareEPaper7P5InV2P::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + delay(20); + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + delay(20); + } +} + +void WaveshareEPaper7P5InV2P::turn_on_display_() { + this->command(0x12); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +void WaveshareEPaper7P5InV2P::initialize() { + this->reset_(); + + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + + // COMMAND BOOSTER SOFT START + this->command(0x06); + this->data(0x17); + this->data(0x17); + this->data(0x28); + this->data(0x17); + + // COMMAND POWER DRIVER HAT UP + this->command(0x04); + delay(100); // NOLINT + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); + this->data(0x20); + this->data(0x01); + this->data(0xE0); + + // COMMAND DUAL SPI MM_EN, DUSPI_EN + this->command(0x15); + this->data(0x00); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + + // COMMAND ENABLE FAST UPDATE + this->command(0xE0); + this->data(0x02); + this->command(0xE5); + this->data(0x5A); + + // COMMAND POWER DRIVER HAT DOWN + this->command(0x02); +} + +void HOT WaveshareEPaper7P5InV2P::display() { + uint32_t buf_len = this->get_buffer_length_(); + + // COMMAND POWER ON + ESP_LOGI(TAG, "Power on the display and hat"); + + this->command(0x04); + delay(200); // NOLINT + this->wait_until_idle_(); + + if (this->full_update_every_ == 1) { + this->command(0x13); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~(this->buffer_[i])); + } + + this->turn_on_display_(); + + this->command(0x02); + this->wait_until_idle_(); + return; + } + + this->command(0x50); + this->data(0xA9); + this->data(0x07); + + if (this->at_update_ == 0) { + // Enable fast refresh + this->command(0xE5); + this->data(0x5A); + + this->command(0x92); + + this->command(0x10); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~(this->buffer_[i])); + } + + delay(100); // NOLINT + this->wait_until_idle_(); + + this->command(0x13); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } + + delay(100); // NOLINT + this->wait_until_idle_(); + + this->turn_on_display_(); + + } else { + // Enable partial refresh + this->command(0xE5); + this->data(0x6E); + + // Activate partial refresh and set window bounds + this->command(0x91); + this->command(0x90); + + this->data(0x00); + this->data(0x00); + this->data((get_width_internal() - 1) >> 8 & 0xFF); + this->data((get_width_internal() - 1) & 0xFF); + + this->data(0x00); + this->data(0x00); + this->data((get_height_internal() - 1) >> 8 & 0xFF); + this->data((get_height_internal() - 1) & 0xFF); + + this->data(0x01); + + this->command(0x13); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } + + delay(100); // NOLINT + this->wait_until_idle_(); + + this->turn_on_display_(); + } + + ESP_LOGV(TAG, "Before command(0x02) (>> power off)"); + this->command(0x02); + this->wait_until_idle_(); + ESP_LOGV(TAG, "After command(0x02) (>> power off)"); + + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; +} + +int WaveshareEPaper7P5InV2P::get_width_internal() { return 800; } +int WaveshareEPaper7P5InV2P::get_height_internal() { return 480; } +uint32_t WaveshareEPaper7P5InV2P::idle_timeout_() { return 10000; } +void WaveshareEPaper7P5InV2P::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.50inv2p"); + ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} +void WaveshareEPaper7P5InV2P::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + /* 7.50in-bc */ void WaveshareEPaper7P5InBC::initialize() { /* The command sequence is similar to the 7P5In display but differs in subtle ways diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index c2277f2962..73090fa009 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -775,6 +775,43 @@ class WaveshareEPaper7P5InV2alt : public WaveshareEPaper7P5InV2 { }; }; +class WaveshareEPaper7P5InV2P : public WaveshareEPaper { + public: + bool wait_until_idle_(); + + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + void set_full_update_every(uint32_t full_update_every); + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + uint32_t idle_timeout_() override; + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + + private: + void reset_(); + + void turn_on_display_(); +}; + class WaveshareEPaper7P5InHDB : public WaveshareEPaper { public: void initialize() override; diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index 4a67ccd67d..667bbb1bb0 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -675,6 +675,26 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + id: epd_7_50inv2p + model: 7.50inv2p + spi_id: spi_waveshare_epaper + cs_pin: + allow_other_uses: true + number: ${cs_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + busy_pin: + allow_other_uses: true + number: ${busy_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper id: epd_7_50hdb model: 7.50in-hd-b From 0401ee94282c3b8e436c3983e23233171fad0c77 Mon Sep 17 00:00:00 2001 From: Rachasak Ragkamnerd Date: Wed, 12 Feb 2025 04:35:07 +0700 Subject: [PATCH 10/31] added Waveshare BWR Mode for the 4.2in Display (#7995) Co-authored-by: rrachasak Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 ++ .../waveshare_epaper/waveshare_epaper.cpp | 58 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 28 +++++++++ tests/components/waveshare_epaper/common.yaml | 19 ++++++ 4 files changed, 109 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 2d8619bc5c..f6e56c543d 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -64,6 +64,9 @@ WaveshareEPaper4P2In = waveshare_epaper_ns.class_( WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( "WaveshareEPaper4P2InBV2", WaveshareEPaper ) +WaveshareEPaper4P2InBV2BWR = waveshare_epaper_ns.class_( + "WaveshareEPaper4P2InBV2BWR", WaveshareEPaperBWR +) WaveshareEPaper5P8In = waveshare_epaper_ns.class_( "WaveshareEPaper5P8In", WaveshareEPaper ) @@ -138,6 +141,7 @@ MODELS = { "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), + "4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR), "5.83in": ("b", WaveshareEPaper5P8In), "5.83inv2": ("b", WaveshareEPaper5P8InV2), "7.50in": ("b", WaveshareEPaper7P5In), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 9f8c32721d..a4439e2d7d 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2274,6 +2274,64 @@ void WaveshareEPaper4P2InBV2::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 4.20in Type B With Red colour support (LUT from OTP) +// Datasheet: +// - https://www.waveshare.com/w/upload/2/20/4.2inch-e-paper-module-user-manual-en.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_4in2b_V2.c +// The implementation is an adaptation of WaveshareEPaper4P2InBV2 class +// ======================================================== +void WaveshareEPaper4P2InBV2BWR::initialize() { + // these exact timings are required for a proper reset/init + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x0f); // LUT from OTP +} + +void HOT WaveshareEPaper4P2InBV2BWR::display() { + const uint32_t buf_len = this->get_buffer_length_() / 2u; + + this->command(0x10); // Send BW data Transmission + delay(2); // Delay to prevent Watchdog error + for (uint32_t i = 0; i < buf_len; ++i) { + this->data(this->buffer_[i]); + } + + this->command(0x13); // Send red data Transmission + delay(2); // Delay to prevent Watchdog error + for (uint32_t i = 0; i < buf_len; ++i) { + // Red color need to flip bit from the buffer. Otherwise, red will conqure the screen! + this->data(~this->buffer_[buf_len + i]); + } + + // COMMAND DISPLAY REFRESH + this->command(0x12); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper4P2InBV2BWR::get_width_internal() { return 400; } +int WaveshareEPaper4P2InBV2BWR::get_height_internal() { return 300; } +void WaveshareEPaper4P2InBV2BWR::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 4.2in (B V2) BWR-Mode"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + void WaveshareEPaper5P8In::initialize() { // COMMAND POWER SETTING this->command(0x01); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 73090fa009..4d811228f1 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -518,6 +518,34 @@ class WaveshareEPaper4P2InBV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper4P2InBV2BWR : public WaveshareEPaperBWR { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0xF7); // border floating + + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check code + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper5P8In : public WaveshareEPaper { public: void initialize() override; diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index 667bbb1bb0..5889b4659e 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -502,6 +502,25 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + id: epd_4_20in_bv2_bwr + model: 4.20in-bv2-bwr + spi_id: spi_waveshare_epaper + cs_pin: + allow_other_uses: true + number: ${cs_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + busy_pin: + allow_other_uses: true + number: ${busy_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + # 5.83 inch displays - platform: waveshare_epaper id: epd_5_83 From a2f1b902388f6d8c89b2f332199273e479beed65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Koek?= Date: Tue, 11 Feb 2025 23:16:33 +0000 Subject: [PATCH 11/31] Add GDEY029T94 support (#7931) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 2 + .../waveshare_epaper/waveshare_epaper.cpp | 75 ++++++++++++++++++- .../waveshare_epaper/waveshare_epaper.h | 19 +++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index f6e56c543d..7b51eb338c 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -52,6 +52,7 @@ WaveshareEPaper2P9InV2R2 = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InV2R2", WaveshareEPaper ) GDEW029T5 = waveshare_epaper_ns.class_("GDEW029T5", WaveshareEPaper) +GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InDKE", WaveshareEPaper ) @@ -136,6 +137,7 @@ MODELS = { "2.70inv2": ("b", WaveshareEPaper2P7InV2), "2.90in-b": ("b", WaveshareEPaper2P9InB), "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), + "gdey029t94": ("c", GDEY029T94), "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), "2.90in-d": ("b", WaveshareEPaper2P9InD), "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index a4439e2d7d..1e8671bfa6 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1677,9 +1677,82 @@ int WaveshareEPaper2P9InV2R2::get_width_controller() { return this->get_width_in void WaveshareEPaper2P9InV2R2::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } +// ======================================================== +// Good Display 2.9in black/white +// Datasheet: +// - https://files.seeedstudio.com/wiki/Other_Display/29-epaper/GDEY029T94.pdf +// - +// https://github.com/Allen-Kuang/e-ink_Demo/blob/main/2.9%20inch%20E-paper%20-%20monocolor%20128x296/example/Display_EPD_W21.cpp +// ======================================================== + +void GDEY029T94::initialize() { + // EPD hardware init start + this->reset_(); + + this->wait_until_idle_(); + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + this->command(0x01); // Driver output control + this->data((this->get_height_internal() - 1) % 256); + this->data((this->get_height_internal() - 1) / 256); + this->data(0x00); + + this->command(0x11); // data entry mode + this->data(0x03); + + this->command(0x44); // set Ram-X address start/end position + this->data(0x00); + this->data(this->get_width_internal() / 8 - 1); + + this->command(0x45); // set Ram-Y address start/end position + this->data(0x00); + this->data(0x00); + this->data((this->get_height_internal() - 1) % 256); + this->data((this->get_height_internal() - 1) / 256); + + this->command(0x3C); // BorderWavefrom + this->data(0x05); + + this->command(0x21); // Display update control + this->data(0x00); + this->data(0x80); + + this->command(0x18); // Read built-in temperature sensor + this->data(0x80); + + this->command(0x4E); // set RAM x address count to 0; + this->data(0x00); + this->command(0x4F); // set RAM y address count to 0X199; + this->command(0x00); + this->command(0x00); + this->wait_until_idle_(); +} +void HOT GDEY029T94::display() { + this->command(0x24); // write RAM for black(0)/white (1) + this->start_data_(); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { + this->write_byte(this->buffer_[i]); + } + this->end_data_(); + this->command(0x22); // Display Update Control + this->data(0xF7); + this->command(0x20); // Activate Display Update Sequence + this->wait_until_idle_(); +} +int GDEY029T94::get_width_internal() { return 128; } +int GDEY029T94::get_height_internal() { return 296; } +void GDEY029T94::dump_config() { + LOG_DISPLAY("", "E-Paper (Good Display)", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in GDEY029T94"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} // ======================================================== -// Good Display 2.9in black/white/grey +// Good Display 2.9in black/white // Datasheet: // - https://v4.cecdn.yun300.cn/100001_1909185148/SSD1680.pdf // - https://github.com/adafruit/Adafruit_EPD/blob/master/src/panels/ThinkInk_290_Grayscale4_T5.h diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 4d811228f1..54e7619ebc 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -277,6 +277,25 @@ class GDEW029T5 : public WaveshareEPaper { uint8_t *old_buffer_{nullptr}; }; +class GDEY029T94 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + this->command(0x10); // Enter deep sleep + this->data(0x01); + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper2P7InV2 : public WaveshareEPaper { public: void initialize() override; From 88cfdc33d458475c4dc3fa67653f7abf355dd7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=BCner?= Date: Wed, 12 Feb 2025 00:17:34 +0100 Subject: [PATCH 12/31] GDEY042T81 e-paper displays support (#8061) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 2 + .../waveshare_epaper/waveshare_epaper.cpp | 200 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 37 ++++ tests/components/waveshare_epaper/common.yaml | 20 ++ 4 files changed, 259 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 7b51eb338c..72823e4e36 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -56,6 +56,7 @@ GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InDKE", WaveshareEPaper ) +GDEY042T81 = waveshare_epaper_ns.class_("GDEY042T81", WaveshareEPaper) WaveshareEPaper2P9InD = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InD", WaveshareEPaper ) @@ -141,6 +142,7 @@ MODELS = { "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), "2.90in-d": ("b", WaveshareEPaper2P9InD), "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), + "gdey042t81": ("c", GDEY042T81), "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 1e8671bfa6..2a540a1b75 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2169,6 +2169,206 @@ void GDEW0154M09::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// Good Display 4.2in black/white GDEY042T81 (SSD1683) +// Product page: +// - https://www.good-display.com/product/386.html +// Datasheet: +// - https://v4.cecdn.yun300.cn/100001_1909185148/GDEY042T81.pdf +// - https://v4.cecdn.yun300.cn/100001_1909185148/SSD1683.PDF +// Reference code from GoodDisplay: +// - https://www.good-display.com/companyfile/1572.html (2024-08-01 15:40:41) +// Other reference code: +// - https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp +// ======================================================== + +void GDEY042T81::initialize() { + this->init_display_(); + ESP_LOGD(TAG, "Initialization complete, set the display to deep sleep"); + this->deep_sleep(); +} + +// conflicting documentation / examples regarding reset timings +// https://v4.cecdn.yun300.cn/100001_1909185148/SSD1683.PDF -> 10ms +// GD sample code (Display_EPD_W21.cpp, see above) -> 10 ms +// https://v4.cecdn.yun300.cn/100001_1909185148/GDEY042T81.pdf (section 14.2) -> 0.2ms (200us) +// https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L351 +// -> 10ms +// 10 ms seems to work, so we use this +GDEY042T81::GDEY042T81() { this->reset_duration_ = 10; } + +void GDEY042T81::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(reset_duration_); // NOLINT + this->reset_pin_->digital_write(true); + delay(reset_duration_); // NOLINT + } +} + +void GDEY042T81::init_display_() { + this->reset_(); + + this->wait_until_idle_(); + this->command(0x12); // SWRESET + this->wait_until_idle_(); + + // Specify number of lines for the driver: 300 (MUX 300) + // https://v4.cecdn.yun300.cn/100001_1909185148/SSD1683.PDF (section 8.1) + // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L354 + this->command(0x01); // driver output control + this->data(0x2B); // (height - 1) % 256 + this->data(0x01); // (height - 1) / 256 + this->data(0x00); + + // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L360 + this->command(0x3C); // BorderWaveform + this->data(0x01); + this->command(0x18); // Read built-in temperature sensor + this->data(0x80); + + // GD sample code (Display_EPD_W21.cpp@90ff) + this->command(0x11); // data entry mode + this->data(0x03); + // set windows (0,0,400,300) + this->command(0x44); // set Ram-X address start/end position + this->data(0); + this->data(0x31); // (width / 8 -1) + + this->command(0x45); // set Ram-y address start/end position + this->data(0); + this->data(0); + this->data(0x2B); // (height - 1) % 256 + this->data(0x01); // (height - 1) / 256 + + // set cursor (0,0) + this->command(0x4E); // set RAM x address count to 0; + this->data(0); + this->command(0x4F); // set RAM y address count to 0; + this->data(0); + this->data(0); + + this->wait_until_idle_(); +} + +// https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L366 +void GDEY042T81::update_full_() { + this->command(0x21); // display update control + this->data(0x40); // bypass RED as 0 + this->data(0x00); // single chip application + + // only ever do a fast update because slow updates are only relevant + // for lower operating temperatures + // see + // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_290_GDEY029T94.h#L30 + // + // Should slow/fast updates be made configurable similar to how GxEPD2 does it? No idea if anyone would need it... + this->command(0x1A); // Write to temperature register + this->data(0x6E); + this->command(0x22); + this->data(0xd7); + + this->command(0x20); + this->wait_until_idle_(); +} + +// https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L389 +void GDEY042T81::update_part_() { + this->command(0x21); // display update control + this->data(0x00); // RED normal + this->data(0x00); // single chip application + + this->command(0x22); + this->data(0xfc); + + this->command(0x20); + this->wait_until_idle_(); +} + +void HOT GDEY042T81::display() { + ESP_LOGD(TAG, "Wake up the display"); + this->init_display_(); + + if (!this->wait_until_idle_()) { + this->status_set_warning(); + ESP_LOGE(TAG, "Failed to perform update, display is busy"); + return; + } + + // basic code structure copied from WaveshareEPaper2P9InV2R2 + if (this->full_update_every_ == 1) { + ESP_LOGD(TAG, "Full update"); + // do single full update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay + this->update_full_(); + return; + } + + // if (this->full_update_every_ == 1 || + if (this->at_update_ == 0) { + ESP_LOGD(TAG, "Update"); + // do base update + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + this->command(0x26); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay; + this->update_full_(); + } else { + // do partial update (full screen) + // no need to load a LUT for GoodDisplays as they seem to have the LUT onboard + // GD example code (Display_EPD_W21.cpp@283ff) + // + // not setting the BorderWaveform here again (contrary to the GD example) because according to + // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L358 + // it seems to be enough to set it during display initialization + ESP_LOGD(TAG, "Partial update"); + this->reset_(); + if (!this->wait_until_idle_()) { + this->status_set_warning(); + ESP_LOGE(TAG, "Failed to perform partial update, display is busy"); + return; + } + + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // TurnOnDisplay + this->update_part_(); + } + + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + this->wait_until_idle_(); + ESP_LOGD(TAG, "Set the display back to deep sleep"); + this->deep_sleep(); +} +void GDEY042T81::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } +int GDEY042T81::get_width_internal() { return 400; } +int GDEY042T81::get_height_internal() { return 300; } +uint32_t GDEY042T81::idle_timeout_() { return 5000; } +void GDEY042T81::dump_config() { + LOG_DISPLAY("", "GoodDisplay E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 4.2in B/W GDEY042T81"); + ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + static const uint8_t LUT_VCOM_DC_4_2[] = { 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, 0x00, 0x0A, 0x01, 0x00, 0x00, 0x01, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 54e7619ebc..1e7cb6c6c7 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -466,6 +466,43 @@ class WaveshareEPaper2P9InD : public WaveshareEPaper { int get_height_internal() override; }; +class GDEY042T81 : public WaveshareEPaper { + public: + GDEY042T81(); + + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x22); + this->data(0x83); + this->command(0x20); + // COMMAND DEEP SLEEP + this->command(0x10); + this->data(0x01); + } + + void set_full_update_every(uint32_t full_update_every); + + protected: + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + + int get_width_internal() override; + int get_height_internal() override; + uint32_t idle_timeout_() override; + + private: + void reset_(); + void update_full_(); + void update_part_(); + void init_display_(); +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index 5889b4659e..09ba1af778 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -463,6 +463,26 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + id: epd_gdew042t81 + model: gdey042t81 + spi_id: spi_waveshare_epaper + cs_pin: + allow_other_uses: true + number: ${cs_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + busy_pin: + allow_other_uses: true + number: ${busy_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + # 4.2 inch displays - platform: waveshare_epaper id: epd_4_20 From 7bb2c3c4968185d6cc4848986b0f3cdc71e7da5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Neil=20S=C3=A9gard?= <55683504+NeilSCGH@users.noreply.github.com> Date: Wed, 12 Feb 2025 00:31:56 +0100 Subject: [PATCH 13/31] Add support for Waveshare 7.3" ACeP 7-Color display (#6380) --- .../components/waveshare_epaper/display.py | 5 + .../waveshare_epaper/waveshare_213v3.cpp | 6 +- .../waveshare_epaper/waveshare_epaper.cpp | 311 +++++++++++++++++- .../waveshare_epaper/waveshare_epaper.h | 56 +++- 4 files changed, 369 insertions(+), 9 deletions(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 72823e4e36..8acb6ac68f 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -24,6 +24,7 @@ WaveshareEPaper = waveshare_epaper_ns.class_("WaveshareEPaper", WaveshareEPaperB WaveshareEPaperBWR = waveshare_epaper_ns.class_( "WaveshareEPaperBWR", WaveshareEPaperBase ) +WaveshareEPaper7C = waveshare_epaper_ns.class_("WaveshareEPaper7C", WaveshareEPaperBase) WaveshareEPaperTypeA = waveshare_epaper_ns.class_( "WaveshareEPaperTypeA", WaveshareEPaper ) @@ -75,6 +76,9 @@ WaveshareEPaper5P8In = waveshare_epaper_ns.class_( WaveshareEPaper5P8InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper5P8InV2", WaveshareEPaper ) +WaveshareEPaper7P3InF = waveshare_epaper_ns.class_( + "WaveshareEPaper7P3InF", WaveshareEPaper7C +) WaveshareEPaper7P5In = waveshare_epaper_ns.class_( "WaveshareEPaper7P5In", WaveshareEPaper ) @@ -148,6 +152,7 @@ MODELS = { "4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR), "5.83in": ("b", WaveshareEPaper5P8In), "5.83inv2": ("b", WaveshareEPaper5P8InV2), + "7.30in-f": ("b", WaveshareEPaper7P3InF), "7.50in": ("b", WaveshareEPaper7P5In), "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), "7.50in-bv3": ("b", WaveshareEPaper7P5InBV3), diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 85d7033d4b..316cd80ccd 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -87,7 +87,11 @@ void WaveshareEPaper2P13InV3::send_reset_() { } void WaveshareEPaper2P13InV3::setup() { - setup_pins_(); + this->init_internal_(this->get_buffer_length_()); + this->setup_pins_(); + this->spi_setup(); + this->reset_(); + delay(20); this->send_reset_(); // as a one-off delay this is not worth working around. diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 2a540a1b75..96fc82fcdd 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -3,6 +3,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include +#include namespace esphome { namespace waveshare_epaper { @@ -110,8 +111,14 @@ static const uint8_t PARTIAL_UPD_2IN9_LUT[PARTIAL_UPD_2IN9_LUT_SIZE] = }; // clang-format on -void WaveshareEPaperBase::setup_pins_() { +void WaveshareEPaperBase::setup() { this->init_internal_(this->get_buffer_length_()); + this->setup_pins_(); + this->spi_setup(); + this->reset_(); + this->initialize(); +} +void WaveshareEPaperBase::setup_pins_() { this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); if (this->reset_pin_ != nullptr) { @@ -121,9 +128,6 @@ void WaveshareEPaperBase::setup_pins_() { if (this->busy_pin_ != nullptr) { this->busy_pin_->setup(); // INPUT } - this->spi_setup(); - - this->reset_(); } float WaveshareEPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } void WaveshareEPaperBase::command(uint8_t value) { @@ -173,6 +177,87 @@ void WaveshareEPaper::fill(Color color) { for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } +void WaveshareEPaper7C::setup() { + this->init_internal_7c_(this->get_buffer_length_()); + this->setup_pins_(); + this->spi_setup(); + this->reset_(); + this->initialize(); +} +void WaveshareEPaper7C::init_internal_7c_(uint32_t buffer_length) { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + uint32_t small_buffer_length = buffer_length / NUM_BUFFERS; + + for (int i = 0; i < NUM_BUFFERS; i++) { + this->buffers_[i] = allocator.allocate(small_buffer_length); + if (this->buffers_[i] == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer %d for display!", i); + for (auto &buffer : this->buffers_) { + allocator.deallocate(buffer, small_buffer_length); + buffer = nullptr; + } + return; + } + } + this->clear(); +} +uint8_t WaveshareEPaper7C::color_to_hex(Color color) { + uint8_t hex_code; + if (color.red > 127) { + if (color.green > 170) { + if (color.blue > 127) { + hex_code = 0x1; // White + } else { + hex_code = 0x5; // Yellow + } + } else if (color.green > 85) { + hex_code = 0x6; // Orange + } else { + hex_code = 0x4; // Red (or Magenta) + } + } else { + if (color.green > 127) { + if (color.blue > 127) { + hex_code = 0x3; // Cyan -> Blue + } else { + hex_code = 0x2; // Green + } + } else { + if (color.blue > 127) { + hex_code = 0x3; // Blue + } else { + hex_code = 0x0; // Black + } + } + } + + return hex_code; +} +void WaveshareEPaper7C::fill(Color color) { + uint8_t pixel_color; + if (color.is_on()) { + pixel_color = this->color_to_hex(color); + } else { + pixel_color = 0x1; + } + + if (this->buffers_[0] == nullptr) { + ESP_LOGE(TAG, "Buffer unavailable!"); + } else { + uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS; + for (auto &buffer : this->buffers_) { + for (uint32_t buffer_pos = 0; buffer_pos < small_buffer_length; buffer_pos += 3) { + // We store 8 bitset<3> in 3 bytes + // | byte 1 | byte 2 | byte 3 | + // |aaabbbaa|abbbaaab|bbaaabbb| + buffer[buffer_pos + 0] = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1; + buffer[buffer_pos + 1] = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2; + buffer[buffer_pos + 2] = pixel_color << 6 | pixel_color << 3 | pixel_color << 0; + } + App.feed_wdt(); + } + } +} void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; @@ -193,6 +278,9 @@ uint32_t WaveshareEPaper::get_buffer_length_() { uint32_t WaveshareEPaperBWR::get_buffer_length_() { return this->get_width_controller() * this->get_height_internal() / 4u; } // black and red buffer +uint32_t WaveshareEPaper7C::get_buffer_length_() { + return this->get_width_controller() * this->get_height_internal() / 8u * 3u; +} // 7 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes void WaveshareEPaperBWR::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); @@ -219,7 +307,33 @@ void HOT WaveshareEPaperBWR::draw_absolute_pixel_internal(int x, int y, Color co this->buffer_[pos + buf_half_len] &= ~(0x80 >> subpos); } } +void HOT WaveshareEPaper7C::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + return; + uint8_t pixel_bits = this->color_to_hex(color); + uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS; + uint32_t pixel_position = x + y * this->get_width_controller(); + uint32_t first_bit_position = pixel_position * 3; + uint32_t byte_position = first_bit_position / 8u; + uint32_t byte_subposition = first_bit_position % 8u; + uint32_t buffer_position = byte_position / small_buffer_length; + uint32_t buffer_subposition = byte_position % small_buffer_length; + + if (byte_subposition <= 5) { + this->buffers_[buffer_position][buffer_subposition] = + (this->buffers_[buffer_position][buffer_subposition] & (0xFF ^ (0b111 << (5 - byte_subposition)))) | + (pixel_bits << (5 - byte_subposition)); + } else { + this->buffers_[buffer_position][buffer_subposition + 0] = + (this->buffers_[buffer_position][buffer_subposition + 0] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | + (pixel_bits >> (byte_subposition - 5)); + + this->buffers_[buffer_position][buffer_subposition + 1] = (this->buffers_[buffer_position][buffer_subposition + 1] & + (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) | + (pixel_bits << (13 - byte_subposition)); + } +} void WaveshareEPaperBase::start_command_() { this->dc_pin_->digital_write(false); this->enable(); @@ -3193,6 +3307,195 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P3InF::initialize() { + if (this->buffers_[0] == nullptr) { + ESP_LOGE(TAG, "Buffer unavailable!"); + return; + } + + this->reset_(); + delay(20); + this->wait_until_idle_(); + + // COMMAND CMDH + this->command(0xAA); + this->data(0x49); + this->data(0x55); + this->data(0x20); + this->data(0x08); + this->data(0x09); + this->data(0x18); + + this->command(0x01); + this->data(0x3F); + this->data(0x00); + this->data(0x32); + this->data(0x2A); + this->data(0x0E); + this->data(0x2A); + + this->command(0x00); + this->data(0x5F); + this->data(0x69); + + this->command(0x03); + this->data(0x00); + this->data(0x54); + this->data(0x00); + this->data(0x44); + + this->command(0x05); + this->data(0x40); + this->data(0x1F); + this->data(0x1F); + this->data(0x2C); + + this->command(0x06); + this->data(0x6F); + this->data(0x1F); + this->data(0x1F); + this->data(0x22); + + this->command(0x08); + this->data(0x6F); + this->data(0x1F); + this->data(0x1F); + this->data(0x22); + + // COMMAND IPC + this->command(0x13); + this->data(0x00); + this->data(0x04); + + this->command(0x30); + this->data(0x3C); + + // COMMAND TSE + this->command(0x41); + this->data(0x00); + + this->command(0x50); + this->data(0x3F); + + this->command(0x60); + this->data(0x02); + this->data(0x00); + + this->command(0x61); + this->data(0x03); + this->data(0x20); + this->data(0x01); + this->data(0xE0); + + this->command(0x82); + this->data(0x1E); + + this->command(0x84); + this->data(0x00); + + // COMMAND AGID + this->command(0x86); + this->data(0x00); + + this->command(0xE3); + this->data(0x2F); + + // COMMAND CCSET + this->command(0xE0); + this->data(0x00); + + // COMMAND TSSET + this->command(0xE6); + this->data(0x00); + + ESP_LOGI(TAG, "Display initialized successfully"); +} +void HOT WaveshareEPaper7P3InF::display() { + if (this->buffers_[0] == nullptr) { + ESP_LOGE(TAG, "Buffer unavailable!"); + return; + } + + // INITIALIZATION + ESP_LOGI(TAG, "Initialise the display"); + this->initialize(); + + // COMMAND DATA START TRANSMISSION + ESP_LOGI(TAG, "Sending data to the display"); + this->command(0x10); + uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS; + uint8_t byte_to_send; + for (auto &buffer : this->buffers_) { + for (uint32_t buffer_pos = 0; buffer_pos < small_buffer_length; buffer_pos += 3) { + std::bitset<24> triplet = + buffer[buffer_pos + 0] << 16 | buffer[buffer_pos + 1] << 8 | buffer[buffer_pos + 2] << 0; + // 8 bitset<3> are stored in 3 bytes + // |aaabbbaa|abbbaaab|bbaaabbb| + // | byte 1 | byte 2 | byte 3 | + byte_to_send = ((triplet >> 17).to_ulong() & 0b01110000) | ((triplet >> 18).to_ulong() & 0b00000111); + this->data(byte_to_send); + + byte_to_send = ((triplet >> 11).to_ulong() & 0b01110000) | ((triplet >> 12).to_ulong() & 0b00000111); + this->data(byte_to_send); + + byte_to_send = ((triplet >> 5).to_ulong() & 0b01110000) | ((triplet >> 6).to_ulong() & 0b00000111); + this->data(byte_to_send); + + byte_to_send = ((triplet << 1).to_ulong() & 0b01110000) | ((triplet << 0).to_ulong() & 0b00000111); + this->data(byte_to_send); + } + App.feed_wdt(); + } + + // COMMAND POWER ON + ESP_LOGI(TAG, "Power on the display"); + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND REFRESH SCREEN + ESP_LOGI(TAG, "Refresh the display"); + this->command(0x12); + this->data(0x00); + this->wait_until_idle_(); + + // COMMAND POWER OFF + ESP_LOGI(TAG, "Power off the display"); + this->command(0x02); + this->data(0x00); + this->wait_until_idle_(); + + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->command(0x07); + this->data(0xA5); +} +int WaveshareEPaper7P3InF::get_width_internal() { return 800; } +int WaveshareEPaper7P3InF::get_height_internal() { return 480; } +uint32_t WaveshareEPaper7P3InF::idle_timeout_() { return 35000; } +void WaveshareEPaper7P3InF::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.3in-F"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +bool WaveshareEPaper7P3InF::wait_until_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + const uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + if (millis() - start > this->idle_timeout_()) { + ESP_LOGE(TAG, "Timeout while displaying image!"); + return false; + } + App.feed_wdt(); + delay(10); + } + delay(200); // NOLINT + return true; +} bool WaveshareEPaper7P5InV2::wait_until_idle_() { if (this->busy_pin_ == nullptr) { return true; diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 1e7cb6c6c7..d6387cd643 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -27,10 +27,7 @@ class WaveshareEPaperBase : public display::DisplayBuffer, void update() override; - void setup() override { - this->setup_pins_(); - this->initialize(); - } + void setup() override; void on_safe_shutdown() override; @@ -86,6 +83,23 @@ class WaveshareEPaperBWR : public WaveshareEPaperBase { uint32_t get_buffer_length_() override; }; +class WaveshareEPaper7C : public WaveshareEPaperBase { + public: + uint8_t color_to_hex(Color color); + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length_() override; + void setup() override; + void init_internal_7c_(uint32_t buffer_length); + + static const int NUM_BUFFERS = 10; + uint8_t *buffers_[NUM_BUFFERS]; +}; + enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_1_54_IN = 0, WAVESHARE_EPAPER_1_54_IN_V2, @@ -160,6 +174,7 @@ enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN_B_V2, WAVESHARE_EPAPER_4_2_IN, WAVESHARE_EPAPER_4_2_IN_B_V2, + WAVESHARE_EPAPER_7_3_IN_F, WAVESHARE_EPAPER_7_5_IN, WAVESHARE_EPAPER_7_5_INV2, WAVESHARE_EPAPER_7_5_IN_B_V2, @@ -668,6 +683,39 @@ class WaveshareEPaper5P8InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P3InF : public WaveshareEPaper7C { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + uint32_t idle_timeout_() override; + + void deep_sleep() override { ; } + + bool wait_until_idle_(); + + bool deep_sleep_between_updates_{true}; + + void reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + delay(20); + this->reset_pin_->digital_write(false); + delay(1); + this->reset_pin_->digital_write(true); + delay(20); + } + }; +}; + class WaveshareEPaper7P5In : public WaveshareEPaper { public: void initialize() override; From 0b6c416680b24efb217d39a88d1bac5e6daed665 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:16:17 +1300 Subject: [PATCH 14/31] Bump esphome-dashboard to 20250212.0 (#8235) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0d93c3cc2d..bbf0ae7f3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.16 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 -esphome-dashboard==20241217.1 +esphome-dashboard==20250212.0 aioesphomeapi==24.6.2 zeroconf==0.143.0 puremagic==1.27 From 2b75e34719acfea017f436c63036050f1c9f3651 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:53:43 +1300 Subject: [PATCH 15/31] Bump version to 2025.2.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 16bfda9478..696cc599fc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.2.0-dev" +__version__ = "2025.2.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ab0d38fbdae7fe5f3d95cdf5c5675d8f6fee3deb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:53:43 +1300 Subject: [PATCH 16/31] Bump version to 2025.3.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 16bfda9478..f74ea64148 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.2.0-dev" +__version__ = "2025.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 3b7a7a2262183682502e74b969b9d52938f93385 Mon Sep 17 00:00:00 2001 From: guillempages Date: Wed, 12 Feb 2025 10:55:32 +0100 Subject: [PATCH 17/31] [online_image]Fix reset if buffer not allocated (#8236) --- .../components/online_image/image_decoder.cpp | 11 +++++++---- esphome/components/online_image/jpeg_image.cpp | 6 ++++-- .../components/online_image/online_image.cpp | 15 ++++++++------- esphome/components/online_image/online_image.h | 17 +++++++++++++++-- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp index d8c0cc33c4..0ab7dadde3 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/image_decoder.cpp @@ -9,10 +9,10 @@ namespace online_image { static const char *const TAG = "online_image.decoder"; bool ImageDecoder::set_size(int width, int height) { - bool resized = this->image_->resize_(width, height); + bool success = this->image_->resize_(width, height) > 0; this->x_scale_ = static_cast(this->image_->buffer_width_) / width; this->y_scale_ = static_cast(this->image_->buffer_height_) / height; - return resized; + return success; } void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { @@ -51,8 +51,9 @@ size_t DownloadBuffer::read(size_t len) { } size_t DownloadBuffer::resize(size_t size) { - if (this->size_ == size) { - return size; + if (this->size_ >= size) { + // Avoid useless reallocations; if the buffer is big enough, don't reallocate. + return this->size_; } this->allocator_.deallocate(this->buffer_, this->size_); this->buffer_ = this->allocator_.allocate(size); @@ -61,6 +62,8 @@ size_t DownloadBuffer::resize(size_t size) { this->size_ = size; return size; } else { + ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size, + this->allocator_.get_max_free_block_size()); this->size_ = 0; return 0; } diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/online_image/jpeg_image.cpp index 0aff576da8..e5ee3dd8bf 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/online_image/jpeg_image.cpp @@ -58,7 +58,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { } if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { - ESP_LOGE(TAG, "Could not open image for decoding."); + ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError()); return DECODE_ERROR_INVALID_TYPE; } auto jpeg_type = this->jpeg_.getJPEGType(); @@ -73,7 +73,9 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { this->jpeg_.setUserPointer(this); this->jpeg_.setPixelType(RGB8888); - this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight()); + if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) { + return DECODE_ERROR_OUT_OF_MEMORY; + } if (!this->jpeg_.decode(0, 0, 0)) { ESP_LOGE(TAG, "Error while decoding."); this->jpeg_.close(); diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 3b3d00a044..3411018901 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -64,33 +64,34 @@ void OnlineImage::release() { } } -bool OnlineImage::resize_(int width_in, int height_in) { +size_t OnlineImage::resize_(int width_in, int height_in) { int width = this->fixed_width_; int height = this->fixed_height_; - if (this->auto_resize_()) { + if (this->is_auto_resize_()) { width = width_in; height = height_in; if (this->width_ != width && this->height_ != height) { this->release(); } } - if (this->buffer_) { - return false; - } size_t new_size = this->get_buffer_size_(width, height); + if (this->buffer_) { + // Buffer already allocated => no need to resize + return new_size; + } ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); this->buffer_ = this->allocator_.allocate(new_size); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, this->allocator_.get_max_free_block_size()); this->end_connection_(); - return false; + return 0; } this->buffer_width_ = width; this->buffer_height_ = height; this->width_ = width; ESP_LOGV(TAG, "New size: (%d, %d)", width, height); - return true; + return new_size; } void OnlineImage::update() { diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 4abc047083..2d10e528b1 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -99,9 +99,22 @@ class OnlineImage : public PollingComponent, int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } - ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } + ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } - bool resize_(int width, int height); + /** + * @brief Resize the image buffer to the requested dimensions. + * + * The buffer will be allocated if not existing. + * If the dimensions have been fixed in the yaml config, the buffer will be created + * with those dimensions and not resized, even on request. + * Otherwise, the old buffer will be deallocated and a new buffer with the requested + * allocated + * + * @param width + * @param height + * @return 0 if no memory could be allocated, the size of the new buffer otherwise. + */ + size_t resize_(int width, int height); /** * @brief Draw a pixel into the buffer. From 43319d4c8aa0ec0a3718c5c01e6e2c383b7918b7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:05:46 +1300 Subject: [PATCH 18/31] [core] Ignore dot-prefixed config entries when looking for target platform (#8240) --- esphome/core/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index 7152414463..2077af02a7 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -217,6 +217,8 @@ def preload_core_config(config, result) -> str: target_platforms = [] for domain, _ in config.items(): + if domain.startswith("."): + continue if _is_target_platform(domain): target_platforms += [domain] From 4a95468fd26f5d4c30d43396b775644853776835 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Feb 2025 19:17:00 -0600 Subject: [PATCH 19/31] Bump zeroconf to 0.144.1 (#8238) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bbf0ae7f3d..8eca21c4bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==4.7.0 click==8.1.7 esphome-dashboard==20250212.0 aioesphomeapi==24.6.2 -zeroconf==0.143.0 +zeroconf==0.144.1 puremagic==1.27 ruamel.yaml==0.18.6 # dashboard_import glyphsets==1.0.0 From 72f64618717e3e21d769d5d5940b423f97fdc1d9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:57:57 +1300 Subject: [PATCH 20/31] [core] Fix ``config_dir`` for dashboard (#8242) --- esphome/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 2d856d99c5..2a7b8b9d91 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -582,7 +582,7 @@ class EsphomeCore: @property def config_dir(self): - return os.path.dirname(os.path.abspath(self.config_path)) + return os.path.abspath(os.path.dirname(self.config_path)) @property def data_dir(self): From 2868210d463f81e5057cb42b5f8e75a145d24b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Poczkodi?= Date: Thu, 13 Feb 2025 04:07:14 +0100 Subject: [PATCH 21/31] [cse7766] Remove stream dependency (#7720) Co-authored-by: Keith Burzinski --- esphome/components/cse7766/cse7766.cpp | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 3907c195d0..88a91e374a 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,8 +1,5 @@ #include "cse7766.h" #include "esphome/core/log.h" -#include -#include -#include namespace esphome { namespace cse7766 { @@ -72,12 +69,8 @@ bool CSE7766Component::check_byte_() { void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::stringstream ss; - ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); - for (uint8_t i = 0; i < 23; i++) { - ss << ' ' << std::setw(2) << static_cast(this->raw_data_[i]); - } - ESP_LOGVV(TAG, "%s", ss.str().c_str()); + std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_)); + ESP_LOGVV(TAG, "Raw data: %s", s.c_str()); } #endif @@ -211,21 +204,20 @@ void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::stringstream ss; - ss << "Parsed:"; + std::string buf = "Parsed:"; if (have_voltage) { - ss << " V=" << voltage << "V"; + buf += str_sprintf(" V=%fV", voltage); } if (have_current) { - ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)"; + buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f); } if (have_power) { - ss << " P=" << power << "W"; + buf += str_sprintf(" P=%fW", power); } if (energy != 0.0f) { - ss << " E=" << energy << "kWh (" << cf_pulses << ")"; + buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses); } - ESP_LOGVV(TAG, "%s", ss.str().c_str()); + ESP_LOGVV(TAG, "%s", buf.c_str()); } #endif } From e190ef9e9bf4d50f0c683e415c7a5e2abd0a2368 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 12 Feb 2025 21:37:29 -0600 Subject: [PATCH 22/31] [graph] Remove ``stream`` dependency (#8243) --- esphome/components/graph/graph.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 09f7557714..cbe059b255 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -4,9 +4,6 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" #include -#include -#include // std::cout, std::fixed -#include namespace esphome { namespace graph { @@ -231,9 +228,8 @@ void GraphLegend::init(Graph *g) { ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh); if (this->values_ != VALUE_POSITION_TYPE_NONE) { - std::stringstream ss; - ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); - std::string valstr = ss.str(); + std::string valstr = + value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { valstr += trace->sensor_->get_unit_of_measurement(); } @@ -368,9 +364,8 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of if (legend_->values_ != VALUE_POSITION_TYPE_NONE) { int xv = x + legend_->xv_; int yv = y + legend_->yv_; - std::stringstream ss; - ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); - std::string valstr = ss.str(); + std::string valstr = + value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { valstr += trace->sensor_->get_unit_of_measurement(); } From ace953bd501d299ec95f86432477fe0b47a26767 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 12 Feb 2025 22:34:16 -0600 Subject: [PATCH 23/31] [modbus_controller] Remove `stream` dependency (#8244) --- .../text_sensor/modbus_textsensor.cpp | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp index acdcacc083..89e86741b0 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -1,8 +1,6 @@ #include "modbus_textsensor.h" #include "esphome/core/log.h" -#include -#include namespace esphome { namespace modbus_controller { @@ -12,20 +10,17 @@ static const char *const TAG = "modbus_controller.text_sensor"; void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); } void ModbusTextSensor::parse_and_publish(const std::vector &data) { - std::ostringstream output; + std::string output_str{}; uint8_t items_left = this->response_bytes; uint8_t index = this->offset; - char buffer[5]; while ((items_left > 0) && index < data.size()) { uint8_t b = data[index]; switch (this->encode_) { case RawEncoding::HEXBYTES: - sprintf(buffer, "%02x", b); - output << buffer; + output_str += str_snprintf("%02x", 2, b); break; case RawEncoding::COMMA: - sprintf(buffer, index != this->offset ? ",%d" : "%d", b); - output << buffer; + output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b); break; case RawEncoding::ANSI: if (b < 0x20) @@ -33,25 +28,24 @@ void ModbusTextSensor::parse_and_publish(const std::vector &data) { // FALLTHROUGH // Anything else no encoding default: - output << (char) b; + output_str += (char) b; break; } items_left--; index++; } - auto result = output.str(); // Is there a lambda registered // call it with the pre converted value and the raw data array if (this->transform_func_.has_value()) { // the lambda can parse the response itself - auto val = (*this->transform_func_)(this, result, data); + auto val = (*this->transform_func_)(this, output_str, data); if (val.has_value()) { ESP_LOGV(TAG, "Value overwritten by lambda"); - result = val.value(); + output_str = val.value(); } } - this->publish_state(result); + this->publish_state(output_str); } } // namespace modbus_controller From fa029e8fc7b553472baf81a1b200fe138a9554a5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 13 Feb 2025 01:40:02 -0600 Subject: [PATCH 24/31] [modbus_controller] Extend tests (#8245) --- .../components/modbus_controller/common.yaml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index 93d8391ff5..7fa9f8dae3 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -33,3 +33,73 @@ modbus_controller: read_lambda: |- return 42.3; max_cmd_retries: 0 + +binary_sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_binary_sensor1 + name: Test Binary Sensor + register_type: read + address: 0x3200 + bitmask: 0x80 + +number: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_number1 + name: Test Number + address: 0x9001 + value_type: U_WORD + multiply: 1.0 + +output: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_output1 + address: 2048 + register_type: holding + value_type: U_WORD + multiply: 1000 + +select: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_select1 + name: Test Select + address: 1000 + value_type: U_WORD + optionsmap: + "Zero": 0 + "One": 1 + "Two": 2 + "Three": 3 + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_sensor1 + name: Test Sensor + register_type: holding + address: 0x9001 + unit_of_measurement: "AH" + value_type: U_WORD + +switch: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_switch1 + name: Test Switch + register_type: coil + address: 0x15 + bitmask: 1 + +text_sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_text_sensor1 + name: Test Text Sensor + register_type: holding + address: 0x9013 + register_count: 3 + raw_encode: HEXBYTES + response_size: 6 From 077ee5b7143d1ea033ec513f354b07e07be1b62c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:05:46 +1300 Subject: [PATCH 25/31] [core] Ignore dot-prefixed config entries when looking for target platform (#8240) --- esphome/core/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index 7152414463..2077af02a7 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -217,6 +217,8 @@ def preload_core_config(config, result) -> str: target_platforms = [] for domain, _ in config.items(): + if domain.startswith("."): + continue if _is_target_platform(domain): target_platforms += [domain] From c8e7e275a4bedde565867288416acb06bbe0cc51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Feb 2025 19:17:00 -0600 Subject: [PATCH 26/31] Bump zeroconf to 0.144.1 (#8238) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bbf0ae7f3d..8eca21c4bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==4.7.0 click==8.1.7 esphome-dashboard==20250212.0 aioesphomeapi==24.6.2 -zeroconf==0.143.0 +zeroconf==0.144.1 puremagic==1.27 ruamel.yaml==0.18.6 # dashboard_import glyphsets==1.0.0 From 4740f12ce8e5e6d63278496e3e8dc909d5a33ab3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:57:57 +1300 Subject: [PATCH 27/31] [core] Fix ``config_dir`` for dashboard (#8242) --- esphome/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 2d856d99c5..2a7b8b9d91 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -582,7 +582,7 @@ class EsphomeCore: @property def config_dir(self): - return os.path.dirname(os.path.abspath(self.config_path)) + return os.path.abspath(os.path.dirname(self.config_path)) @property def data_dir(self): From 35d303809e8d5b4ed11e0b6bc70065bf417aed75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Poczkodi?= Date: Thu, 13 Feb 2025 04:07:14 +0100 Subject: [PATCH 28/31] [cse7766] Remove stream dependency (#7720) Co-authored-by: Keith Burzinski --- esphome/components/cse7766/cse7766.cpp | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 3907c195d0..88a91e374a 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,8 +1,5 @@ #include "cse7766.h" #include "esphome/core/log.h" -#include -#include -#include namespace esphome { namespace cse7766 { @@ -72,12 +69,8 @@ bool CSE7766Component::check_byte_() { void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::stringstream ss; - ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); - for (uint8_t i = 0; i < 23; i++) { - ss << ' ' << std::setw(2) << static_cast(this->raw_data_[i]); - } - ESP_LOGVV(TAG, "%s", ss.str().c_str()); + std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_)); + ESP_LOGVV(TAG, "Raw data: %s", s.c_str()); } #endif @@ -211,21 +204,20 @@ void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::stringstream ss; - ss << "Parsed:"; + std::string buf = "Parsed:"; if (have_voltage) { - ss << " V=" << voltage << "V"; + buf += str_sprintf(" V=%fV", voltage); } if (have_current) { - ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)"; + buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f); } if (have_power) { - ss << " P=" << power << "W"; + buf += str_sprintf(" P=%fW", power); } if (energy != 0.0f) { - ss << " E=" << energy << "kWh (" << cf_pulses << ")"; + buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses); } - ESP_LOGVV(TAG, "%s", ss.str().c_str()); + ESP_LOGVV(TAG, "%s", buf.c_str()); } #endif } From e9a537784e314484dff0e2bd32377177465c33fe Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 12 Feb 2025 21:37:29 -0600 Subject: [PATCH 29/31] [graph] Remove ``stream`` dependency (#8243) --- esphome/components/graph/graph.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 09f7557714..cbe059b255 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -4,9 +4,6 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" #include -#include -#include // std::cout, std::fixed -#include namespace esphome { namespace graph { @@ -231,9 +228,8 @@ void GraphLegend::init(Graph *g) { ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh); if (this->values_ != VALUE_POSITION_TYPE_NONE) { - std::stringstream ss; - ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); - std::string valstr = ss.str(); + std::string valstr = + value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { valstr += trace->sensor_->get_unit_of_measurement(); } @@ -368,9 +364,8 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of if (legend_->values_ != VALUE_POSITION_TYPE_NONE) { int xv = x + legend_->xv_; int yv = y + legend_->yv_; - std::stringstream ss; - ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); - std::string valstr = ss.str(); + std::string valstr = + value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { valstr += trace->sensor_->get_unit_of_measurement(); } From be5639faf1b34fccc0a99c77dabeb87417eb3332 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 12 Feb 2025 22:34:16 -0600 Subject: [PATCH 30/31] [modbus_controller] Remove `stream` dependency (#8244) --- .../text_sensor/modbus_textsensor.cpp | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp index acdcacc083..89e86741b0 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -1,8 +1,6 @@ #include "modbus_textsensor.h" #include "esphome/core/log.h" -#include -#include namespace esphome { namespace modbus_controller { @@ -12,20 +10,17 @@ static const char *const TAG = "modbus_controller.text_sensor"; void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); } void ModbusTextSensor::parse_and_publish(const std::vector &data) { - std::ostringstream output; + std::string output_str{}; uint8_t items_left = this->response_bytes; uint8_t index = this->offset; - char buffer[5]; while ((items_left > 0) && index < data.size()) { uint8_t b = data[index]; switch (this->encode_) { case RawEncoding::HEXBYTES: - sprintf(buffer, "%02x", b); - output << buffer; + output_str += str_snprintf("%02x", 2, b); break; case RawEncoding::COMMA: - sprintf(buffer, index != this->offset ? ",%d" : "%d", b); - output << buffer; + output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b); break; case RawEncoding::ANSI: if (b < 0x20) @@ -33,25 +28,24 @@ void ModbusTextSensor::parse_and_publish(const std::vector &data) { // FALLTHROUGH // Anything else no encoding default: - output << (char) b; + output_str += (char) b; break; } items_left--; index++; } - auto result = output.str(); // Is there a lambda registered // call it with the pre converted value and the raw data array if (this->transform_func_.has_value()) { // the lambda can parse the response itself - auto val = (*this->transform_func_)(this, result, data); + auto val = (*this->transform_func_)(this, output_str, data); if (val.has_value()) { ESP_LOGV(TAG, "Value overwritten by lambda"); - result = val.value(); + output_str = val.value(); } } - this->publish_state(result); + this->publish_state(output_str); } } // namespace modbus_controller From b0f6dd7d9c44690b8e33c87b63acd033a72dc233 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:44:12 +1300 Subject: [PATCH 31/31] Bump version to 2025.2.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 696cc599fc..7390187d7e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.2.0b1" +__version__ = "2025.2.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (