From af15a4e71053f9bbe38a5686b632f5a8b1e964e6 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 19 Oct 2019 16:37:05 -0300 Subject: [PATCH] Add dfplayer mini component (#655) * Add dfplayer mini component * receiving some data * implemented many actions * lint * undo homeassistant_time.h * Update esphome/components/dfplayer/__init__.py Co-Authored-By: Otto Winter * Update esphome/components/dfplayer/dfplayer.cpp Co-Authored-By: Otto Winter * add set device. fixes * lint * Fixes and sync with docs * add test * lint * lint * lint --- esphome/components/dfplayer/__init__.py | 221 +++++++++++++++++++++++ esphome/components/dfplayer/dfplayer.cpp | 120 ++++++++++++ esphome/components/dfplayer/dfplayer.h | 165 +++++++++++++++++ tests/test3.yaml | 87 +++++++++ 4 files changed, 593 insertions(+) create mode 100644 esphome/components/dfplayer/__init__.py create mode 100644 esphome/components/dfplayer/dfplayer.cpp create mode 100644 esphome/components/dfplayer/dfplayer.h diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py new file mode 100644 index 0000000000..78bcfa80f4 --- /dev/null +++ b/esphome/components/dfplayer/__init__.py @@ -0,0 +1,221 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +dfplayer_ns = cg.esphome_ns.namespace('dfplayer') +DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) +DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_('DFPlayerFinishedPlaybackTrigger', + automation.Trigger.template()) +DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', automation.Condition) + +MULTI_CONF = True +CONF_FOLDER = 'folder' +CONF_FILE = 'file' +CONF_LOOP = 'loop' +CONF_VOLUME = 'volume' +CONF_DEVICE = 'device' +CONF_EQ_PRESET = 'eq_preset' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +EqPreset = dfplayer_ns.enum("EqPreset") +EQ_PRESET = { + 'NORMAL': EqPreset.NORMAL, + 'POP': EqPreset.POP, + 'ROCK': EqPreset.ROCK, + 'JAZZ': EqPreset.JAZZ, + 'CLASSIC': EqPreset.CLASSIC, + 'BASS': EqPreset.BASS, +} +Device = dfplayer_ns.enum("Device") +DEVICE = { + 'USB': Device.USB, + 'TF_CARD': Device.TF_CARD, +} + +NextAction = dfplayer_ns.class_('NextAction', automation.Action) +PreviousAction = dfplayer_ns.class_('PreviousAction', automation.Action) +PlayFileAction = dfplayer_ns.class_('PlayFileAction', automation.Action) +PlayFolderAction = dfplayer_ns.class_('PlayFolderAction', automation.Action) +SetVolumeAction = dfplayer_ns.class_('SetVolumeAction', automation.Action) +SetEqAction = dfplayer_ns.class_('SetEqAction', automation.Action) +SleepAction = dfplayer_ns.class_('SleepAction', automation.Action) +ResetAction = dfplayer_ns.class_('ResetAction', automation.Action) +StartAction = dfplayer_ns.class_('StartAction', automation.Action) +PauseAction = dfplayer_ns.class_('PauseAction', automation.Action) +StopAction = dfplayer_ns.class_('StopAction', automation.Action) +RandomAction = dfplayer_ns.class_('RandomAction', automation.Action) +SetDeviceAction = dfplayer_ns.class_('SetDeviceAction', automation.Action) + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(DFPlayer), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DFPlayerFinishedPlaybackTrigger), + }), +}).extend(uart.UART_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('dfplayer.play_next', NextAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_next_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play_previous', PreviousAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_previous_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play', PlayFileAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +}, key=CONF_FILE)) +def dfplayer_play_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.play_folder', PlayFolderAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FOLDER): cv.templatable(cv.int_), + cv.Optional(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +})) +def dfplayer_play_folder_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FOLDER], args, float) + cg.add(var.set_folder(template_)) + if CONF_FILE in config: + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.set_device', SetDeviceAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True), +}, key=CONF_DEVICE)) +def dfplayer_set_device_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_DEVICE], args, Device) + cg.add(var.set_device(template_)) + yield var + + +@automation.register_action('dfplayer.set_volume', SetVolumeAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_VOLUME): cv.templatable(cv.int_), +}, key=CONF_VOLUME)) +def dfplayer_set_volume_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_VOLUME], args, float) + cg.add(var.set_volume(template_)) + yield var + + +@automation.register_action('dfplayer.set_eq', SetEqAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)), +}, key=CONF_EQ_PRESET)) +def dfplayer_set_eq_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset) + cg.add(var.set_eq(template_)) + yield var + + +@automation.register_action('dfplayer.sleep', SleepAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_sleep_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.reset', ResetAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.start', StartAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_start_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.pause', PauseAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_pause_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.random', RandomAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_random_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('dfplayer.is_playing', DFPlayerIsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp new file mode 100644 index 0000000000..9589a907c4 --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -0,0 +1,120 @@ +#include "dfplayer.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dfplayer { + +static const char* TAG = "dfplayer"; + +void DFPlayer::play_folder(uint16_t folder, uint16_t file) { + if (folder < 100 && file < 256) { + this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); + } else if (folder <= 10 && file <= 1000) { + this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); + } else { + ESP_LOGE(TAG, "Cannot play folder %d file %d.", folder, file); + } +} + +void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { + uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef}; + uint16_t checksum = 0; + for (uint8_t i = 1; i < 7; i++) + checksum += buffer[i]; + checksum = -checksum; + buffer[7] = checksum >> 8; + buffer[8] = (uint8_t) checksum; + + this->sent_cmd_ = cmd; + + ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument); + this->write_array(buffer, 10); +} + +void DFPlayer::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + switch (this->read_pos_) { + case 0: // Start mark + if (byte != 0x7E) + continue; + break; + case 1: // Version + if (byte != 0xFF) { + ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 2: // Buffer length + if (byte != 0x06) { + ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 9: // End byte + if (byte != 0xEF) { + ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + // Parse valid received command + uint8_t cmd = this->read_buffer_[3]; + uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; + + ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); + + switch (cmd) { + case 0x3A: + if (argument == 1) { + ESP_LOGI(TAG, "USB loaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card loaded"); + break; + case 0x3B: + if (argument == 1) { + ESP_LOGI(TAG, "USB unloaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card unloaded"); + break; + case 0x3F: + if (argument == 1) { + ESP_LOGI(TAG, "USB available"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card available"); + } else if (argument == 3) { + ESP_LOGI(TAG, "USB, TF Card available"); + } + break; + case 0x41: + ESP_LOGV(TAG, "Ack ok"); + this->is_playing_ |= this->ack_set_is_playing_; + this->is_playing_ &= !this->ack_reset_is_playing_; + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + break; + case 0x3D: // Playback finished + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + default: + ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument); + } + this->sent_cmd_ = 0; + this->read_pos_ = 0; + continue; + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + } +} + +} // namespace dfplayer +} // namespace esphome diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h new file mode 100644 index 0000000000..2cb9eea4eb --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.h @@ -0,0 +1,165 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +const size_t DFPLAYER_READ_BUFFER_LENGTH = 25; // two messages + some extra + +namespace esphome { +namespace dfplayer { + +enum EqPreset { + NORMAL = 0, + POP = 1, + ROCK = 2, + JAZZ = 3, + CLASSIC = 4, + BASS = 5, +}; + +enum Device { + USB = 1, + TF_CARD = 2, +}; + +class DFPlayer : public uart::UARTDevice, public Component { + public: + void loop() override; + + void next() { this->send_cmd_(0x01); } + void previous() { this->send_cmd_(0x02); } + void play_file(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x03, file); + } + void play_file_loop(uint16_t file) { this->send_cmd_(0x08, file); } + void play_folder(uint16_t folder, uint16_t file); + void play_folder_loop(uint16_t folder) { this->send_cmd_(0x17, folder); } + void volume_up() { this->send_cmd_(0x04); } + void volume_down() { this->send_cmd_(0x05); } + void set_device(Device device) { this->send_cmd_(0x09, device); } + void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); } + void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); } + void sleep() { this->send_cmd_(0x0A); } + void reset() { this->send_cmd_(0x0C); } + void start() { this->send_cmd_(0x0D); } + void pause() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x0E); + } + void stop() { this->send_cmd_(0x16); } + void random() { this->send_cmd_(0x18); } + + bool is_playing() { return is_playing_; } + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + void send_cmd_(uint8_t cmd, uint16_t argument = 0); + void send_cmd_(uint8_t cmd, uint16_t high, uint16_t low) { + this->send_cmd_(cmd, ((high & 0xFF) << 8) | (low & 0xFF)); + } + uint8_t sent_cmd_{0}; + + char read_buffer_[DFPLAYER_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + bool is_playing_{false}; + bool ack_set_is_playing_{false}; + bool ack_reset_is_playing_{false}; + + CallbackManager on_finished_playback_callback_; +}; + +#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ + template class ACTION_CLASS : public Action, public Parented { \ + public: \ + void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + }; + +DFPLAYER_SIMPLE_ACTION(NextAction, next) +DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) + +template class PlayFileAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_file_loop(file); + } else { + this->parent_->play_file(file); + } + } +}; + +template class PlayFolderAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, folder) + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto folder = this->folder_.value(x...); + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_folder_loop(folder); + } else { + this->parent_->play_folder(folder, file); + } + } +}; + +template class SetDeviceAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { + auto device = this->device_.value(x...); + this->parent_->set_device(device); + } +}; + +template class SetVolumeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { + auto volume = this->volume_.value(x...); + this->parent_->set_volume(volume); + } +}; + +template class SetEqAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { + auto eq = this->eq_.value(x...); + this->parent_->set_eq(eq); + } +}; + +DFPLAYER_SIMPLE_ACTION(SleepAction, sleep) +DFPLAYER_SIMPLE_ACTION(ResetAction, reset) +DFPLAYER_SIMPLE_ACTION(StartAction, start) +DFPLAYER_SIMPLE_ACTION(PauseAction, pause) +DFPLAYER_SIMPLE_ACTION(StopAction, stop) +DFPLAYER_SIMPLE_ACTION(RandomAction, random) + +template class DFPlayerIsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class DFPlayerFinishedPlaybackTrigger : public Trigger<> { + public: + explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace dfplayer +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index a7e8d26168..df4afe5ad9 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -60,6 +60,83 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + - service: dfplayer_next + then: + - dfplayer.play_next: + - service: dfplayer_previous + then: + - dfplayer.play_previous: + - service: dfplayer_play + variables: + file: int + then: + - dfplayer.play: !lambda 'return file;' + - service: dfplayer_play_loop + variables: + file: int + loop_: bool + then: + - dfplayer.play: + file: !lambda 'return file;' + loop: !lambda 'return loop_;' + - service: dfplayer_play_folder + variables: + folder: int + file: int + then: + - dfplayer.play_folder: + folder: !lambda 'return folder;' + file: !lambda 'return file;' + + - service: dfplayer_play_loo_folder + variables: + folder: int + then: + - dfplayer.play_folder: + folder: !lambda 'return folder;' + loop: True + + - service: dfplayer_set_device + variables: + device: int + then: + - dfplayer.set_device: + device: TF_CARD + + - service: dfplayer_set_volume + variables: + volume: int + then: + - dfplayer.set_volume: !lambda 'return volume;' + - service: dfplayer_set_eq + variables: + preset: int + then: + - dfplayer.set_eq: !lambda 'return static_cast(preset);' + + - service: dfplayer_sleep + then: + - dfplayer.sleep + + - service: dfplayer_reset + then: + - dfplayer.reset + + - service: dfplayer_start + then: + - dfplayer.start + + - service: dfplayer_pause + then: + - dfplayer.pause + + - service: dfplayer_stop + then: + - dfplayer.stop + + - service: dfplayer_random + then: + - dfplayer.random wifi: ssid: 'MySSID' @@ -532,3 +609,13 @@ sim800l: - sim800l.send_sms: message: 'hello you' recipient: '+1234' + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: + dfplayer.is_playing + then: + logger.log: 'Playback finished event'