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.