ESP32-S3-WROOM-1 and DFPlayer Pro

Hi all and thanks for looking.

First major ESPHome project here. I am trying to set up the DFPlayer Pro to play a specific file on doorbell push. I have created a few probes and such with ESP devices, but this is a little different. Background:

  1. ESP32-S3-WROOM-1

  2. DF Robot DFPlayer Pro

  3. Using ESP32 UART0 TX GPIO43 Pin connected to the DFPlayer Pro RX and the ESP32 UART0 RX GPIO44 Pin connected to the DFPlayer Pro TX

  4. Using one of the 3.3v headers and a GND from the board to power the DFPlayer Pro.

Upon powerup, I get appropriate power to the DF and can use the onboard “Play” button to cycle through mp3s on the board. Works without issue.

In Esphome, I have used the standard DFPlayer references in my configuration and create a button in the YAML to expose as an entity in HA.

button:
  - platform: template    
    name: Play
    id: dfplayer_play_button

    # Optional variables:
    icon: "mdi:play"
    on_press:
      - dfplayer.play: 1

This creates the button. When I push it from HA, I can see in the log ESPHome is functioning properly, with the message that the player is playing, “file 1,” but the DFPlayer Pro does not respond.

  • From my understanding, the Pro and Non-Pro versions have commands which are a little different. Looking at the SRC files, I can see the differences.
  • Likewise, the native baud for the Pro is 115200 which is different from 9600 in the Non-Pro version.
  • To rule out the speakers taking too much power from the DFPlayer board to function properly, I also connected the Player with a 5v source and 1K resistors on my ESP32 RX/TX connections with no changed effect.

This brings me to my question. I will need to tinker a bit, but all of this is new for me. I don’t see any examples of using these boards together through hours of searching.

  1. Would it be better to see if I can change the baud in the ESPHome YAML and hope that works? I get an error saying 115200 is not a valid setting (telling me the pro is not integrated with a library), so how would I override this default?
  2. Would it be better to downgrade the native baud in the DFPlayer Pro to 9600? DFRobot talks about being able to make this baud setting change.
  3. Would it be better to copy the DFPlayer Pro Git libraries locally to a custom components ESPHome folder and reference in my YAML? This part would probably require me to write a completely new custom component to work…

What is the best way forward here?

→ or just bite the bullet and buy a mini? Ha!

If you set the baudrate of the uart, you get errors?

uart:
  tx_pin: GPIO43
  rx_pin: GPIO44
  baud_rate: 115200

If yes, post them here.

Yes. My apologies, I’m driving at the moment, but I get a UI error in ESPhome which states that’s not an accepted baud rate for the DFPlayer.

That’s why I was thinking I would need to define my own custom component.

That would not be a problem since you can change the baudrate at runtime.
But quick research results that mini and pro use different protocol.
So you need to make your custom component (or find one).

Agree. I would take the time to look up how to set it at runtime, which I might assume would need an intermediary (or updating the chip settings–not too hard) since it can’t be set in code, but the easiest solution is to use the proven methods in this case.

Even if I were able to intercede while the code was being passed from the esp to the player device, it looks as if the library commands are different and would require porting into a custom device.

In the end, the Player Mini chip is so inexpensive, it just makes the most sense to move forward with a purchase and using the existing library rather than reinvent the wheel. I have other fun projects to implement and this just slows progress. :slight_smile:

Thanks for your input.

I totally agree!
Anyway, to change baudrate on the fly is well documented:

select:
  - id: change_baud_rate
    name: Baud rate
    platform: template
    options:
      - "2400"
      - "9600"
      - "38400"
      - "57600"
      - "115200"
      - "256000"
      - "512000"
      - "921600"
    initial_option: "115200"
    optimistic: true
    restore_value: True
    internal: false
    entity_category: config
    icon: mdi:swap-horizontal
    set_action:
      - lambda: |-
          id(my_uart).flush();
          uint32_t new_baud_rate = stoi(x);
          ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(my_uart).get_baud_rate(), new_baud_rate);
          if (id(my_uart).get_baud_rate() != new_baud_rate) {
            id(my_uart).set_baud_rate(new_baud_rate);
            id(my_uart).load_settings();

Karosm, thanks for posting the code to include in the ESP YAML to change the baud. Nice work. For anyone landing here in the future, his code is missing a closing brace “}” at the end but works as expected and creates a baud rate dropdown entity in your device. Slick option which I had not seen.

This may cause me to spend too much time tomorrow tinkering with porting the DF Pro library over and working it in. Ha! Thanks Karo.

Any luck in porting over the DF Pro library?

I use this with the mqtt-mediaplayer from Troy Fernandez.

Here is the arduino code. I ended up switching my board as the arduino was easier to work with, but assume porting the code to the S3 would only change the libraries and the pinouts. You would need to change and use your own SSID information and MQTT settings. The discovery prefix is often “homeassistant” by default.

#include <SoftwareSerial.h>
#include <DFRobot_DF1201S.h>
#include <WiFiS3.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

// WiFi settings
const char* ssid = "YourSSID";
const char* password = "YourSSIDPassword";
// MQTT settings
const char* mqtt_server = "192.168.X.X";
const int mqtt_port = XXXX;
const char* mqtt_username = "YourMQTTUserName";
const char* mqtt_password = "YourMQTTPassword";
const char* mqtt_client_id = "Doorbell-Chime-DFPlayer";
const char* mqtt_control_topic = "dfplayer/control";
const char* mqtt_response_topic = "dfplayer/response";
const char* mqtt_discovery_prefix = "TheDiscoveryPrefixYouAssignedInHASMQTT";

SoftwareSerial softSerial(2, 3); // RX, TX
DFRobot_DF1201S player;
bool isPaused = false;
unsigned long lastCommandTime = 0;
const unsigned long commandCooldown = 200; // 200ms cooldown
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
unsigned long lastDiscoveryTime = 0;
unsigned long lastStateTime = 0;
const unsigned long discoveryInterval = 30000; // 30s
const unsigned long stateInterval = 10000; // 10s
int publishFailCount = 0;
const int maxFails = 3;

void clearRetainedDiscovery() {
  String discoveryTopic = String(mqtt_discovery_prefix) + "/media_player/mqtt_doorbell_chime/config";
  if (mqttClient.publish(discoveryTopic.c_str(), "", true)) {
    Serial.println("Cleared discovery: " + discoveryTopic);
    publishFailCount = 0;
  } else {
    Serial.println("Failed to clear discovery: " + discoveryTopic + " (rc=" + mqttClient.state() + ")");
    if (++publishFailCount >= maxFails) {
      Serial.println("Max publish failures, forcing reconnect");
      mqttClient.disconnect();
      publishFailCount = 0;
    }
  }
}

void publishDiscovery() {
  DynamicJsonDocument doc(256);
  
  doc["name"] = "MQTT Doorbell Chime";
  doc["unique_id"] = "mqtt_doorbell_chime";
  doc["state_topic"] = mqtt_response_topic;
  doc["command_topic"] = mqtt_control_topic;  // Add this
  
  // This is the key - tell HA how to read the state
  doc["state_value_template"] = "{% if value_json.state == 'playing' %}playing{% else %}idle{% endif %}";
  
  // Device info
  JsonObject device = doc.createNestedObject("device");
  device["identifiers"][0] = "mqtt_doorbell_chime";
  device["name"] = "Doorbell Chime DFPlayer";
  
  String discoveryPayload;
  serializeJson(doc, discoveryPayload);
  String discoveryTopic = String(mqtt_discovery_prefix) + "/media_player/mqtt_doorbell_chime/config";
  
  Serial.println("Discovery payload: " + discoveryPayload);
  if (mqttClient.publish(discoveryTopic.c_str(), discoveryPayload.c_str(), true)) {
    Serial.println("Published discovery: " + discoveryTopic);
  }
}

void publishState() {
  DynamicJsonDocument doc(256);  // Increase size for more data
  doc["command"] = "status";
  doc["status"] = "success";
  doc["state"] = isPaused ? "paused" : "playing";
  doc["file_number"] = player.getCurFileNumber();
  doc["file_name"] = player.getFileName();
  doc["volume"] = player.getVol();
  doc["current_time"] = player.getCurTime();  // Add current playback time
  doc["total_time"] = player.getTotalTime();   // Add total track length
  
  String statePayload;
  serializeJson(doc, statePayload);
  if (mqttClient.publish(mqtt_response_topic, statePayload.c_str(), false)) {
    Serial.println("Published state: " + String(mqtt_response_topic));
    publishFailCount = 0;
  } else {
    Serial.println("Failed to publish state: " + String(mqtt_response_topic) + " (rc=" + mqttClient.state() + ")");
    if (++publishFailCount >= maxFails) {
      Serial.println("Max publish failures, forcing reconnect");
      mqttClient.disconnect();
      publishFailCount = 0;
    }
  }
}

void handleCommand(char c) {
  if (millis() - lastCommandTime < commandCooldown) {
    Serial.println("Command ignored: Cooldown active");
    return;
  }
  lastCommandTime = millis();
  DynamicJsonDocument doc(200);
  doc["command"] = String(c);
  doc["status"] = "success";
  switch (c) {
    case 's':
      {
        int currentFile = player.getCurFileNumber();
        if (player.setPlayTime(0) && player.playFileNum(currentFile)) {
          isPaused = false;
          doc["state"] = "playing";
          doc["file_number"] = currentFile;
          doc["file_name"] = player.getFileName();
          doc["volume"] = player.getVol();
          delay(100);
        } else {
          doc["status"] = "error";
          doc["message"] = "Failed to play";
        }
      }
      break;
    case 'p':
      {
        bool wasPaused = isPaused;
        if (wasPaused) {
          if (player.start()) {
            isPaused = false;
            doc["state"] = "playing";
            doc["file_number"] = player.getCurFileNumber();
            doc["file_name"] = player.getFileName();
            doc["volume"] = player.getVol();
            delay(100);
          } else {
            doc["status"] = "error";
            doc["message"] = "Failed to resume";
          }
        } else {
          if (player.pause()) {
            isPaused = true;
            doc["state"] = "paused";
            doc["volume"] = player.getVol();
            delay(100);
          } else {
            doc["status"] = "error";
            doc["message"] = "Failed to pause";
          }
        }
      }
      break;
    case 'n':
      if (player.setPlayTime(0) && player.next()) {
        isPaused = false;
        doc["state"] = "playing";
        doc["file_number"] = player.getCurFileNumber();
        doc["file_name"] = player.getFileName();
        doc["volume"] = player.getVol();
        delay(100);
      } else {
        doc["status"] = "error";
        doc["message"] = "Failed to play next";
      }
      break;
    case 'l':
      if (player.setPlayTime(0) && player.last()) {
        isPaused = false;
        doc["state"] = "playing";
        doc["file_number"] = player.getCurFileNumber();
        doc["file_name"] = player.getFileName();
        doc["volume"] = player.getVol();
        delay(100);
      } else {
        doc["status"] = "error";
        doc["message"] = "Failed to play previous";
      }
      break;
    case '+':
      {
        uint8_t vol = player.getVol();
        if (vol < 30 && player.setVol(vol + 1)) {
          doc["volume"] = player.getVol();
        } else {
          doc["status"] = "error";
          doc["message"] = vol >= 30 ? "Volume at maximum" : "Failed to increase volume";
        }
      }
      break;
    case '-':
      {
        uint8_t vol = player.getVol();
        if (vol > 0 && player.setVol(vol - 1)) {
          doc["volume"] = player.getVol();
        } else {
          doc["status"] = "error";
          doc["message"] = vol <= 0 ? "Volume at minimum" : "Failed to decrease volume";
        }
      }
      break;
    case 'x':  // Stop command (reset to beginning and pause)
      {
        // First set playback position to beginning
        if (player.setPlayTime(0)) {
          delay(50);  // Give it time to process
          // Then pause
          if (player.pause()) {
            isPaused = true;
            doc["state"] = "stopped";  // or "idle" 
            doc["file_number"] = player.getCurFileNumber();
            doc["file_name"] = player.getFileName();
            doc["volume"] = player.getVol();
            doc["current_time"] = 0;  // We're at the beginning
            doc["total_time"] = player.getTotalTime();
            delay(100);
          } else {
            doc["status"] = "error";
            doc["message"] = "Failed to pause after reset";
          }
        } else {
          doc["status"] = "error";
          doc["message"] = "Failed to reset position";
        }
      }
      break;
    case 't':  // Get time info
      {
        doc["state"] = isPaused ? "paused" : "playing";
        doc["file_number"] = player.getCurFileNumber();
        doc["file_name"] = player.getFileName();
        doc["current_time"] = player.getCurTime();  // Time played in seconds
        doc["total_time"] = player.getTotalTime();   // Total track length in seconds
        doc["volume"] = player.getVol();
        delay(100);
      }
      break;
    case 'i':  // Get full info
      {
        doc["state"] = isPaused ? "paused" : "playing";
        doc["file_number"] = player.getCurFileNumber();
        doc["file_name"] = player.getFileName();
        doc["current_time"] = player.getCurTime();
        doc["total_time"] = player.getTotalTime();
        doc["total_files"] = player.getTotalFile();
        doc["volume"] = player.getVol();
        delay(100);
      }
      break;
    default:
      doc["status"] = "error";
      doc["message"] = "Unknown command";
      break;
  }
  String response;
  serializeJson(doc, response);
  mqttClient.publish(mqtt_response_topic, response.c_str(), false);
  Serial.println("Command response: " + response);
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
  if (length > 0) {
    handleCommand((char)payload[0]);
  }
}

void reconnect() {
  while (!mqttClient.connected()) {
    Serial.print("Connecting to MQTT: ");
    Serial.print(mqtt_server);
    Serial.print(":");
    Serial.println(mqtt_port);
    mqttClient.setBufferSize(512);
    mqttClient.setKeepAlive(60);
    if (mqttClient.connect(mqtt_client_id, mqtt_username, mqtt_password)) {
      Serial.println("Connected to MQTT");
      mqttClient.subscribe(mqtt_control_topic);
      Serial.println("Subscribed to: " + String(mqtt_control_topic));
      clearRetainedDiscovery();
      publishDiscovery();
      publishState();
      publishFailCount = 0;
    } else {
      Serial.println("Connection failed, rc=" + String(mqttClient.state()) + ", retrying in 5s");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  while (!Serial);
  softSerial.begin(115200);
  if (!player.begin(softSerial)) {
    Serial.println("Serial init failed!");
    while (1);
  }
  Serial.println("Serial init OK");
  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  int wifi_attempts = 0;
  while (WiFi.status() != WL_CONNECTED && wifi_attempts < 20) {
    delay(500);
    Serial.print(".");
    wifi_attempts++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nWiFi connected, IP: " + WiFi.localIP().toString());
  } else {
    Serial.println("\nWiFi failed");
    while (1);
  }
  mqttClient.setServer(mqtt_server, mqtt_port);
  mqttClient.setCallback(mqttCallback);
  reconnect();
  player.switchFunction(player.MUSIC);
  player.setVol(5);
  player.setPlayMode(player.SINGLE); // Play file once and stop
  player.start();
  isPaused = false;
  Serial.println("Player ready, volume: " + String(player.getVol()) + ", files: " + String(player.getTotalFile()));
}

void loop() {
  if (!mqttClient.connected()) {
    reconnect();
  }
  mqttClient.loop();
  if (millis() - lastDiscoveryTime >= discoveryInterval && mqttClient.connected()) {
    publishDiscovery();
    lastDiscoveryTime = millis();
  }
  if (millis() - lastStateTime >= stateInterval && mqttClient.connected()) {
    publishState();
    lastStateTime = millis();
  }
}

Here is my mqtt media player YAML. This is a stand-alone file referenced in my configuration.yaml:

# MQTT Doorbell Chime Media Player Entity

- platform: mqtt-mediaplayer
  name: "MQTT Doorbell Chime"
  topic:
    player_status: "{{ state_attr('sensor.mqtt_doorbell_chime_state', 'state') }}"
    song_title: "{{ state_attr('sensor.mqtt_doorbell_chime_state', 'file_name') | default('MQTT Doorbell') }}"
    song_volume: "{{ state_attr('sensor.mqtt_doorbell_chime_state', 'volume') | default(5) }}"
  status_keyword: "playing"
  play:
    service: mqtt.publish
    data:
      topic: "dfplayer/control"
      payload: "s"
  pause:
    service: mqtt.publish
    data:
      topic: "dfplayer/control"
      payload: "p"
  # Removed turn_off - no power button
  next:
    service: mqtt.publish
    data:
      topic: "dfplayer/control"
      payload: "n"
  previous:
    service: mqtt.publish
    data:
      topic: "dfplayer/control"
      payload: "l"
  vol_up:
    service: mqtt.publish
    data:
      topic: "dfplayer/control"
      payload: "+"
  vol_down:
    service: mqtt.publish
    data:
      topic: "dfplayer/control"
      payload: "-"

Here are the sensors I created to work with the player MQTT messages.

sensor:
  - name: "MQTT Doorbell Chime State"
    unique_id: "mqtt_doorbell_chime_state"
    state_topic: "dfplayer/response"
    value_template: "{{ value_json.state if value_json is defined else 'unknown' }}"
    json_attributes_topic: "dfplayer/response"
    json_attributes_template: "{{ value_json | tojson if value_json is defined else {} }}"

  - name: "MQTT Doorbell Chime Volume"
    unique_id: "mqtt_doorbell_chime_volume"
    state_topic: "dfplayer/response"
    value_template: "{{ value_json.volume if value_json is defined else 0 }}"
    unit_of_measurement: ""
    icon: mdi:volume-high

  - name: "MQTT Doorbell Current File"
    unique_id: "mqtt_doorbell_current_file"
    state_topic: "dfplayer/response"
    value_template: "{{ value_json.file_number if value_json is defined else 0 }}"
    icon: mdi:file-music

  - name: "MQTT Doorbell Chime Progress"
    unique_id: "mqtt_doorbell_chime_progress"
    state_topic: "dfplayer/response"
    value_template: >
      {% if value_json.current_time is defined and value_json.total_time is defined and value_json.total_time > 0 %}
        {{ ((value_json.current_time / value_json.total_time) * 100) | round(0) }}
      {% else %}
        0
      {% endif %}
    unit_of_measurement: "%"
    icon: mdi:percent

  - name: "MQTT Doorbell Chime Time"
    unique_id: "mqtt_doorbell_chime_time"
    state_topic: "dfplayer/response"
    value_template: >
      {% if value_json.current_time is defined %}
        {{ value_json.current_time | int | timestamp_custom('%M:%S', false) }}
      {% else %}
        00:00
      {% endif %}
    json_attributes_topic: "dfplayer/response"
    json_attributes_template: >
      {
        "total_time": "{% if value_json.total_time is defined %}{{ value_json.total_time | int | timestamp_custom('%M:%S', false) }}{% else %}00:00{% endif %}",
        "current_seconds": {{ value_json.current_time | default(0) }},
        "total_seconds": {{ value_json.total_time | default(0) }}
      }
    icon: mdi:timer

Here are the scripts to tie it all together:

mqtt_doorbell_chime_pause_resume:
  alias: MQTT Doorbell Chime Pause/Resume
  description: Pauses or resumes the doorbell chime via MQTT
  sequence:
    - action: mqtt.publish
      data:
        topic: dfplayer/control
        payload: p
        qos: "1"
        retain: false
  icon: mdi:play-pause
mqtt_doorbell_chime_stop:
  alias: MQTT Doorbell Chime Stop
  description: Stops playback (resets to beginning and pauses)
  sequence:
    - action: mqtt.publish
      data:
        topic: dfplayer/control
        payload: x
    - action: input_boolean.turn_off
      data:
        entity_id: input_boolean.mqtt_doorbell_chime_playing
  icon: mdi:stop-circle-outline
mqtt_doorbell_chime_get_time:
  alias: MQTT Doorbell Chime Get Time Info
  description: Gets current playback time and total track length
  sequence:
    - action: mqtt.publish
      data:
        topic: dfplayer/control
        payload: t
  icon: mdi:timer
mqtt_doorbell_chime_get_info:
  alias: MQTT Doorbell Chime Get Full Info
  description: Gets all current player information
  sequence:
    - action: mqtt.publish
      data:
        topic: dfplayer/control
        payload: i
  icon: mdi:information

And finally, the YAML in the dashboard to create the player controls:

type: grid
cards:
  - type: vertical-stack
    cards:
      - type: horizontal-stack
        cards:
          - type: button
            icon: mdi:skip-previous
            name: Previous
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: l
          - type: button
            icon: mdi:play
            name: Play
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: s
          - type: button
            icon: mdi:pause
            name: Pause
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: p
          - type: button
            icon: mdi:stop-circle-outline
            name: Stop
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: x
          - type: button
            icon: mdi:skip-next
            name: Next
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: "n"
      - type: entities
        title: MQTT Doorbell Chime
        entities:
          - type: attribute
            entity: sensor.mqtt_doorbell_chime_state
            attribute: file_name
            name: Now Playing
            icon: mdi:music-note
          - entity: sensor.mqtt_doorbell_current_file
            name: File Number
          - entity: sensor.mqtt_doorbell_chime_state
            name: Status
          - entity: sensor.mqtt_doorbell_chime_time
            name: Current Time
          - entity: sensor.mqtt_doorbell_chime_progress
            name: Progress
          - entity: sensor.mqtt_doorbell_chime_volume
            name: Volume
      - type: horizontal-stack
        cards:
          - type: button
            icon: mdi:volume-minus
            name: Volume -
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: "-"
          - type: button
            icon: mdi:volume-plus
            name: Volume +
            tap_action:
              action: call-service
              service: mqtt.publish
              data:
                topic: dfplayer/control
                payload: +
          - type: button
            entity: script.mqtt_doorbell_chime_get_time
            icon: mdi:timer-refresh
            name: Update

I use this simply as an unwired doorbell chime. If you want it to be a fully functioning media player, you might make some tweaks to this code.

1 Like