mirror of
https://github.com/esphome/esphome.git
synced 2025-04-13 22:30:31 +01:00
Merge branch 'esphome:dev' into mcp4461
This commit is contained in:
commit
c0fab3999e
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user