From 6de30126a9013e69e3cc759a5c4e9ed531a69217 Mon Sep 17 00:00:00 2001 From: aaronk6 Date: Sat, 16 Mar 2024 17:40:59 +0100 Subject: [PATCH 1/6] MQTT: Add is_active and is _playing --- mqtt.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mqtt.c b/mqtt.c index f662009d..fd172161 100644 --- a/mqtt.c +++ b/mqtt.c @@ -21,6 +21,10 @@ struct mosquitto *global_mosq = NULL; char *topic = NULL; int connected = 0; +// track active/playing state +int is_active = 0; +int is_playing = 0; + // mosquitto logging void _cb_log(__attribute__((unused)) struct mosquitto *mosq, __attribute__((unused)) void *userdata, int level, const char *str) { @@ -167,12 +171,16 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le } else if (type == 'ssnc') { switch (code) { case 'abeg': + is_active = 1; + mqtt_publish("is_active", "1", 1); mqtt_publish("active_start", data, length); break; case 'acre': mqtt_publish("active_remote_id", data, length); break; case 'aend': + is_active = 0; + mqtt_publish("is_active", "0", 1); mqtt_publish("active_end", data, length); break; case 'asal': @@ -210,9 +218,13 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le mqtt_publish("output_frame_rate", data, length); break; case 'pbeg': + is_playing = 1; + mqtt_publish("is_playing", "1", 1); mqtt_publish("play_start", data, length); break; case 'pend': + is_playing = 0; + mqtt_publish("is_playing", "0", 1); mqtt_publish("play_end", data, length); break; case 'pfls': @@ -224,6 +236,8 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le } break; case 'prsm': + is_playing = 1; + mqtt_publish("is_playing", "1", 1); mqtt_publish("play_resume", data, length); break; case 'pvol': From cda315b6aac424656cf7155a0adb2ffc5b4d4060 Mon Sep 17 00:00:00 2001 From: aaronk6 Date: Sun, 17 Mar 2024 13:12:55 +0100 Subject: [PATCH 2/6] Add support for MQTT autodiscovery --- common.h | 2 ++ mqtt.c | 46 +++++++++++++++++++++++++++++++++++++ scripts/shairport-sync.conf | 2 ++ shairport.c | 5 ++++ 4 files changed, 55 insertions(+) diff --git a/common.h b/common.h index 9de26f63..6a341f7b 100644 --- a/common.h +++ b/common.h @@ -175,6 +175,8 @@ typedef struct { int mqtt_publish_parsed; int mqtt_publish_cover; int mqtt_enable_remote; + int mqtt_enable_autodiscovery; + char *mqtt_autodiscovery_prefix; char *mqtt_empty_payload_substitute; #endif uint8_t ap1_prefix[6]; diff --git a/mqtt.c b/mqtt.c index fd172161..2b9bd5b2 100644 --- a/mqtt.c +++ b/mqtt.c @@ -94,6 +94,52 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, snprintf(remotetopic, strlen(config.mqtt_topic) + 8, "%s/remote", config.mqtt_topic); mosquitto_subscribe(mosq, NULL, remotetopic, 0); } + + // send autodiscovery messages if enabled + if (config.mqtt_enable_autodiscovery && config.mqtt_publish_parsed) { + send_autodiscovery_messages(mosq); + } +} + +void send_autodiscovery_messages(struct mosquitto *mosq) { + const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL) ? + config.autodiscovery_prefix : "homeassistant"; + char base_topic[256]; + snprintf(base_topic, sizeof(base_topic), "%s/sensor/shairport-sync-", autodiscovery_prefix); + + char full_topic[256]; + char payload[1024]; + const char *device_name = config.service_name; + + const char *sensors[] = { + "artist", "album", "title", "genre", "format", "track_id", + "client_ip", "client_name", "volume", "is_active", "is_playing", NULL + }; + // Human-readable names + const char *sensor_names[] = { + "Artist", "Album", "Title", "Genre", "Format", "Track ID", + "Client IP", "Client Name", "Volume", "Is Active", "Is Playing", NULL + }; + + for (int i = 0; sensors[i] != NULL; i++) { + snprintf(full_topic, sizeof(full_topic), "%s%s/config", base_topic, sensors[i]); + snprintf(payload, sizeof(payload), + "{\n" + " \"name\": \"%s\",\n" + " \"state_topic\": \"%s/%s\",\n" + " \"icon\": \"mdi:music-note\",\n" + " \"unique_id\": \"shairport-sync-%s\",\n" + " \"device\": {\n" + " \"identifiers\": [\"shairport-sync\"],\n" + " \"name\": \"%s\",\n" + " \"model\": \"Shairport-Sync\",\n" + " \"manufacturer\": \"shairport-sync\"\n" + " }\n" + "}", sensor_names[i], config.mqtt_topic, sensors[i], sensors[i], device_name); + + mosquitto_publish(mosq, NULL, full_topic, strlen(payload), payload, 0, true); + debug(2, "[MQTT Autodiscovery]: Published autodiscovery for %s", sensor_names[i]); + } } // helper function to publish under a topic and automatically append the main topic diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 517a7560..8423b807 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -285,6 +285,8 @@ mqtt = // Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip, // Additionally, messages at the topics play_start,play_end,play_flush,play_resume are published // publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker +// enable_autodiscovery = "no"; //whether to publish an autodiscovery message to automatically appear in Home Assistant +// autodiscovery_prefix = "homeassistant"; //string to prepend to autodiscovery topic // enable_remote = "no"; //whether to remote control via MQTT. RC is available under `topic`/remote. // Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup" }; diff --git a/shairport.c b/shairport.c index 1060e0b7..87ac0ea7 100644 --- a/shairport.c +++ b/shairport.c @@ -1300,6 +1300,10 @@ int parse_options(int argc, char **argv) { if (config.mqtt_publish_cover && !config.get_coverart) { die("You need to have metadata.include_cover_art enabled in order to use mqtt.publish_cover"); } + config_set_lookup_bool(config.cfg, "mqtt.enable_autodiscovery", &config.mqtt_enable_autodiscovery); + if (config_lookup_string(config.cfg, "mqtt.autodiscovery_prefix", &str)) { + config.mqtt_autodiscovery_prefix = (char *)str; + } config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote); if (config_lookup_string(config.cfg, "mqtt.empty_payload_substitute", &str)) { if (strlen(str) == 0) @@ -2550,6 +2554,7 @@ int main(int argc, char **argv) { debug(1, "mqtt will%s publish parsed metadata.", config.mqtt_publish_parsed ? "" : " not"); debug(1, "mqtt will%s publish cover Art.", config.mqtt_publish_cover ? "" : " not"); debug(1, "mqtt remote control is %sabled.", config.mqtt_enable_remote ? "en" : "dis"); + debug(1, "mqtt autodiscovery is %sabled.", config.mqtt_enable_autodiscovery ? "en" : "dis"); #endif #ifdef CONFIG_CONVOLUTION From b4f160e3201295bd9e640bd3ca9258c4ac302c8e Mon Sep 17 00:00:00 2001 From: aaronk6 Date: Sun, 17 Mar 2024 19:26:18 +0100 Subject: [PATCH 3/6] MQTT autodiscovery (WIP) --- mqtt.c | 105 ++++++++++++++++++++++++++++++++++++++++++--------------- mqtt.h | 1 + 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/mqtt.c b/mqtt.c index 2b9bd5b2..51e474d8 100644 --- a/mqtt.c +++ b/mqtt.c @@ -102,43 +102,92 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, } void send_autodiscovery_messages(struct mosquitto *mosq) { - const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL) ? - config.autodiscovery_prefix : "homeassistant"; - char base_topic[256]; - snprintf(base_topic, sizeof(base_topic), "%s/sensor/shairport-sync-", autodiscovery_prefix); - - char full_topic[256]; - char payload[1024]; const char *device_name = config.service_name; + const char *sw_version = get_version_string(); + const char *model = "shairport-sync"; + const char *manufacturer = "Mike Brady"; + const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL) ? + config.mqtt_autodiscovery_prefix : "homeassistant"; + char topic[512]; + char payload[1024]; + char device_payload[512]; + + snprintf(device_payload, sizeof(device_payload), + "\"device\": {" + "\"identifiers\": [\"%s\"]," + "\"name\": \"%s\"," + "\"model\": \"%s\"," + "\"sw_version\": \"%s\"," + "\"manufacturer\": \"%s\"" + "}", + model, device_name, model, sw_version, manufacturer); + + // when adding sensors here, be sure to also update sensor_names and icons below! const char *sensors[] = { - "artist", "album", "title", "genre", "format", "track_id", - "client_ip", "client_name", "volume", "is_active", "is_playing", NULL + "artist", + "album", + "title", + "genre", + "format", + "track_id", + "client_ip", + "client_name", + "volume", + "is_active", + "is_playing", + NULL }; - // Human-readable names + const char *sensor_names[] = { - "Artist", "Album", "Title", "Genre", "Format", "Track ID", - "Client IP", "Client Name", "Volume", "Is Active", "Is Playing", NULL + "Artist", + "Album", + "Title", + "Genre", + "Format", + "Track ID", + "Client IP", + "Client Name", + "Volume", + "Is Active", + "Is Playing" + }; + + const char *icons[] = { + "mdi:account-music", // artist + "mdi:album", // album + "mdi:music", // title + "mdi:music-box-multiple", // genre + "mdi:file", // format + "mdi:identifier", // track ID + "mdi:ip", // client IP + "mdi:cellphone-wireless", // client name + "mdi:volume-high", // volume + "mdi:play-box-multiple", // is active + "mdi:play-box-multiple-outline" // is playing }; for (int i = 0; sensors[i] != NULL; i++) { - snprintf(full_topic, sizeof(full_topic), "%s%s/config", base_topic, sensors[i]); - snprintf(payload, sizeof(payload), - "{\n" - " \"name\": \"%s\",\n" - " \"state_topic\": \"%s/%s\",\n" - " \"icon\": \"mdi:music-note\",\n" - " \"unique_id\": \"shairport-sync-%s\",\n" - " \"device\": {\n" - " \"identifiers\": [\"shairport-sync\"],\n" - " \"name\": \"%s\",\n" - " \"model\": \"Shairport-Sync\",\n" - " \"manufacturer\": \"shairport-sync\"\n" - " }\n" - "}", sensor_names[i], config.mqtt_topic, sensors[i], sensors[i], device_name); + bool is_binary_sensor = (strcmp(sensors[i], "is_active") == 0 || strcmp(sensors[i], "is_playing") == 0); - mosquitto_publish(mosq, NULL, full_topic, strlen(payload), payload, 0, true); - debug(2, "[MQTT Autodiscovery]: Published autodiscovery for %s", sensor_names[i]); + snprintf(topic, sizeof(topic), "%s/%ssensor/%s/%s_%s%s/config", + autodiscovery_prefix, is_binary_sensor ? "binary_" : "", + model, model, device_name, sensors[i]); + + snprintf(payload, sizeof(payload), + "{" + "\"name\": \"%s\"," + "\"state_topic\": \"%s/%s\"," + "\"icon\": \"%s\"," + "\"unique_id\": \"%s_%s\"," + "%s%s" + "}", + sensor_names[i], config.mqtt_topic, sensors[i], icons[i], device_name, sensors[i], + is_binary_sensor ? "\"payload_on\": \"1\",\"payload_off\": \"0\"," : "", + device_payload); + + mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, 0, true); + debug(2, "[MQTT]: Published autodiscovery for %s", device_name) } } diff --git a/mqtt.h b/mqtt.h index 4bf680ed..69ad75bd 100644 --- a/mqtt.h +++ b/mqtt.h @@ -7,6 +7,7 @@ int initialise_mqtt(); void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length); void mqtt_publish(char *topic, char *data, uint32_t length); void mqtt_setup(); +void send_autodiscovery_messages(struct mosquitto *mosq); void on_connect(struct mosquitto *mosq, void *userdata, int rc); void on_disconnect(struct mosquitto *mosq, void *userdata, int rc); void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *msg); From e252ee7fb5ac54dc52a21e87441e78b3eedd7fed Mon Sep 17 00:00:00 2001 From: aaronk6 Date: Sun, 17 Mar 2024 21:22:47 +0000 Subject: [PATCH 4/6] MQTT auto discovery working --- mqtt.c | 79 ++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/mqtt.c b/mqtt.c index 51e474d8..c85dfeb9 100644 --- a/mqtt.c +++ b/mqtt.c @@ -21,10 +21,6 @@ struct mosquitto *global_mosq = NULL; char *topic = NULL; int connected = 0; -// track active/playing state -int is_active = 0; -int is_playing = 0; - // mosquitto logging void _cb_log(__attribute__((unused)) struct mosquitto *mosq, __attribute__((unused)) void *userdata, int level, const char *str) { @@ -101,17 +97,20 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, } } +// function to send autodiscovery messages for Home Assistant void send_autodiscovery_messages(struct mosquitto *mosq) { const char *device_name = config.service_name; const char *sw_version = get_version_string(); const char *model = "shairport-sync"; + const char *model_friendly = "Shairport Sync"; const char *manufacturer = "Mike Brady"; const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL) ? config.mqtt_autodiscovery_prefix : "homeassistant"; char topic[512]; - char payload[1024]; + char payload[1280]; char device_payload[512]; + char id_string[128]; snprintf(device_payload, sizeof(device_payload), "\"device\": {" @@ -121,7 +120,7 @@ void send_autodiscovery_messages(struct mosquitto *mosq) { "\"sw_version\": \"%s\"," "\"manufacturer\": \"%s\"" "}", - model, device_name, model, sw_version, manufacturer); + model, device_name, model_friendly, sw_version, manufacturer); // when adding sensors here, be sure to also update sensor_names and icons below! const char *sensors[] = { @@ -130,12 +129,18 @@ void send_autodiscovery_messages(struct mosquitto *mosq) { "title", "genre", "format", + "output_format", + "output_frame_rate", "track_id", "client_ip", + "client_mac_address", "client_name", + "client_model", + "client_device_id", + "server_ip", "volume", - "is_active", - "is_playing", + "active", + "playing", NULL }; @@ -145,12 +150,18 @@ void send_autodiscovery_messages(struct mosquitto *mosq) { "Title", "Genre", "Format", + "Output Format", + "Output Frame Rate", "Track ID", "Client IP", + "Client MAC Address", "Client Name", + "Client Model", + "Client Device ID", + "Server IP", "Volume", - "Is Active", - "Is Playing" + "Active Session", + "Playing" }; const char *icons[] = { @@ -159,35 +170,48 @@ void send_autodiscovery_messages(struct mosquitto *mosq) { "mdi:music", // title "mdi:music-box-multiple", // genre "mdi:file", // format + "mdi:file", // output format + "mdi:file-chart", // output frame rate "mdi:identifier", // track ID "mdi:ip", // client IP - "mdi:cellphone-wireless", // client name + "mdi:hexadecimal", // client MAC address + "mdi:cellphone-text", // client name + "mdi:cellphone-text", // client model + "mdi:hexadecimal", // client device ID + "mdi:ip-network", // server IP "mdi:volume-high", // volume - "mdi:play-box-multiple", // is active - "mdi:play-box-multiple-outline" // is playing + "mdi:play-box-multiple", // active + "mdi:play-box-multiple-outline" // playing }; for (int i = 0; sensors[i] != NULL; i++) { - bool is_binary_sensor = (strcmp(sensors[i], "is_active") == 0 || strcmp(sensors[i], "is_playing") == 0); + bool is_binary_sensor = (strcmp(sensors[i], "active") == 0 || strcmp(sensors[i], "playing") == 0); + bool is_volume_sensor = strcmp(sensors[i], "volume") == 0; - snprintf(topic, sizeof(topic), "%s/%ssensor/%s/%s_%s%s/config", + snprintf(topic, sizeof(topic), "%s/%ssensor/%s/%s_%s/config", autodiscovery_prefix, is_binary_sensor ? "binary_" : "", - model, model, device_name, sensors[i]); + model, device_name, sensors[i]); + + snprintf(id_string, sizeof(id_string), "%s_%s_%s", model, device_name, sensors[i]); snprintf(payload, sizeof(payload), "{" "\"name\": \"%s\"," "\"state_topic\": \"%s/%s\"," "\"icon\": \"%s\"," - "\"unique_id\": \"%s_%s\"," - "%s%s" + "\"unique_id\": \"%s\"," + "\"object_id\": \"%s\"," + "%s%s%s" "}", - sensor_names[i], config.mqtt_topic, sensors[i], icons[i], device_name, sensors[i], + sensor_names[i], config.mqtt_topic, sensors[i], icons[i], id_string, id_string, is_binary_sensor ? "\"payload_on\": \"1\",\"payload_off\": \"0\"," : "", + is_volume_sensor ? "\"value_template\": \"{{ ((value | regex_findall_index(" + "find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1) * 100) | round(0) }}\"," + "\"unit_of_measurement\": \"%\"," : "", device_payload); mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, 0, true); - debug(2, "[MQTT]: Published autodiscovery for %s", device_name) + debug(2, "[MQTT]: published autodiscovery for %s", id_string); } } @@ -266,16 +290,14 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le } else if (type == 'ssnc') { switch (code) { case 'abeg': - is_active = 1; - mqtt_publish("is_active", "1", 1); + mqtt_publish("active", "1", 1); mqtt_publish("active_start", data, length); break; case 'acre': mqtt_publish("active_remote_id", data, length); break; case 'aend': - is_active = 0; - mqtt_publish("is_active", "0", 1); + mqtt_publish("active", "0", 1); mqtt_publish("active_end", data, length); break; case 'asal': @@ -313,13 +335,11 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le mqtt_publish("output_frame_rate", data, length); break; case 'pbeg': - is_playing = 1; - mqtt_publish("is_playing", "1", 1); + mqtt_publish("playing", "1", 1); mqtt_publish("play_start", data, length); break; case 'pend': - is_playing = 0; - mqtt_publish("is_playing", "0", 1); + mqtt_publish("playing", "0", 1); mqtt_publish("play_end", data, length); break; case 'pfls': @@ -331,8 +351,7 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le } break; case 'prsm': - is_playing = 1; - mqtt_publish("is_playing", "1", 1); + mqtt_publish("playing", "1", 1); mqtt_publish("play_resume", data, length); break; case 'pvol': From af6d90282e5c223a8682b2088fc329957b08146c Mon Sep 17 00:00:00 2001 From: aaronk6 Date: Sun, 17 Mar 2024 22:02:58 +0000 Subject: [PATCH 5/6] Set unique device identifier --- mqtt.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mqtt.c b/mqtt.c index c85dfeb9..cc85e039 100644 --- a/mqtt.c +++ b/mqtt.c @@ -100,6 +100,7 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, // function to send autodiscovery messages for Home Assistant void send_autodiscovery_messages(struct mosquitto *mosq) { const char *device_name = config.service_name; + const char *device_id = config.airplay_device_id ? config.airplay_device_id : config.service_name; const char *sw_version = get_version_string(); const char *model = "shairport-sync"; const char *model_friendly = "Shairport Sync"; @@ -120,7 +121,7 @@ void send_autodiscovery_messages(struct mosquitto *mosq) { "\"sw_version\": \"%s\"," "\"manufacturer\": \"%s\"" "}", - model, device_name, model_friendly, sw_version, manufacturer); + device_id, device_name, model_friendly, sw_version, manufacturer); // when adding sensors here, be sure to also update sensor_names and icons below! const char *sensors[] = { From 6258021d5585fb7d1a370494ec5ba94b5b348967 Mon Sep 17 00:00:00 2001 From: aaronk6 Date: Sun, 17 Mar 2024 22:33:54 +0000 Subject: [PATCH 6/6] Fixed multiple devices getting merged in Home Assistant --- mqtt.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mqtt.c b/mqtt.c index cc85e039..addf04f6 100644 --- a/mqtt.c +++ b/mqtt.c @@ -101,6 +101,7 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, void send_autodiscovery_messages(struct mosquitto *mosq) { const char *device_name = config.service_name; const char *device_id = config.airplay_device_id ? config.airplay_device_id : config.service_name; + const char *device_id_no_colons = str_replace(device_id, ":", ""); const char *sw_version = get_version_string(); const char *model = "shairport-sync"; const char *model_friendly = "Shairport Sync"; @@ -189,9 +190,9 @@ void send_autodiscovery_messages(struct mosquitto *mosq) { bool is_binary_sensor = (strcmp(sensors[i], "active") == 0 || strcmp(sensors[i], "playing") == 0); bool is_volume_sensor = strcmp(sensors[i], "volume") == 0; - snprintf(topic, sizeof(topic), "%s/%ssensor/%s/%s_%s/config", + snprintf(topic, sizeof(topic), "%s/%ssensor/%s_%s/%s/config", autodiscovery_prefix, is_binary_sensor ? "binary_" : "", - model, device_name, sensors[i]); + model, device_id_no_colons, sensors[i]); snprintf(id_string, sizeof(id_string), "%s_%s_%s", model, device_name, sensors[i]);