HomeUser GuideHome Automation › Dashboard
Home Assistant · step by step

Build this dashboard in about an hour

Ten steps, every card's YAML, and a complete view you can paste in one go. Built and photographed on a real Marantz CINEMA 30. Change two strings and it is yours.

The finished dashboard: a now-playing hero, volume with dB presets, a source grid, surround modes, room correction, scenes and a volume graph
What you are building
In a hurry? Jump to step 10, download the complete view, change two strings and paste it. The steps in between explain what each piece does so you can change it.

Before you start

Two things feed this dashboard, and the difference matters at every step below.

Comes fromGives you
AVR Maestro's synced scriptsEvery control. Inputs, surround modes, Audyssey, Dirac, Smart Selects, presets, dimmer, eco, your Scenes.
The Denon AVR integration's media player entityLive state. Power, selected source, surround mode, volume, mute.
Step 1

Install four cards from HACS

All four are in the default HACS store. In Home Assistant: HACS › Frontend › Explore & Download Repositories, search the name, Download, then reload your browser.

Search forUsed forIf you skip it
MushroomEvery control tile, and the volume sliderUse a plain button card. Works, looks squarer.
button-cardSection headers with a live labelUse a markdown card, or plain headings.
card-modThe colours, and the lit state on the active tileEverything still works. Nothing lights up.
apexcharts-cardThe history graphsUse the built-in history-graph card.
None of them are required to make it work. Every control below runs from a stock Home Assistant button card pointed at the same script. The custom cards are makeup, not plumbing.
Step 2

Find your two entity IDs

Every snippet on this page uses two placeholders. Find yours once and the rest is copy and paste.

The middle of every script ID is your receiver's MAC address, twelve lowercase characters, the same on all of them. Copy it once. Everywhere this page writes MAC, put yours.

Ignore any that say unavailable. Older builds named scripts after the receiver's name rather than its MAC. If you synced back then, those older ones are still listed and are dead. The live ones are the MAC-named ones.
Step 3

Create the view

  1. 1. Settings › Dashboards › Add Dashboard › New dashboard from scratch. Call it whatever you like.
  2. 2. Open it, then the pencil icon top right to edit.
  3. 3. Add a view, and choose the Sections layout. Not Masonry. Sections is what makes it reflow properly on a phone.
  4. 4. In the view settings, set Max columns to 3 and turn on Dense section placement.

From here on, every step is: add a section, then paste cards into it. To paste YAML, use the three-dot menu on a card and choose Edit in YAML, or the three-dot menu on the dashboard for Raw configuration editor to paste the whole thing at once.

# The view's own settings, if you prefer to type them
type: sections
title: AVR Maestro
icon: mdi:audio-video
max_columns: 3
dense_section_placement: true
Step 4

Add a control

Start with the simplest thing on the board, so you know your entity IDs are right before you build anything complicated. Add a section, add a card, Edit in YAML, paste:

type: button
name: Pure Direct
icon: mdi:speaker
tap_action:
  action: perform-action
  perform_action: script.turn_on
  target:
    entity_id: script.avr_maestro_MAC_mode_pure_direct

Stock Home Assistant. No HACS needed.

Tap it. If the receiver changes, your IDs are right and everything below will work. If nothing happens, go back to step 2.

Step 5

Make it light up when it is the live one

This is the pattern worth learning, and every source and surround tile on the board is a copy of it. The card sends a script, and colours itself from the receiver entity, so the input you are actually on is lit and the others are not.

The source grid with Nvidia Shield lit in orange and the other twelve inputs dark
One lit tile, twelve dark ones
type: custom:mushroom-template-card
entity: script.avr_maestro_MAC_input_cd
icon: mdi:disc
primary: CD
icon_color: >-
  {{ 'orange' if (state_attr('media_player.your_receiver','source') or '')|lower
     == 'cd' else 'grey' }}
layout: vertical
fill_container: true
tap_action:
  action: perform-action
  perform_action: script.turn_on
  target:
    entity_id: script.avr_maestro_MAC_input_cd
card_mod:
  style: |
    ha-card {
      border-radius: 14px;
    {% if (state_attr('media_player.your_receiver','source') or '')|lower == 'cd' %}
      background: linear-gradient(160deg, rgba(226,86,10,0.34), rgba(15,15,17,0.88));
      border: 1px solid rgba(255,122,24,0.80);
    {% else %}
      background: rgba(15,15,17,0.72);
      border: 1px solid rgba(255,122,24,0.14);
    {% endif %}
    }

Change three things per input: the script, the icon, and the string it compares against.

Two things to get right, or a tile will never light:

Surround modes are the same card with sound_mode in place of source.

Step 6

Give each section a live header

A plain title is dead space. Put the live value under it and "Sources" becomes "Sources / Now on Nvidia Shield".

type: custom:button-card
name: Sources
icon: mdi:video-input-hdmi
color_type: label-card
entity: media_player.your_receiver
show_label: true
label: >-
  [[[ var v = states["media_player.your_receiver"].attributes.source;
      return v ? ("Now on " + v) : "Receiver in standby"; ]]]
tap_action:
  action: none
styles:
  grid:
    - grid-template-areas: '"i n" "i l"'
    - grid-template-columns: min-content auto
  name:
    - font-size: 13px
    - letter-spacing: 3px
    - text-transform: uppercase
    - justify-self: start
  label:
    - font-size: 11px
    - justify-self: start

The two-line grid is what puts the label under the title instead of beside it.

Swap attributes.source for attributes.sound_mode on the Surround header, and so on.

Step 7

Show volume in dB

Home Assistant reports volume as a fraction. Nobody sets a receiver to "0.67". One line of maths fixes it and the whole section starts reading like a receiver.

The volume section: a slider, up, down, mute and four dB presets
Night, Dialog, Movie and Loud, each labelled with the dB it sets
The conversion. Home Assistant publishes volume_level as master volume divided by 100, and on Denon and Marantz receivers MV 80 is the 0 dB reference. So dB = volume_level × 100 − 80. A volume_level of 0.67 is master volume 67, which is −13.0 dB.
# The readout
type: custom:mushroom-template-card
entity: media_player.your_receiver
icon: mdi:volume-high
primary: >-
  {% set v = state_attr('media_player.your_receiver','volume_level') %}
  {% if v is not none %}{{ (v * 100 - 80) | round(1) }} dB{% else %}Standby{% endif %}
secondary: Master volume
layout: vertical
# The slider. media_controls must be empty or the bar becomes a power button
type: custom:mushroom-media-player-card
entity: media_player.your_receiver
volume_controls:
  - volume_set
media_controls: []
show_volume_level: true
# A preset. 0.50 is master volume 50, which is -30 dB
type: custom:mushroom-template-card
entity: media_player.your_receiver
icon: mdi:weather-night
primary: Night
secondary: "-30 dB"
tap_action:
  action: perform-action
  perform_action: media_player.volume_set
  target:
    entity_id: media_player.your_receiver
  data:
    volume_level: 0.50
Choose your loudest preset deliberately. The set above stops at −10 dB. A dashboard is much easier to hit by accident than a remote, and reference level on a big receiver is genuinely loud.
Step 8

Add your Scenes

Read the name from the script instead of typing it, and renaming a Scene in AVR Maestro renames the tile with no dashboard edit.

Four scene tiles, each listing the settings that scene restores
Each tile says what its scene actually restores
type: custom:mushroom-template-card
entity: script.avr_maestro_MAC_scene_1234567890123
icon: mdi:theater
primary: >-
  {{ state_attr('script.avr_maestro_MAC_scene_1234567890123','friendly_name')
     | replace('AVR Maestro - ', '') | replace(' (Your Receiver)', '') }}
secondary: >-
  {% set t = states.script['avr_maestro_MAC_scene_1234567890123'].attributes.last_triggered %}
  {{ 'recalled ' ~ relative_time(t) ~ ' ago' if t else 'not run yet' }}
multiline_secondary: true
tap_action:
  action: perform-action
  perform_action: script.turn_on
  target:
    entity_id: script.avr_maestro_MAC_scene_1234567890123

The two replace() calls trim the prefix and the receiver name off the friendly name.

A second line is worth the extra minute. A wall of evocatively named buttons tells you nothing at 11pm; a line saying what each one restores tells you which to press.

Step 9

Add history

The receiver's media player entity is recorded by Home Assistant like any other, so you get a listening history for free. Everything below comes from that one entity: no extra sensors, no helpers, nothing to configure first.

Five history graphs: power and activity, master volume in dB, volume by day, source in use and surround mode
Five graphs, one entity, seven days

The plain state timeline needs no custom cards at all, and is the one most worth having:

type: history-graph
hours_to_show: 168
entities:
  - entity: media_player.your_receiver
    name: Receiver

On, off, and what it was doing, across a week.

Volume in dB uses the same conversion as step 7:

type: custom:apexcharts-card
graph_span: 7d
header:
  show: true
  title: Master volume, last 7 days
series:
  - entity: media_player.your_receiver
    attribute: volume_level
    name: Master volume
    type: line
    curve: smooth
    unit: dB
    float_precision: 1
    transform: "return x == null ? null : Number(x) * 100 - 80;"
    extend_to: now
    group_by:
      duration: 1h
      func: last
      fill: last
apex_config:
  chart:
    height: 240
  yaxis:
    min: -60
    max: 0

group_by with fill: last turns sparse events into a continuous line. The y-axis has to go in apex_config to take effect.

A daily average and peak turns a spiky line into something with a shape:

type: custom:apexcharts-card
graph_span: 7d
span:
  end: day
header:
  show: true
  title: Master volume by day in dB, average and peak
series:
  - entity: media_player.your_receiver
    attribute: volume_level
    name: Day average
    type: column
    float_precision: 1
    transform: "return x == null ? null : Number(x) * 100;"
    group_by: {duration: 1d, func: avg, fill: 'null'}
  - entity: media_player.your_receiver
    attribute: volume_level
    name: Day peak
    type: line
    curve: smooth
    float_precision: 1
    transform: "return x == null ? null : Number(x) * 100;"
    group_by: {duration: 1d, func: max, fill: 'null'}
apex_config:
  yaxis:
    min: 0
    max: 80
    labels:
      formatter: "EVAL:function (v) { return (v - 80).toFixed(0); }"
  tooltip:
    y:
      formatter: "EVAL:function (v) { return v == null ? '' : (v - 80).toFixed(1) + ' dB'; }"

Two series off the same attribute, grouped by day.

Why this one plots 0 to 80 and formats to dB, instead of plotting dB. Bars grow from the axis floor. Plot dB directly and every bar hangs down from 0, so a long bar means quiet, which reads backwards. Plot the raw master volume number and subtract 80 in the axis and tooltip formatters, and the bars grow upward while everything the reader sees is still dB. fill: 'null' means a day with no readings gets no bar rather than yesterday's carried forward.

And the two that answer "what do I actually watch on, and in what": source and surround mode, plotted as a timeline of which one was selected.

type: custom:apexcharts-card
graph_span: 7d
span:
  end: day
header:
  show: true
  title: Source in use, last 7 days
series:
  - entity: media_player.your_receiver
    attribute: source
    name: Source
    type: line
    curve: stepline
    stroke_width: 3
    extend_to: now
    float_precision: 0
    transform: |
      if (x == null) return null;
      var m = ["Phono", "NET", "Xbox", "Nvidia Shield"];
      var i = m.indexOf(x);
      return i < 0 ? 0 : i + 1;
apex_config:
  yaxis:
    min: 0
    max: 4
    tickAmount: 4
    labels:
      formatter: >-
        EVAL:function (v) { return ["Other", "Phono", "HEOS Net", "Xbox",
        "Shield"][Math.round(v)] || ''; }
  tooltip:
    y:
      formatter: >-
        EVAL:function (v) { return v == null ? 'off' : ["Other", "Phono",
        "HEOS Net", "Xbox", "Shield"][Math.round(v)] || 'Other'; }

Put your own sources in the transform array and in both formatter arrays. Keep index 0 as "Other".

A chart axis needs numbers, so the transform maps each source name to a position and the formatters map it back for the labels and the tooltip. Make the mapping total: null (receiver off) stays a gap, and anything not in your list falls to "Other", so the two can never be confused. Same card again with sound_mode gives you surround mode, with the strings in upper case exactly as your receiver reports them.

Keep stepline on those last two. A source or a surround mode is a category, not a number. Smooth the curve and the line swoops between Shield and Xbox through whatever sits between them on the axis, drawing an input that was never selected. Volume is a real quantity and smooths honestly; a category does not.
Check your recorder retention first. Home Assistant keeps ten days by default, so seven days is about the most you can usefully ask for. To go back further you need a state_class: measurement template sensor, which Home Assistant keeps as long-term statistics for years.
A missing day is usually correct. Home Assistant records changes, not samples. Leave the receiver on one input at one volume for two days and it writes nothing at all, so that day has no bar. It looks like data loss and it is the opposite: nothing happened.
Step 10

Or take the whole thing

The complete view, every section and every card, with placeholders instead of entity IDs.

  1. 1. Download the view YAML.
  2. 2. Open it in any text editor and replace two strings, everywhere they appear: YOUR_RECEIVER with your media player entity's name, and YOURMAC with your receiver's twelve characters from step 2.
  3. 3. In Home Assistant, open your dashboard, pencil icon, three-dot menu, Raw configuration editor, and paste it in as a view.
Your Scene IDs will not match. Scene scripts are named after when the Scene was created, so the four in the file do not exist on your system: those cards will render and do nothing. Replace their entity IDs with your own from step 2, or delete the Scenes section and rebuild it with step 8.
Two other things to expect. The source and surround graphs carry the source names from the receiver this was built on, so edit those arrays as step 9 describes. And the tile labels are English; nothing in the file is translated.

If a tile never lights up

Before assuming you built it wrong: some of them cannot light up, and it is worth knowing which.

A tile can only show its state if Home Assistant can read it. Home Assistant reads your receiver through the Denon AVR integration, and that publishes a specific list.

Home Assistant can seeHome Assistant cannot see
PowerAudyssey MultEQ curve
Selected sourceDynamic Volume setting
Surround modeWhich Dirac slot is loaded
Volume and muteFront panel dimmer level
Dynamic EQ, sometimesEco mode, speaker preset, virtualizer

Everything in the right-hand column is readable from the receiver - it answers when asked, over the same protocol AVR Maestro uses to draw its own screens. It just is not published to Home Assistant, so a dashboard has nowhere to read it from. Those buttons work perfectly; they cannot show you which one is active.

Measured on the receiver this was built on: twenty of sixty controls can show their state, and forty cannot. Publishing the other forty is on the roadmap, and it is the difference between a wall of buttons and a control surface.

If a value can mean two things, say so. Dynamic EQ arrives as true, false, or not at all. Treating "not at all" as "off" puts a confident wrong answer on your wall.
icon_color: >-
  {% set d = state_attr('media_player.your_receiver','dynamic_eq') %}
  {{ 'orange' if d is true else 'grey' }}
secondary: >-
  {% set d = state_attr('media_player.your_receiver','dynamic_eq') %}
  {% if d is none %}state not published{% elif d %}on{% else %}off{% endif %}

Three states, not two.

Other things worth knowing

The same dashboard at phone width, reflowed into a single column
Same dashboard at phone width: the three columns become one, and nothing was configured to make that happen

Your whole receiver, on one wall

Home Automation is the one paid unlock in AVR Maestro. Everything else, including the on-device AI assistant, is included.

Get it on Google Play Download on the App Store Download on the Mac App Store Get it on the Microsoft Store

Back to Home Automation → · Full User Guide →