mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'esphome:dev' into mcp4461
This commit is contained in:
		| @@ -391,6 +391,7 @@ esphome/components/sn74hc165/* @jesserockz | |||||||
| esphome/components/socket/* @esphome/core | esphome/components/socket/* @esphome/core | ||||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||||
| esphome/components/speaker/* @jesserockz @kahrendt | esphome/components/speaker/* @jesserockz @kahrendt | ||||||
|  | esphome/components/speaker/media_player/* @kahrendt @synesthesiam | ||||||
| esphome/components/spi/* @clydebarrow @esphome/core | esphome/components/spi/* @clydebarrow @esphome/core | ||||||
| esphome/components/spi_device/* @clydebarrow | esphome/components/spi_device/* @clydebarrow | ||||||
| esphome/components/spi_led_strip/* @clydebarrow | esphome/components/spi_led_strip/* @clydebarrow | ||||||
|   | |||||||
| @@ -1,8 +1,5 @@ | |||||||
| #include "cse7766.h" | #include "cse7766.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include <cinttypes> |  | ||||||
| #include <iomanip> |  | ||||||
| #include <sstream> |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace cse7766 { | namespace cse7766 { | ||||||
| @@ -72,12 +69,8 @@ bool CSE7766Component::check_byte_() { | |||||||
| void CSE7766Component::parse_data_() { | void CSE7766Component::parse_data_() { | ||||||
| #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE | #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||||
|   { |   { | ||||||
|     std::stringstream ss; |     std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_)); | ||||||
|     ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); |     ESP_LOGVV(TAG, "Raw data: %s", s.c_str()); | ||||||
|     for (uint8_t i = 0; i < 23; i++) { |  | ||||||
|       ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]); |  | ||||||
|     } |  | ||||||
|     ESP_LOGVV(TAG, "%s", ss.str().c_str()); |  | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -211,21 +204,20 @@ void CSE7766Component::parse_data_() { | |||||||
|  |  | ||||||
| #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE | #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||||
|   { |   { | ||||||
|     std::stringstream ss; |     std::string buf = "Parsed:"; | ||||||
|     ss << "Parsed:"; |  | ||||||
|     if (have_voltage) { |     if (have_voltage) { | ||||||
|       ss << " V=" << voltage << "V"; |       buf += str_sprintf(" V=%fV", voltage); | ||||||
|     } |     } | ||||||
|     if (have_current) { |     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) { |     if (have_power) { | ||||||
|       ss << " P=" << power << "W"; |       buf += str_sprintf(" P=%fW", power); | ||||||
|     } |     } | ||||||
|     if (energy != 0.0f) { |     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 | #endif | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,9 +4,6 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
| #include <sstream> |  | ||||||
| #include <iostream>  // std::cout, std::fixed |  | ||||||
| #include <iomanip> |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace graph { | namespace graph { | ||||||
|  |  | ||||||
| @@ -231,9 +228,8 @@ void GraphLegend::init(Graph *g) { | |||||||
|     ESP_LOGI(TAGL, "  %s %d %d", txtstr.c_str(), fw, fh); |     ESP_LOGI(TAGL, "  %s %d %d", txtstr.c_str(), fw, fh); | ||||||
|  |  | ||||||
|     if (this->values_ != VALUE_POSITION_TYPE_NONE) { |     if (this->values_ != VALUE_POSITION_TYPE_NONE) { | ||||||
|       std::stringstream ss; |       std::string valstr = | ||||||
|       ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); |           value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); | ||||||
|       std::string valstr = ss.str(); |  | ||||||
|       if (this->units_) { |       if (this->units_) { | ||||||
|         valstr += trace->sensor_->get_unit_of_measurement(); |         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) { |     if (legend_->values_ != VALUE_POSITION_TYPE_NONE) { | ||||||
|       int xv = x + legend_->xv_; |       int xv = x + legend_->xv_; | ||||||
|       int yv = y + legend_->yv_; |       int yv = y + legend_->yv_; | ||||||
|       std::stringstream ss; |       std::string valstr = | ||||||
|       ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); |           value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); | ||||||
|       std::string valstr = ss.str(); |  | ||||||
|       if (legend_->units_) { |       if (legend_->units_) { | ||||||
|         valstr += trace->sensor_->get_unit_of_measurement(); |         valstr += trace->sensor_->get_unit_of_measurement(); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -18,8 +18,8 @@ namespace esphome { | |||||||
| namespace http_request { | namespace http_request { | ||||||
|  |  | ||||||
| struct Header { | struct Header { | ||||||
|   const char *name; |   std::string name; | ||||||
|   const char *value; |   std::string value; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Some common HTTP status codes | // Some common HTTP status codes | ||||||
|   | |||||||
| @@ -96,7 +96,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s | |||||||
|     container->client_.setUserAgent(this->useragent_); |     container->client_.setUserAgent(this->useragent_); | ||||||
|   } |   } | ||||||
|   for (const auto &header : headers) { |   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 |   // returned needed headers must be collected before the requests | ||||||
|   | |||||||
| @@ -84,7 +84,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin | |||||||
|   container->set_secure(secure); |   container->set_secure(secure); | ||||||
|  |  | ||||||
|   for (const auto &header : headers) { |   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(); |   const int body_len = body.length(); | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| from esphome import automation | from esphome import automation | ||||||
| from esphome.automation import maybe_simple_id |  | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -21,6 +20,16 @@ media_player_ns = cg.esphome_ns.namespace("media_player") | |||||||
|  |  | ||||||
| MediaPlayer = media_player_ns.class_("MediaPlayer") | 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 = media_player_ns.class_( | ||||||
|     "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) |     "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) | ||||||
| @@ -47,7 +56,7 @@ VolumeSetAction = media_player_ns.class_( | |||||||
|     "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) |     "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | CONF_ANNOUNCEMENT = "announcement" | ||||||
| CONF_ON_PLAY = "on_play" | CONF_ON_PLAY = "on_play" | ||||||
| CONF_ON_PAUSE = "on_pause" | CONF_ON_PAUSE = "on_pause" | ||||||
| CONF_ON_ANNOUNCEMENT = "on_announcement" | 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( | @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.GenerateID(): cv.use_id(MediaPlayer), | ||||||
|             cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), |             cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), | ||||||
|  |             cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), | ||||||
|         }, |         }, | ||||||
|         key=CONF_MEDIA_URL, |         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) |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|     await cg.register_parented(var, config[CONF_ID]) |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|     media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) |     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_media_url(media_url)) | ||||||
|  |     cg.add(var.set_announcement(announcement)) | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -161,19 +182,27 @@ async def media_player_play_media_action(config, action_id, template_arg, args): | |||||||
| @automation.register_action( | @automation.register_action( | ||||||
|     "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA |     "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): | 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) |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|     await cg.register_parented(var, config[CONF_ID]) |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|     return var |     return var | ||||||
|   | |||||||
| @@ -10,7 +10,10 @@ namespace media_player { | |||||||
| template<MediaPlayerCommand Command, typename... Ts> | template<MediaPlayerCommand Command, typename... Ts> | ||||||
| class MediaPlayerCommandAction : public Action<Ts...>, public Parented<MediaPlayer> { | class MediaPlayerCommandAction : public Action<Ts...>, public Parented<MediaPlayer> { | ||||||
|  public: |  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<typename... Ts> | template<typename... Ts> | ||||||
| @@ -28,7 +31,13 @@ using VolumeDownAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAY | |||||||
|  |  | ||||||
| template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> { | template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> { | ||||||
|   TEMPLATABLE_VALUE(std::string, media_url) |   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<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> { | template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> { | ||||||
|   | |||||||
| @@ -41,6 +41,14 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { | |||||||
|       return "VOLUME_UP"; |       return "VOLUME_UP"; | ||||||
|     case MEDIA_PLAYER_COMMAND_VOLUME_DOWN: |     case MEDIA_PLAYER_COMMAND_VOLUME_DOWN: | ||||||
|       return "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: |     default: | ||||||
|       return "UNKNOWN"; |       return "UNKNOWN"; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -24,6 +24,10 @@ enum MediaPlayerCommand : uint8_t { | |||||||
|   MEDIA_PLAYER_COMMAND_TOGGLE = 5, |   MEDIA_PLAYER_COMMAND_TOGGLE = 5, | ||||||
|   MEDIA_PLAYER_COMMAND_VOLUME_UP = 6, |   MEDIA_PLAYER_COMMAND_VOLUME_UP = 6, | ||||||
|   MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7, |   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); | const char *media_player_command_to_string(MediaPlayerCommand command); | ||||||
|  |  | ||||||
| @@ -72,10 +76,10 @@ class MediaPlayerCall { | |||||||
|  |  | ||||||
|   void perform(); |   void perform(); | ||||||
|  |  | ||||||
|   const optional<MediaPlayerCommand> &get_command() const { return command_; } |   const optional<MediaPlayerCommand> &get_command() const { return this->command_; } | ||||||
|   const optional<std::string> &get_media_url() const { return media_url_; } |   const optional<std::string> &get_media_url() const { return this->media_url_; } | ||||||
|   const optional<float> &get_volume() const { return volume_; } |   const optional<float> &get_volume() const { return this->volume_; } | ||||||
|   const optional<bool> &get_announcement() const { return announcement_; } |   const optional<bool> &get_announcement() const { return this->announcement_; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void validate_(); |   void validate_(); | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
|  |  | ||||||
| #include "modbus_textsensor.h" | #include "modbus_textsensor.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include <iomanip> |  | ||||||
| #include <sstream> |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace modbus_controller { | 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::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); } | ||||||
|  |  | ||||||
| void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) { | void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) { | ||||||
|   std::ostringstream output; |   std::string output_str{}; | ||||||
|   uint8_t items_left = this->response_bytes; |   uint8_t items_left = this->response_bytes; | ||||||
|   uint8_t index = this->offset; |   uint8_t index = this->offset; | ||||||
|   char buffer[5]; |  | ||||||
|   while ((items_left > 0) && index < data.size()) { |   while ((items_left > 0) && index < data.size()) { | ||||||
|     uint8_t b = data[index]; |     uint8_t b = data[index]; | ||||||
|     switch (this->encode_) { |     switch (this->encode_) { | ||||||
|       case RawEncoding::HEXBYTES: |       case RawEncoding::HEXBYTES: | ||||||
|         sprintf(buffer, "%02x", b); |         output_str += str_snprintf("%02x", 2, b); | ||||||
|         output << buffer; |  | ||||||
|         break; |         break; | ||||||
|       case RawEncoding::COMMA: |       case RawEncoding::COMMA: | ||||||
|         sprintf(buffer, index != this->offset ? ",%d" : "%d", b); |         output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b); | ||||||
|         output << buffer; |  | ||||||
|         break; |         break; | ||||||
|       case RawEncoding::ANSI: |       case RawEncoding::ANSI: | ||||||
|         if (b < 0x20) |         if (b < 0x20) | ||||||
| @@ -33,25 +28,24 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) { | |||||||
|       // FALLTHROUGH |       // FALLTHROUGH | ||||||
|       // Anything else no encoding |       // Anything else no encoding | ||||||
|       default: |       default: | ||||||
|         output << (char) b; |         output_str += (char) b; | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|     items_left--; |     items_left--; | ||||||
|     index++; |     index++; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto result = output.str(); |  | ||||||
|   // Is there a lambda registered |   // Is there a lambda registered | ||||||
|   // call it with the pre converted value and the raw data array |   // call it with the pre converted value and the raw data array | ||||||
|   if (this->transform_func_.has_value()) { |   if (this->transform_func_.has_value()) { | ||||||
|     // the lambda can parse the response itself |     // 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()) { |     if (val.has_value()) { | ||||||
|       ESP_LOGV(TAG, "Value overwritten by lambda"); |       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 | }  // namespace modbus_controller | ||||||
|   | |||||||
| @@ -9,10 +9,10 @@ namespace online_image { | |||||||
| static const char *const TAG = "online_image.decoder"; | static const char *const TAG = "online_image.decoder"; | ||||||
|  |  | ||||||
| bool ImageDecoder::set_size(int width, int height) { | 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<double>(this->image_->buffer_width_) / width; |   this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width; | ||||||
|   this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height; |   this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height; | ||||||
|   return resized; |   return success; | ||||||
| } | } | ||||||
|  |  | ||||||
| void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { | void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { | ||||||
| @@ -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) { | uint8_t *DownloadBuffer::data(size_t offset) { | ||||||
|   if (offset > this->size_) { |   if (offset > this->size_) { | ||||||
|     ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); |     ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); | ||||||
| @@ -42,16 +51,20 @@ size_t DownloadBuffer::read(size_t len) { | |||||||
| } | } | ||||||
|  |  | ||||||
| size_t DownloadBuffer::resize(size_t size) { | size_t DownloadBuffer::resize(size_t size) { | ||||||
|   if (this->size_ == size) { |   if (this->size_ >= size) { | ||||||
|     return size; |     // Avoid useless reallocations; if the buffer is big enough, don't reallocate. | ||||||
|  |     return this->size_; | ||||||
|   } |   } | ||||||
|   this->allocator_.deallocate(this->buffer_, this->size_); |   this->allocator_.deallocate(this->buffer_, this->size_); | ||||||
|   this->size_ = size; |  | ||||||
|   this->buffer_ = this->allocator_.allocate(size); |   this->buffer_ = this->allocator_.allocate(size); | ||||||
|   this->reset(); |   this->reset(); | ||||||
|   if (this->buffer_) { |   if (this->buffer_) { | ||||||
|  |     this->size_ = size; | ||||||
|     return size; |     return size; | ||||||
|   } else { |   } 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; |     return 0; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,8 +29,12 @@ class ImageDecoder { | |||||||
|    * @brief Initialize the decoder. |    * @brief Initialize the decoder. | ||||||
|    * |    * | ||||||
|    * @param download_size The total number of bytes that need to be downloaded for the image. |    * @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. |    * @brief Decode a part of the image. It will try reading from the buffer. | ||||||
| @@ -83,10 +87,7 @@ class ImageDecoder { | |||||||
|  |  | ||||||
| class DownloadBuffer { | class DownloadBuffer { | ||||||
|  public: |  public: | ||||||
|   DownloadBuffer(size_t size) : size_(size) { |   DownloadBuffer(size_t size); | ||||||
|     this->buffer_ = this->allocator_.allocate(size); |  | ||||||
|     this->reset(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } |   virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -41,13 +41,14 @@ static int draw_callback(JPEGDRAW *jpeg) { | |||||||
|   return 1; |   return 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| void JpegDecoder::prepare(size_t download_size) { | int JpegDecoder::prepare(size_t download_size) { | ||||||
|   ImageDecoder::prepare(download_size); |   ImageDecoder::prepare(download_size); | ||||||
|   auto size = this->image_->resize_download_buffer(download_size); |   auto size = this->image_->resize_download_buffer(download_size); | ||||||
|   if (size < download_size) { |   if (size < download_size) { | ||||||
|     ESP_LOGE(TAG, "Resize failed!"); |     ESP_LOGE(TAG, "Download buffer resize failed!"); | ||||||
|     // TODO: return an error code; |     return DECODE_ERROR_OUT_OF_MEMORY; | ||||||
|   } |   } | ||||||
|  |   return 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { | int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { | ||||||
| @@ -57,7 +58,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { |   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; |     return DECODE_ERROR_INVALID_TYPE; | ||||||
|   } |   } | ||||||
|   auto jpeg_type = this->jpeg_.getJPEGType(); |   auto jpeg_type = this->jpeg_.getJPEGType(); | ||||||
| @@ -72,7 +73,9 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { | |||||||
|  |  | ||||||
|   this->jpeg_.setUserPointer(this); |   this->jpeg_.setUserPointer(this); | ||||||
|   this->jpeg_.setPixelType(RGB8888); |   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)) { |   if (!this->jpeg_.decode(0, 0, 0)) { | ||||||
|     ESP_LOGE(TAG, "Error while decoding."); |     ESP_LOGE(TAG, "Error while decoding."); | ||||||
|     this->jpeg_.close(); |     this->jpeg_.close(); | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ class JpegDecoder : public ImageDecoder { | |||||||
|   JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} |   JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} | ||||||
|   ~JpegDecoder() override {} |   ~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; |   int HOT decode(uint8_t *buffer, size_t size) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -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 width = this->fixed_width_; | ||||||
|   int height = this->fixed_height_; |   int height = this->fixed_height_; | ||||||
|   if (this->auto_resize_()) { |   if (this->is_auto_resize_()) { | ||||||
|     width = width_in; |     width = width_in; | ||||||
|     height = height_in; |     height = height_in; | ||||||
|     if (this->width_ != width && this->height_ != height) { |     if (this->width_ != width && this->height_ != height) { | ||||||
|       this->release(); |       this->release(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (this->buffer_) { |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|   size_t new_size = this->get_buffer_size_(width, height); |   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); |   ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); | ||||||
|   this->buffer_ = this->allocator_.allocate(new_size); |   this->buffer_ = this->allocator_.allocate(new_size); | ||||||
|   if (this->buffer_ == nullptr) { |   if (this->buffer_ == nullptr) { | ||||||
|     ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, |     ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, | ||||||
|              this->allocator_.get_max_free_block_size()); |              this->allocator_.get_max_free_block_size()); | ||||||
|     this->end_connection_(); |     this->end_connection_(); | ||||||
|     return false; |     return 0; | ||||||
|   } |   } | ||||||
|   this->buffer_width_ = width; |   this->buffer_width_ = width; | ||||||
|   this->buffer_height_ = height; |   this->buffer_height_ = height; | ||||||
|   this->width_ = width; |   this->width_ = width; | ||||||
|   ESP_LOGV(TAG, "New size: (%d, %d)", width, height); |   ESP_LOGV(TAG, "New size: (%d, %d)", width, height); | ||||||
|   return true; |   return new_size; | ||||||
| } | } | ||||||
|  |  | ||||||
| void OnlineImage::update() { | void OnlineImage::update() { | ||||||
| @@ -124,7 +125,7 @@ void OnlineImage::update() { | |||||||
|     default: |     default: | ||||||
|       accept_mime_type = "image/*"; |       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); |   headers.push_back(accept_header); | ||||||
|  |  | ||||||
| @@ -178,7 +179,12 @@ void OnlineImage::update() { | |||||||
|     this->download_error_callback_.call(); |     this->download_error_callback_.call(); | ||||||
|     return; |     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); |   ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size); | ||||||
|   this->start_time_ = ::time(nullptr); |   this->start_time_ = ::time(nullptr); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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; } |   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. |    * @brief Draw a pixel into the buffer. | ||||||
|   | |||||||
| @@ -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); |   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); |   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_user_data(this->pngle_, this); | ||||||
|   pngle_set_init_callback(this->pngle_, init_callback); |   pngle_set_init_callback(this->pngle_, init_callback); | ||||||
|   pngle_set_draw_callback(this->pngle_, draw_callback); |   pngle_set_draw_callback(this->pngle_, draw_callback); | ||||||
|  |   return 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { | int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ class PngDecoder : public ImageDecoder { | |||||||
|   PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} |   PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} | ||||||
|   ~PngDecoder() override { pngle_destroy(this->pngle_); } |   ~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; |   int HOT decode(uint8_t *buffer, size_t size) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -83,6 +83,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { | |||||||
|     this->update_entity_row_(stream, obj, area, node, friendly_name); |     this->update_entity_row_(stream, obj, area, node, friendly_name); | ||||||
| #endif | #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); |   req->send(stream); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -770,6 +776,54 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update:: | |||||||
| } | } | ||||||
| #endif | #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 prometheus | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -161,6 +161,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { | |||||||
|   void handle_update_state_(AsyncResponseStream *stream, update::UpdateState state); |   void handle_update_state_(AsyncResponseStream *stream, update::UpdateState state); | ||||||
| #endif | #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_; |   web_server_base::WebServerBase *base_; | ||||||
|   bool include_internal_{false}; |   bool include_internal_{false}; | ||||||
|   std::map<EntityBase *, std::string> relabel_map_id_; |   std::map<EntityBase *, std::string> relabel_map_id_; | ||||||
|   | |||||||
							
								
								
									
										458
									
								
								esphome/components/speaker/media_player/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								esphome/components/speaker/media_player/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										568
									
								
								esphome/components/speaker/media_player/audio_pipeline.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										568
									
								
								esphome/components/speaker/media_player/audio_pipeline.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,568 @@ | |||||||
|  | #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); | ||||||
|  |       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_(); | ||||||
|  |         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; | ||||||
|  |     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<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); | ||||||
|  |         this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); | ||||||
|  |       } else { | ||||||
|  |         RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::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<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); | ||||||
|  |         this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); | ||||||
|  |       } else { | ||||||
|  |         RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::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<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); | ||||||
|  |         stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); | ||||||
|  |       } else { | ||||||
|  |         RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::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<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); | ||||||
|  |         stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); | ||||||
|  |       } else { | ||||||
|  |         RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::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<audio::AudioReader> reader = | ||||||
|  |           make_unique<audio::AudioReader>(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<RingBuffer> 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<audio::AudioDecoder> decoder = | ||||||
|  |           make_unique<audio::AudioDecoder>(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<RingBuffer> 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 | ||||||
							
								
								
									
										159
									
								
								esphome/components/speaker/media_player/audio_pipeline.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								esphome/components/speaker/media_player/audio_pipeline.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | #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 <freertos/FreeRTOS.h> | ||||||
|  | #include <freertos/event_groups.h> | ||||||
|  | #include <freertos/queue.h> | ||||||
|  |  | ||||||
|  | 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<esp_err_t> err; | ||||||
|  |   optional<audio::AudioFileType> file_type; | ||||||
|  |   optional<audio::AudioStreamInfo> audio_stream_info; | ||||||
|  |   optional<DecodingError> 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 hard_stop_{false}; | ||||||
|  |   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<RingBuffer> 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 | ||||||
							
								
								
									
										26
									
								
								esphome/components/speaker/media_player/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/speaker/media_player/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<typename... Ts> class PlayOnDeviceMediaAction : public Action<Ts...>, public Parented<SpeakerMediaPlayer> { | ||||||
|  |   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 | ||||||
							
								
								
									
										577
									
								
								esphome/components/speaker/media_player/speaker_media_player.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										577
									
								
								esphome/components/speaker/media_player/speaker_media_player.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<VolumeRestoreState>(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<AudioPipeline>(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<AudioPipeline>(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<float, float>(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 | ||||||
							
								
								
									
										160
									
								
								esphome/components/speaker/media_player/speaker_media_player.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								esphome/components/speaker/media_player/speaker_media_player.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <deque> | ||||||
|  | #include <freertos/FreeRTOS.h> | ||||||
|  | #include <freertos/queue.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace speaker { | ||||||
|  |  | ||||||
|  | struct MediaCallCommand { | ||||||
|  |   optional<media_player::MediaPlayerCommand> command; | ||||||
|  |   optional<float> volume; | ||||||
|  |   optional<bool> announce; | ||||||
|  |   optional<bool> new_url; | ||||||
|  |   optional<bool> new_file; | ||||||
|  |   optional<bool> enqueue; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct PlaylistItem { | ||||||
|  |   optional<std::string> url; | ||||||
|  |   optional<audio::AudioFile *> 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<float> *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<AudioPipeline> announcement_pipeline_; | ||||||
|  |   std::unique_ptr<AudioPipeline> media_pipeline_; | ||||||
|  |   Speaker *media_speaker_{nullptr}; | ||||||
|  |   Speaker *announcement_speaker_{nullptr}; | ||||||
|  |  | ||||||
|  |   optional<media_player::MediaPlayerSupportedFormat> 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<media_player::MediaPlayerSupportedFormat> 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<PlaylistItem> announcement_playlist_; | ||||||
|  |   std::deque<PlaylistItem> 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<float> *volume_trigger_ = new Trigger<float>(); | ||||||
|  |  | ||||||
|  |   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 | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| #include "voice_assistant.h" | #include "voice_assistant.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|  |  | ||||||
| @@ -127,7 +128,7 @@ void VoiceAssistant::clear_buffers_() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
| #ifdef USE_SPEAKER | #ifdef USE_SPEAKER | ||||||
|   if (this->speaker_buffer_ != nullptr) { |   if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { | ||||||
|     memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); |     memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); | ||||||
|  |  | ||||||
|     this->speaker_buffer_size_ = 0; |     this->speaker_buffer_size_ = 0; | ||||||
| @@ -159,7 +160,7 @@ void VoiceAssistant::deallocate_buffers_() { | |||||||
|   this->input_buffer_ = nullptr; |   this->input_buffer_ = nullptr; | ||||||
|  |  | ||||||
| #ifdef USE_SPEAKER | #ifdef USE_SPEAKER | ||||||
|   if (this->speaker_buffer_ != nullptr) { |   if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { | ||||||
|     ExternalRAMAllocator<uint8_t> speaker_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); |     ExternalRAMAllocator<uint8_t> speaker_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||||
|     speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE); |     speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE); | ||||||
|     this->speaker_buffer_ = nullptr; |     this->speaker_buffer_ = nullptr; | ||||||
| @@ -389,14 +390,7 @@ void VoiceAssistant::loop() { | |||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       if (playing) { |       if (playing) { | ||||||
|         this->set_timeout("playing", 2000, [this]() { |         this->start_playback_timeout_(); | ||||||
|           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); |  | ||||||
|         }); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| @@ -614,6 +608,8 @@ void VoiceAssistant::request_stop() { | |||||||
|       this->desired_state_ = State::IDLE; |       this->desired_state_ = State::IDLE; | ||||||
|       break; |       break; | ||||||
|     case State::AWAITING_RESPONSE: |     case State::AWAITING_RESPONSE: | ||||||
|  |       this->signal_stop_(); | ||||||
|  |       break; | ||||||
|     case State::STREAMING_RESPONSE: |     case State::STREAMING_RESPONSE: | ||||||
|     case State::RESPONSE_FINISHED: |     case State::RESPONSE_FINISHED: | ||||||
|       break;  // Let the incoming audio stream finish then it will go to idle. |       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); |   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) { | void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | ||||||
|   ESP_LOGD(TAG, "Event Type: %" PRId32, msg.event_type); |   ESP_LOGD(TAG, "Event Type: %" PRId32, msg.event_type); | ||||||
|   switch (msg.event_type) { |   switch (msg.event_type) { | ||||||
| @@ -715,6 +722,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|         if (this->media_player_ != nullptr) { |         if (this->media_player_ != nullptr) { | ||||||
|           this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); |           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 | #endif | ||||||
|         this->tts_end_trigger_->trigger(url); |         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: { |     case api::enums::VOICE_ASSISTANT_RUN_END: { | ||||||
|       ESP_LOGD(TAG, "Assist Pipeline ended"); |       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(); |         this->ring_buffer_->reset(); | ||||||
| #ifdef USE_ESP_ADF | #ifdef USE_ESP_ADF | ||||||
|         if (this->use_wake_word_) { |         if (this->use_wake_word_) { | ||||||
| @@ -736,9 +749,6 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
|         { |         { | ||||||
|           this->set_state_(State::IDLE, State::IDLE); |           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(); }); |       this->defer([this]() { this->end_trigger_->trigger(); }); | ||||||
|       break; |       break; | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ enum VoiceAssistantFeature : uint32_t { | |||||||
|   FEATURE_SPEAKER = 1 << 1, |   FEATURE_SPEAKER = 1 << 1, | ||||||
|   FEATURE_API_AUDIO = 1 << 2, |   FEATURE_API_AUDIO = 1 << 2, | ||||||
|   FEATURE_TIMERS = 1 << 3, |   FEATURE_TIMERS = 1 << 3, | ||||||
|  |   FEATURE_ANNOUNCE = 1 << 4, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum class State { | enum class State { | ||||||
| @@ -136,6 +137,12 @@ class VoiceAssistant : public Component { | |||||||
|       flags |= VoiceAssistantFeature::FEATURE_TIMERS; |       flags |= VoiceAssistantFeature::FEATURE_TIMERS; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | #ifdef USE_MEDIA_PLAYER | ||||||
|  |     if (this->media_player_ != nullptr) { | ||||||
|  |       flags |= VoiceAssistantFeature::FEATURE_ANNOUNCE; | ||||||
|  |     } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|     return flags; |     return flags; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -209,6 +216,7 @@ class VoiceAssistant : public Component { | |||||||
|   void set_state_(State state); |   void set_state_(State state); | ||||||
|   void set_state_(State state, State desired_state); |   void set_state_(State state, State desired_state); | ||||||
|   void signal_stop_(); |   void signal_stop_(); | ||||||
|  |   void start_playback_timeout_(); | ||||||
|  |  | ||||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; |   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||||
|   struct sockaddr_storage dest_addr_; |   struct sockaddr_storage dest_addr_; | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ WaveshareEPaper = waveshare_epaper_ns.class_("WaveshareEPaper", WaveshareEPaperB | |||||||
| WaveshareEPaperBWR = waveshare_epaper_ns.class_( | WaveshareEPaperBWR = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaperBWR", WaveshareEPaperBase |     "WaveshareEPaperBWR", WaveshareEPaperBase | ||||||
| ) | ) | ||||||
|  | WaveshareEPaper7C = waveshare_epaper_ns.class_("WaveshareEPaper7C", WaveshareEPaperBase) | ||||||
| WaveshareEPaperTypeA = waveshare_epaper_ns.class_( | WaveshareEPaperTypeA = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaperTypeA", WaveshareEPaper |     "WaveshareEPaperTypeA", WaveshareEPaper | ||||||
| ) | ) | ||||||
| @@ -52,21 +53,32 @@ WaveshareEPaper2P9InV2R2 = waveshare_epaper_ns.class_( | |||||||
|     "WaveshareEPaper2P9InV2R2", WaveshareEPaper |     "WaveshareEPaper2P9InV2R2", WaveshareEPaper | ||||||
| ) | ) | ||||||
| GDEW029T5 = waveshare_epaper_ns.class_("GDEW029T5", WaveshareEPaper) | GDEW029T5 = waveshare_epaper_ns.class_("GDEW029T5", WaveshareEPaper) | ||||||
|  | GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) | ||||||
| WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( | WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper2P9InDKE", WaveshareEPaper |     "WaveshareEPaper2P9InDKE", WaveshareEPaper | ||||||
| ) | ) | ||||||
|  | GDEY042T81 = waveshare_epaper_ns.class_("GDEY042T81", WaveshareEPaper) | ||||||
|  | WaveshareEPaper2P9InD = waveshare_epaper_ns.class_( | ||||||
|  |     "WaveshareEPaper2P9InD", WaveshareEPaper | ||||||
|  | ) | ||||||
| WaveshareEPaper4P2In = waveshare_epaper_ns.class_( | WaveshareEPaper4P2In = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper4P2In", WaveshareEPaper |     "WaveshareEPaper4P2In", WaveshareEPaper | ||||||
| ) | ) | ||||||
| WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( | WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper4P2InBV2", WaveshareEPaper |     "WaveshareEPaper4P2InBV2", WaveshareEPaper | ||||||
| ) | ) | ||||||
|  | WaveshareEPaper4P2InBV2BWR = waveshare_epaper_ns.class_( | ||||||
|  |     "WaveshareEPaper4P2InBV2BWR", WaveshareEPaperBWR | ||||||
|  | ) | ||||||
| WaveshareEPaper5P8In = waveshare_epaper_ns.class_( | WaveshareEPaper5P8In = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper5P8In", WaveshareEPaper |     "WaveshareEPaper5P8In", WaveshareEPaper | ||||||
| ) | ) | ||||||
| WaveshareEPaper5P8InV2 = waveshare_epaper_ns.class_( | WaveshareEPaper5P8InV2 = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper5P8InV2", WaveshareEPaper |     "WaveshareEPaper5P8InV2", WaveshareEPaper | ||||||
| ) | ) | ||||||
|  | WaveshareEPaper7P3InF = waveshare_epaper_ns.class_( | ||||||
|  |     "WaveshareEPaper7P3InF", WaveshareEPaper7C | ||||||
|  | ) | ||||||
| WaveshareEPaper7P5In = waveshare_epaper_ns.class_( | WaveshareEPaper7P5In = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper7P5In", WaveshareEPaper |     "WaveshareEPaper7P5In", WaveshareEPaper | ||||||
| ) | ) | ||||||
| @@ -88,6 +100,9 @@ WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( | |||||||
| WaveshareEPaper7P5InV2alt = waveshare_epaper_ns.class_( | WaveshareEPaper7P5InV2alt = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper7P5InV2alt", WaveshareEPaper |     "WaveshareEPaper7P5InV2alt", WaveshareEPaper | ||||||
| ) | ) | ||||||
|  | WaveshareEPaper7P5InV2P = waveshare_epaper_ns.class_( | ||||||
|  |     "WaveshareEPaper7P5InV2P", WaveshareEPaper | ||||||
|  | ) | ||||||
| WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( | WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( | ||||||
|     "WaveshareEPaper7P5InHDB", WaveshareEPaper |     "WaveshareEPaper7P5InHDB", WaveshareEPaper | ||||||
| ) | ) | ||||||
| @@ -120,19 +135,24 @@ MODELS = { | |||||||
|     "2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74), |     "2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74), | ||||||
|     "2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), |     "2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), | ||||||
|     "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), |     "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), | ||||||
|     "gdew029t5": ("b", GDEW029T5), |     "gdew029t5": ("c", GDEW029T5), | ||||||
|     "2.70in": ("b", WaveshareEPaper2P7In), |     "2.70in": ("b", WaveshareEPaper2P7In), | ||||||
|     "2.70in-b": ("b", WaveshareEPaper2P7InB), |     "2.70in-b": ("b", WaveshareEPaper2P7InB), | ||||||
|     "2.70in-bv2": ("b", WaveshareEPaper2P7InBV2), |     "2.70in-bv2": ("b", WaveshareEPaper2P7InBV2), | ||||||
|     "2.70inv2": ("b", WaveshareEPaper2P7InV2), |     "2.70inv2": ("b", WaveshareEPaper2P7InV2), | ||||||
|     "2.90in-b": ("b", WaveshareEPaper2P9InB), |     "2.90in-b": ("b", WaveshareEPaper2P9InB), | ||||||
|     "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), |     "2.90in-bv3": ("b", WaveshareEPaper2P9InBV3), | ||||||
|  |     "gdey029t94": ("c", GDEY029T94), | ||||||
|     "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), |     "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2), | ||||||
|  |     "2.90in-d": ("b", WaveshareEPaper2P9InD), | ||||||
|     "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), |     "2.90in-dke": ("c", WaveshareEPaper2P9InDKE), | ||||||
|  |     "gdey042t81": ("c", GDEY042T81), | ||||||
|     "4.20in": ("b", WaveshareEPaper4P2In), |     "4.20in": ("b", WaveshareEPaper4P2In), | ||||||
|     "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), |     "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), | ||||||
|  |     "4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR), | ||||||
|     "5.83in": ("b", WaveshareEPaper5P8In), |     "5.83in": ("b", WaveshareEPaper5P8In), | ||||||
|     "5.83inv2": ("b", WaveshareEPaper5P8InV2), |     "5.83inv2": ("b", WaveshareEPaper5P8InV2), | ||||||
|  |     "7.30in-f": ("b", WaveshareEPaper7P3InF), | ||||||
|     "7.50in": ("b", WaveshareEPaper7P5In), |     "7.50in": ("b", WaveshareEPaper7P5In), | ||||||
|     "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), |     "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), | ||||||
|     "7.50in-bv3": ("b", WaveshareEPaper7P5InBV3), |     "7.50in-bv3": ("b", WaveshareEPaper7P5InBV3), | ||||||
| @@ -140,6 +160,7 @@ MODELS = { | |||||||
|     "7.50in-bc": ("b", WaveshareEPaper7P5InBC), |     "7.50in-bc": ("b", WaveshareEPaper7P5InBC), | ||||||
|     "7.50inv2": ("b", WaveshareEPaper7P5InV2), |     "7.50inv2": ("b", WaveshareEPaper7P5InV2), | ||||||
|     "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), |     "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), | ||||||
|  |     "7.50inv2p": ("c", WaveshareEPaper7P5InV2P), | ||||||
|     "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), |     "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), | ||||||
|     "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), |     "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), | ||||||
|     "2.13inv3": ("c", WaveshareEPaper2P13InV3), |     "2.13inv3": ("c", WaveshareEPaper2P13InV3), | ||||||
|   | |||||||
| @@ -87,7 +87,11 @@ void WaveshareEPaper2P13InV3::send_reset_() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void WaveshareEPaper2P13InV3::setup() { | void WaveshareEPaper2P13InV3::setup() { | ||||||
|   setup_pins_(); |   this->init_internal_(this->get_buffer_length_()); | ||||||
|  |   this->setup_pins_(); | ||||||
|  |   this->spi_setup(); | ||||||
|  |   this->reset_(); | ||||||
|  |  | ||||||
|   delay(20); |   delay(20); | ||||||
|   this->send_reset_(); |   this->send_reset_(); | ||||||
|   // as a one-off delay this is not worth working around. |   // as a one-off delay this is not worth working around. | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -27,10 +27,7 @@ class WaveshareEPaperBase : public display::DisplayBuffer, | |||||||
|  |  | ||||||
|   void update() override; |   void update() override; | ||||||
|  |  | ||||||
|   void setup() override { |   void setup() override; | ||||||
|     this->setup_pins_(); |  | ||||||
|     this->initialize(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void on_safe_shutdown() override; |   void on_safe_shutdown() override; | ||||||
|  |  | ||||||
| @@ -86,6 +83,23 @@ class WaveshareEPaperBWR : public WaveshareEPaperBase { | |||||||
|   uint32_t get_buffer_length_() override; |   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 { | enum WaveshareEPaperTypeAModel { | ||||||
|   WAVESHARE_EPAPER_1_54_IN = 0, |   WAVESHARE_EPAPER_1_54_IN = 0, | ||||||
|   WAVESHARE_EPAPER_1_54_IN_V2, |   WAVESHARE_EPAPER_1_54_IN_V2, | ||||||
| @@ -160,6 +174,7 @@ enum WaveshareEPaperTypeBModel { | |||||||
|   WAVESHARE_EPAPER_2_7_IN_B_V2, |   WAVESHARE_EPAPER_2_7_IN_B_V2, | ||||||
|   WAVESHARE_EPAPER_4_2_IN, |   WAVESHARE_EPAPER_4_2_IN, | ||||||
|   WAVESHARE_EPAPER_4_2_IN_B_V2, |   WAVESHARE_EPAPER_4_2_IN_B_V2, | ||||||
|  |   WAVESHARE_EPAPER_7_3_IN_F, | ||||||
|   WAVESHARE_EPAPER_7_5_IN, |   WAVESHARE_EPAPER_7_5_IN, | ||||||
|   WAVESHARE_EPAPER_7_5_INV2, |   WAVESHARE_EPAPER_7_5_INV2, | ||||||
|   WAVESHARE_EPAPER_7_5_IN_B_V2, |   WAVESHARE_EPAPER_7_5_IN_B_V2, | ||||||
| @@ -247,6 +262,37 @@ class WaveshareEPaper2P7InBV2 : public WaveshareEPaperBWR { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| class GDEW029T5 : public WaveshareEPaper { | class GDEW029T5 : public WaveshareEPaper { | ||||||
|  |  public: | ||||||
|  |   void initialize() override; | ||||||
|  |  | ||||||
|  |   void display() override; | ||||||
|  |  | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   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 GDEY029T94 : public WaveshareEPaper { | ||||||
|  public: |  public: | ||||||
|   void initialize() override; |   void initialize() override; | ||||||
|  |  | ||||||
| @@ -255,9 +301,8 @@ class GDEW029T5 : public WaveshareEPaper { | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|   void deep_sleep() override { |   void deep_sleep() override { | ||||||
|     // COMMAND DEEP SLEEP |     this->command(0x10);  // Enter deep sleep | ||||||
|     this->command(0x07); |     this->data(0x01); | ||||||
|     this->data(0xA5);  // check byte |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -416,6 +461,63 @@ class WaveshareEPaper2P9InDKE : public WaveshareEPaper { | |||||||
|   int get_height_internal() override; |   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 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 { | class WaveshareEPaper4P2In : public WaveshareEPaper { | ||||||
|  public: |  public: | ||||||
|   void initialize() override; |   void initialize() override; | ||||||
| @@ -487,6 +589,34 @@ class WaveshareEPaper4P2InBV2 : public WaveshareEPaper { | |||||||
|   int get_height_internal() override; |   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 { | class WaveshareEPaper5P8In : public WaveshareEPaper { | ||||||
|  public: |  public: | ||||||
|   void initialize() override; |   void initialize() override; | ||||||
| @@ -553,6 +683,39 @@ class WaveshareEPaper5P8InV2 : public WaveshareEPaper { | |||||||
|   int get_height_internal() override; |   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 { | class WaveshareEPaper7P5In : public WaveshareEPaper { | ||||||
|  public: |  public: | ||||||
|   void initialize() override; |   void initialize() override; | ||||||
| @@ -744,6 +907,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 { | class WaveshareEPaper7P5InHDB : public WaveshareEPaper { | ||||||
|  public: |  public: | ||||||
|   void initialize() override; |   void initialize() override; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Constants used by esphome.""" | """Constants used by esphome.""" | ||||||
|  |  | ||||||
| __version__ = "2025.2.0-dev" | __version__ = "2025.3.0-dev" | ||||||
|  |  | ||||||
| ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ||||||
| VALID_SUBSTITUTIONS_CHARACTERS = ( | VALID_SUBSTITUTIONS_CHARACTERS = ( | ||||||
|   | |||||||
| @@ -582,7 +582,7 @@ class EsphomeCore: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def config_dir(self): |     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 |     @property | ||||||
|     def data_dir(self): |     def data_dir(self): | ||||||
|   | |||||||
| @@ -217,6 +217,8 @@ def preload_core_config(config, result) -> str: | |||||||
|     target_platforms = [] |     target_platforms = [] | ||||||
|  |  | ||||||
|     for domain, _ in config.items(): |     for domain, _ in config.items(): | ||||||
|  |         if domain.startswith("."): | ||||||
|  |             continue | ||||||
|         if _is_target_platform(domain): |         if _is_target_platform(domain): | ||||||
|             target_platforms += [domain] |             target_platforms += [domain] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,9 +12,9 @@ pyserial==3.5 | |||||||
| platformio==6.1.16  # When updating platformio, also update Dockerfile | platformio==6.1.16  # When updating platformio, also update Dockerfile | ||||||
| esptool==4.7.0 | esptool==4.7.0 | ||||||
| click==8.1.7 | click==8.1.7 | ||||||
| esphome-dashboard==20241217.1 | esphome-dashboard==20250212.0 | ||||||
| aioesphomeapi==24.6.2 | aioesphomeapi==24.6.2 | ||||||
| zeroconf==0.143.0 | zeroconf==0.144.1 | ||||||
| puremagic==1.27 | puremagic==1.27 | ||||||
| ruamel.yaml==0.18.6 # dashboard_import | ruamel.yaml==0.18.6 # dashboard_import | ||||||
| glyphsets==1.0.0 | glyphsets==1.0.0 | ||||||
|   | |||||||
| @@ -21,6 +21,8 @@ media_player: | |||||||
|       - media_player.pause: |       - media_player.pause: | ||||||
|     on_play: |     on_play: | ||||||
|       - media_player.stop: |       - media_player.stop: | ||||||
|  |       - media_player.stop: | ||||||
|  |           announcement: true | ||||||
|     on_pause: |     on_pause: | ||||||
|       - media_player.toggle: |       - media_player.toggle: | ||||||
|       - wait_until: |       - wait_until: | ||||||
|   | |||||||
| @@ -33,3 +33,73 @@ modbus_controller: | |||||||
|         read_lambda: |- |         read_lambda: |- | ||||||
|           return 42.3; |           return 42.3; | ||||||
|     max_cmd_retries: 0 |     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 | ||||||
|   | |||||||
| @@ -121,6 +121,14 @@ number: | |||||||
|     max_value: 100 |     max_value: 100 | ||||||
|     step: 1 |     step: 1 | ||||||
|  |  | ||||||
|  | valve: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Template Valve" | ||||||
|  |     lambda: |- | ||||||
|  |       return VALVE_OPEN; | ||||||
|  |     optimistic: true | ||||||
|  |     has_position: true | ||||||
|  |  | ||||||
| prometheus: | prometheus: | ||||||
|   include_internal: true |   include_internal: true | ||||||
|   relabel: |   relabel: | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								tests/components/speaker/common-media_player.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tests/components/speaker/common-media_player.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										9
									
								
								tests/components/speaker/media_player.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tests/components/speaker/media_player.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										9
									
								
								tests/components/speaker/media_player.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tests/components/speaker/media_player.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
| @@ -459,6 +459,27 @@ display: | |||||||
|     reset_pin: |     reset_pin: | ||||||
|       allow_other_uses: true |       allow_other_uses: true | ||||||
|       number: ${reset_pin} |       number: ${reset_pin} | ||||||
|  |     full_update_every: 30 | ||||||
|  |     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: |- |     lambda: |- | ||||||
|       it.rectangle(0, 0, it.get_width(), it.get_height()); |       it.rectangle(0, 0, it.get_width(), it.get_height()); | ||||||
|  |  | ||||||
| @@ -501,6 +522,25 @@ display: | |||||||
|     lambda: |- |     lambda: |- | ||||||
|       it.rectangle(0, 0, it.get_width(), it.get_height()); |       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 |   # 5.83 inch displays | ||||||
|   - platform: waveshare_epaper |   - platform: waveshare_epaper | ||||||
|     id: epd_5_83 |     id: epd_5_83 | ||||||
| @@ -674,6 +714,26 @@ display: | |||||||
|     lambda: |- |     lambda: |- | ||||||
|       it.rectangle(0, 0, it.get_width(), it.get_height()); |       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 |   - platform: waveshare_epaper | ||||||
|     id: epd_7_50hdb |     id: epd_7_50hdb | ||||||
|     model: 7.50in-hd-b |     model: 7.50in-hd-b | ||||||
| @@ -713,6 +773,25 @@ display: | |||||||
|     lambda: |- |     lambda: |- | ||||||
|       it.rectangle(0, 0, it.get_width(), it.get_height()); |       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 |   - platform: waveshare_epaper | ||||||
|     model: 2.90in |     model: 2.90in | ||||||
|     spi_id: spi_waveshare_epaper |     spi_id: spi_waveshare_epaper | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user