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 <otto@otto-winter.com>

* Update esphome/components/dfplayer/dfplayer.cpp

Co-Authored-By: Otto Winter <otto@otto-winter.com>

* add set device. fixes

* lint

* Fixes and sync with docs

* add test

* lint

* lint

* lint
This commit is contained in:
Guillermo Ruffino 2019-10-19 16:37:05 -03:00 committed by Otto Winter
parent 18426b71e4
commit af15a4e710
4 changed files with 593 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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<void()> 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<void()> on_finished_playback_callback_;
};
#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \
template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<DFPlayer> { \
public: \
void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \
};
DFPLAYER_SIMPLE_ACTION(NextAction, next)
DFPLAYER_SIMPLE_ACTION(PreviousAction, previous)
template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Parented<DFPlayer> {
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<typename... Ts> class PlayFolderAction : public Action<Ts...>, public Parented<DFPlayer> {
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<typename... Ts> class SetDeviceAction : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(Device, device)
void play(Ts... x) override {
auto device = this->device_.value(x...);
this->parent_->set_device(device);
}
};
template<typename... Ts> class SetVolumeAction : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(uint8_t, volume)
void play(Ts... x) override {
auto volume = this->volume_.value(x...);
this->parent_->set_volume(volume);
}
};
template<typename... Ts> class SetEqAction : public Action<Ts...>, public Parented<DFPlayer> {
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<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> {
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

View file

@ -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<dfplayer::EqPreset>(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'