Skip to content

ADR-0067: Whole-home HiFi in the Music popup — rooms, library and outputs through Music Assistant, pushed by Home Assistant

Status: Proposed (2026-10-18, asked for by the author; contract written before the code, not tried on a tablet when written). Extends the music popup of ADR-0050 and the favourite playlists page of ADR-0068 (its own pull request, #515). Date: 2026-10-18

Context

The author asked for « une gestion complète HiFi » on the screen, built on Music Assistant: players, sources, outputs, « toute la maison », beautiful, practical, accessible but extremely complete, from the smallest installation to the largest. ADR-0050 gives one player at a time (cover, transport, volume, six chips); ADR-0068 adds favourite YouTube playlists for an Apple TV.

Constraints: push-only and events-only (ADR-0001, ADR-0025) — the firmware names no entity, calls no action and talks to nothing but Home Assistant; nothing of a real home in the public HA files, no !secret (ADR-0024); one popup with pages and one swipe (ADR-0046); the music card stays 940 × 536 on both boards (the author's choice on 2026-10-18, after a mock-up); colours from the palette; every icon in its font's glyphs:; instant transitions; kPayloadMax (16 KiB) per payload.

Verified on the author's Home Assistant on 2026-10-18 (HAOS 18.3, aarch64 VM, Core 2026.10.2):

  • The Music Assistant app 2.10.7 (d5369777_music_assistant, host network) is found by Home Assistant (hassio discovery, one confirmation) and its integration music_assistant loads: one media_player per player, mass_player_type (player / group), active_queue, group_members when the player can group, plus one « add the current song to favourites » button per player.
  • Players seen with no setting: AirPlay (Apple TV 4K — unavailable until paired with a PIN shown on the TV —, Freebox Player), DLNA (Samsung TV), Google Cast (Lenovo Smart Clock), and the « Home Assistant MediaPlayers » provider for the ESPHome tablet, the Steam Deck and a voice satellite.
  • music_assistant.get_library (config_entry_id, media_type, limit, offset, favorite, order_by, response) returned the 12 radios of the library: uri library://radio/N, name, image = the image's original URL (a Wikipedia PNG, a favicon…), favorite.
  • music_assistant.play_media (library://radio/2) on the tablet's Music Assistant player: the tablet's own ESPHome media_player went playing at once (11:59:51), volume 20 % set before.
  • The shown track's entity_picture on a Music Assistant player is Music Assistant's own image proxy, in plain http on the LAN: http://<ha>:8095/imageproxy/<id>?size=512&fmt=png. The proxy serves sizes 0, 80, 160, 256, 512, 1024 only; <id> is sha256("<provider>/<path>") and must have been issued by Music Assistant (otherwise 404). Many radio logos fail upstream even there (Wikimedia refuses the size asked, a favicon is gone): a missing cover is normal, not a fault.
  • music_assistant.get_queue gives the current and the next item and the number of items, not the list; Home Assistant has no action to jump to an item of the queue.

Decision

Pages of the Music popup

Same card, same chrome, same swipe; the popup always opens on « Lecture ». Pages, in this order, each only when it has something to show:

  1. Lecture (ADR-0050), plus, with Music Assistant, one line under the album: « À suivre · » and « n titres » (the queue), and a chip « + n » after the player's name when it leads a group.</li> <li><strong>Playlists</strong> (<a href="../0068-favourite-playlists/">ADR-0068</a>, favourite playlists): its grid, its return to « Lecture » 600 ms after a tap and its button in the accent colour for 30 s are unchanged; since both were merged (2026-10-18), its page, its name on the title line and its swipe are those of this popup's single page brick (<code>tab5_musique.cpp</code>), no longer a second one in <code>tab5_lecteur.cpp</code>.</li> <li><strong>Pièces</strong> — every output of the home, eight at most, a grid of 4 × 2 cards: the kind's icon, the name, the title playing or the state, the volume (slider 0-100, sent on release), and up to three buttons each shown only when the player can: link / unlink to the shown player's group (<code>g</code>), power (<code>o</code>), « send the music here » (<code>t</code>, not on the shown one). A tap shows that player on « Lecture »; a long press opens the wheel (<a href="../0036-quick-action-wheel/">ADR-0036</a>): source (<code>s</code>), mute (<code>m</code>), transfer (<code>t</code>), unlink. On the title line, « Tout ici » (group every available player with the shown one) and « Tout séparer », with Music Assistant only. The shown player's card has the accent border, its group's cards a thinner one.</li> <li><strong>Bibliothèque</strong> — with Music Assistant only. A row of categories (Favoris, Playlists, Albums, Artistes, Radios, Podcasts, Récents), then a grid of 5 × 2 items: cover (or the kind's icon), name, second line. A tap plays a playable item (track, radio, episode, audiobook) on the shown player — replacing the queue — and opens a container (artist → albums, album / playlist → tracks, podcast → episodes) with a back button; a long press opens the wheel: play now, play next, add to the queue, radio mode, open. « Suivants » / « Précédents » when there are more. The sources of a player (from its wheel) show in the same grid.</li> </ol> <p>From the smallest to the largest home, with nothing to set on the screen:</p> <table> <thead> <tr> <th>Home</th> <th>Pages</th> </tr> </thead> <tbody> <tr> <td>One <code>media_player</code>, no Music Assistant</td> <td>Lecture (ADR-0050)</td> </tr> <tr> <td>Several chosen players, no Music Assistant</td> <td>Lecture, Pièces: the « Tab5 · lecteurs de musique » list (six), volume, power, mute, source; link only for players that group natively (<code>group_members</code>)</td> </tr> <tr> <td>Music Assistant</td> <td>Lecture with the queue, Pièces with every Music Assistant player (eight, sorted by name), groups and transfer, Bibliothèque</td> </tr> </tbody> </table> <p>Home Assistant tells which case applies (<code>ma</code> below); the firmware keeps nothing about it.</p> <h3 id="contract-minor-two-actions-one-event">Contract (minor: two actions, one event)<a class="headerlink" href="#contract-minor-two-actions-one-event" title="Permanent link">¶</a></h3> <p><strong><code>tab5_maj_musique_pieces(pieces, contexte)</code></strong> — pushed on <code>esphome.tab5_connected</code>, on <code>ouvrir</code>, and when a mirror signature of the listed players changes (state, volume, mute, group, title; <code>mode: restart</code>, 1 s wait: a burst is one push). Always sent, even empty.</p> <ul> <li><code>contexte</code> = <code>ma|montre|file|suivant</code>: <code>ma</code> <code>1</code> when the <code>music_assistant</code> integration is loaded, else <code>0</code>; <code>montre</code> the index in <code>pieces</code> of the shown player (<code>sensor.tab5_lecteur_montre</code>, ADR-0050), <code>-1</code> if absent; <code>file</code> the number of items of its queue (<code>0</code> unknown); <code>suivant</code> « title — artist » of the next item, empty if none — last field, takes the rest.</li> <li><code>pieces</code> = up to eight entries separated by <code>;</code>, each <code>nom|genre|etat|volume|muet|groupe|fonctions|titre</code>:</li> <li><code>nom</code> 40 characters at most;</li> <li><code>genre</code> one letter for the icon: <code>t</code> TV, <code>e</code> speaker, <code>a</code> screen / tablet, <code>g</code> group, <code>x</code> other;</li> <li><code>etat</code> <code>j</code> playing, <code>p</code> paused, <code>r</code> on and idle, <code>e</code> off / standby, <code>i</code> unavailable;</li> <li><code>volume</code> 0-100, empty if unknown; <code>muet</code> <code>0</code> / <code>1</code>;</li> <li><code>groupe</code> the index of the piece that leads its group (its own index when it leads members), <code>-1</code> alone;</li> <li><code>fonctions</code> letters: <code>v</code> volume, <code>m</code> mute, <code>o</code> power, <code>g</code> can group, <code>s</code> source list, <code>t</code> can receive a transfer (a Music Assistant player);</li> <li><code>titre</code> « title — artist » when playing or paused, 60 characters at most.</li> </ul> <p><strong><code>tab5_maj_musique_liste(contexte, elements)</code></strong> — pushed only in answer to an event (<code>parcourir</code>, <code>detail</code>, <code>sources</code>, later <code>chercher</code>). Home Assistant keeps nothing: the tablet sends back the <code>ref</code> it received.</p> <ul> <li><code>contexte</code> = <code>vue|titre|page|suite|retour</code>: <code>vue</code> <code>fav</code> <code>pl</code> <code>alb</code> <code>art</code> <code>rad</code> <code>pod</code> <code>rec</code> <code>det</code> (a container's content) <code>src</code> (a piece's sources) <code>rech</code> (search, later) or <code>err</code> (titre = the sentence to show); <code>titre</code> 48 characters at most; <code>page</code> from 0; <code>suite</code> <code>1</code> if a next page exists; <code>retour</code> <code>1</code> to show the back button.</li> <li><code>elements</code> = up to ten entries separated by <code>;</code>, each <code>nom|sous|genre|ref|image</code>:</li> <li><code>nom</code>, <code>sous</code> 40 characters at most;</li> <li><code>genre</code> <code>t</code> track, <code>a</code> album, <code>r</code> artist, <code>p</code> playlist, <code>d</code> radio, <code>o</code> podcast or episode, <code>l</code> audiobook, <code>s</code> source, <code>x</code> other (decides the icon and what a tap does);</li> <li><code>ref</code> a Music Assistant URI (<code>library://album/12</code>, <code>ytmusic://playlist/…</code>), 160 characters at most, or for <code>s</code> the source's index in the player's <code>source_list</code>;</li> <li><code>image</code> an http(s) URL, may be empty — last field, takes the rest.</li> <li>Home Assistant asks small images: a Music Assistant proxy URL gets <code>size=160</code>; a <code>…googleusercontent.com/…=wN-hN</code> URL gets <code>=w160-h160</code>; other URLs go as they are. In every name, « | » becomes « / » and « ; » becomes « , »; in a URL « ; » becomes <code>%3B</code>.</li> </ul> <p><strong>Event <code>esphome.tab5_musique</code></strong> (<code>action</code>, <code>piece</code>, <code>valeur</code>); <code>piece</code> is an index of the last <code>pieces</code> pushed or <code>-1</code> (the shown player):</p> <table> <thead> <tr> <th>action</th> <th>valeur</th> <th>Home Assistant does</th> </tr> </thead> <tbody> <tr> <td><code>ouvrir</code></td> <td>—</td> <td>pushes <code>pieces</code></td> </tr> <tr> <td><code>parcourir</code></td> <td><code>vue\|page</code></td> <td><code>get_library</code> (favourites, playlists, albums, artists, radios, podcasts) or the recently played, ten per page, pushes the list</td> </tr> <tr> <td><code>detail</code></td> <td><code>ref\|page</code></td> <td>the content of that artist / album / playlist / podcast</td> </tr> <tr> <td><code>jouer</code></td> <td><code>mode\|ref</code></td> <td><code>music_assistant.play_media</code> on the piece; <code>mode</code> <code>m</code> now (replace), <code>s</code> next, <code>f</code> at the end, <code>r</code> radio mode</td> </tr> <tr> <td><code>montrer</code></td> <td>—</td> <td>the piece becomes the shown player (as a chip does, ADR-0050)</td> </tr> <tr> <td><code>grouper</code> / <code>separer</code></td> <td>—</td> <td><code>media_player.join</code> the piece to the shown player / <code>media_player.unjoin</code> it</td> </tr> <tr> <td><code>tout_grouper</code> / <code>tout_separer</code></td> <td>—</td> <td>every available piece that can group / every grouped piece</td> </tr> <tr> <td><code>transferer</code></td> <td>—</td> <td><code>music_assistant.transfer_queue</code> from the shown player to the piece, which becomes the shown one</td> </tr> <tr> <td><code>volume</code></td> <td>0-100</td> <td><code>media_player.volume_set</code></td> </tr> <tr> <td><code>muet</code> / <code>marche</code></td> <td>—</td> <td>toggles mute / power</td> </tr> <tr> <td><code>sources</code></td> <td><code>page</code></td> <td>pushes the piece's <code>source_list</code> (<code>vue</code> <code>src</code>)</td> </tr> <tr> <td><code>source</code></td> <td>index</td> <td><code>media_player.select_source</code> with that entry of <code>source_list</code></td> </tr> </tbody> </table> <p>Home Assistant rebuilds the list of pieces the same way to resolve an index (an index out of range does nothing), accepts a <code>ref</code> only if it matches <code>^[a-z0-9_-]+://[^|;\s]+$</code>, a <code>mode</code> only among the four letters, and never takes an entity or an action from the event. Every action has <code>continue_on_error</code>.</p> <h3 id="where-the-code-goes">Where the code goes<a class="headerlink" href="#where-the-code-goes" title="Permanent link">¶</a></h3> <ul> <li>Home Assistant: one new package, <code>packages/tab5_musique.yaml</code> (mirror signature, the two pushes, <code>script.tab5_musique_commande</code>), one branch in <code>packages/tab5_evenements.yaml</code>; it reuses <code>sensor.tab5_lecteur_montre</code> and <code>script.tab5_lecteur_montrer</code> of <code>packages/tab5_lecteur.yaml</code>. The integration's <code>config_entry_id</code> is found with <code>integration_entities('music_assistant')</code> / <code>config_entry_id()</code>, never written.</li> <li>Firmware: a new module <code>Tab5/ecran/tab5_musique.*</code> (pages Pièces and Bibliothèque, the downloads), new components <code>Tab5/ui_components/musique_piece.yaml</code> (×8) and <code>musique_element.yaml</code> (×10), the pages registered on the Music popup's <code>PagesPopup</code>. Reading in pure C++ tested on a PC (<code>Tab5/socle/tab5_parse.*</code>, a new section: <code>musique_pieces_lire()</code>, <code>musique_liste_lire()</code>; cases in <code>tools/test_parse.cpp</code>, a call in <code>tools/fuzz/fuzz_parse.cpp</code>).</li> <li>Covers of the library: ten <code>online_image</code> of 160 × 160 (RGB565, about 50 KiB each in PSRAM), downloaded one after the other from the first item; a failed download keeps the kind's icon. The shown track's cover stays ADR-0050's.</li> </ul> <h2 id="rejected">Rejected<a class="headerlink" href="#rejected" title="Permanent link">¶</a></h2> <ul> <li><strong>The tablet talking to Music Assistant itself</strong> (its websocket or HTTP API): against ADR-0001 / ADR-0025, and a Music Assistant token on the device.</li> <li><strong>The whole queue and « play item n »</strong>: Home Assistant's integration has no action for it; Music Assistant's own API needs a token, which would be a secret in public files (ADR-0024). Possible later through the optional Tab5 integration (<a href="../0035-hacs-integration-ha-files/">ADR-0035</a>); until then « À suivre » and the count.</li> <li><strong>Resized images through Music Assistant's proxy for every item</strong>: the proxy id needs the image's provider, which Home Assistant's response does not carry; only the URLs that are already proxy URLs (local files, now playing) are resized.</li> <li><strong><code>media_player.browse_media</code></strong> instead of <code>get_library</code>: no paging (500 items at once) and no favourites filter.</li> <li><strong>A second popup or a bigger card</strong>: the author kept the 940 × 536 card; one popup with pages (ADR-0046).</li> <li><strong>Every <code>media_player</code> of the house without Music Assistant</strong>: ADR-0050's rejection stays (TV apps, the tablet's own speaker…); with Music Assistant, its players are already the author's choice of outputs.</li> <li><strong>« Don't stop the music » as a toggle</strong>: no Home Assistant action sets it; the radio mode of <code>jouer</code> gives the same « keep playing » from an item.</li> </ul> <h2 id="consequences">Consequences<a class="headerlink" href="#consequences" title="Permanent link">¶</a></h2> <ul> <li>API contract 1.3.0 → 1.4.0 (two actions and one event added). Update order: the firmware first; new HA files with an older firmware: the two pushes fail with <code>continue_on_error</code>, nothing else stops. An older HA with the new firmware: no Pièces / Bibliothèque page (nothing pushed).</li> <li>The page names on the title line: Lecture, [Playlists], Pièces, [Bibliothèque] — up to four on a 940 px card next to the player's name; to be judged on the 7B.</li> <li>New glyphs (speaker, TV, group, link / unlink, power, transfer, album, artist, playlist, radio, podcast, history, back), new screen texts in the six languages, new screens in the off-device render (Pièces with a group, Bibliothèque with and without covers, the sources).</li> <li>Search by text needs a keyboard on the screen: a later lot, with a <code>chercher</code> action (<code>texte|page</code>) already reserved in this table.</li> <li>Not measured when written: the ten downloads' time and memory on the 7B, the time of <code>get_library</code> with a large library, a push of eight pieces during a volume drag.</li> </ul> </article> </div> <script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script> </div> <button type="button" class="md-top md-icon" data-md-component="top" hidden> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg> Back to top </button> </main> <footer class="md-footer"> <nav class="md-footer__inner md-grid" aria-label="Footer" > <a href="../0066-rooms-count-cut-tiles/" class="md-footer__link md-footer__link--prev" aria-label="Previous: ADR-0066: The number of rooms is chosen in Home Assistant (up to ten), and a room takes up to twenty elements by cutting tiles in two or in four"> <div class="md-footer__button md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg> </div> <div class="md-footer__title"> <span class="md-footer__direction"> Previous </span> <div class="md-ellipsis"> ADR-0066: The number of rooms is chosen in Home Assistant (up to ten), and a room takes up to twenty elements by cutting tiles in two or in four </div> </div> </a> <a href="../0068-favourite-playlists/" class="md-footer__link md-footer__link--next" aria-label="Next: ADR-0068: Favourite playlists in the music popup — names pushed by the blueprint, links kept in Home Assistant, played on an Apple TV through the YouTube app"> <div class="md-footer__title"> <span class="md-footer__direction"> Next </span> <div class="md-ellipsis"> ADR-0068: Favourite playlists in the music popup — names pushed by the blueprint, links kept in Home Assistant, played on an Apple TV through the YouTube app </div> </div> <div class="md-footer__button md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg> </div> </a> </nav> <div class="md-footer-meta md-typeset"> <div class="md-footer-meta__inner md-grid"> <div class="md-copyright"> <div class="md-copyright__highlight"> MIT licence · shared in case it is useful · made with the help of AI </div> Made with <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener"> Material for MkDocs </a> </div> </div> </div> </footer> </div> <div class="md-dialog" data-md-component="dialog"> <div class="md-dialog__inner md-typeset"></div> </div> <script id="__config" type="application/json">{"annotate": null, "base": "../..", "features": ["navigation.indexes", "navigation.footer", "navigation.top", "navigation.tracking", "search.suggest", "search.highlight", "content.code.copy", "toc.follow"], "search": "../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script> <script src="../../assets/javascripts/bundle.d7400e89.min.js"></script> </body> </html>