Command Dashboard Redesign ("Command") Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a new phone-first YAML-mode Home Assistant dashboard "Command" in the Midnight Glass language (Hybrid Hero + Bubble-Card pop-ups) with 5 curated tabs, leaving the existing Home Command Dashboard untouched.
Architecture: One new command_dashboard.yaml (YAML-mode, registered in configuration.yaml), backed by a few new scripts.yaml quick-action scripts and configuration_tou_addition.yaml template sensors. Shared components (hero, chip row, glass tile, Bubble pop-ups, media hub) are built first in the Overview tab; later tab tasks reuse those in-file patterns. Deployed via the existing CI pipeline (push → restart).
Tech Stack: HA YAML/Lovelace, custom:button-card (with [[[ JS ]]] templates), custom:bubble-card (pop-ups), custom:mini-media-player, custom:apexcharts-card, custom:mini-graph-card, custom:mushroom-*, custom:card-mod, GitHub Actions CI, pytest.
Reference spec: docs/superpowers/specs/2026-06-17-command-dashboard-redesign-design.md
Key facts & conventions (read once)¶
- Repo root:
/Users/pk/code/homeassistant. YAML = 2-space indent..yamllintdisablesline-length(long[[[ JS ]]]lines are fine). - Gates per task:
make lint(yamllint + JSON/Grafana guards) andmake pytest(20+ structural/entity-ref tests) must pass before commit.make testadditionally runs containercheck_config(needs Docker; if unavailable locally, CI runs it). - Commit after each task:
auto: <what changed>+ trailerCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>. Do not push until Task 12. - Midnight Glass tokens: background
radial-gradient(130% 80% at 50% -5%, #1a2440 0%, #0a0e1a 55%) fixed; glassrgba(255,255,255,0.10)fill /1px solid rgba(255,255,255,0.20)border /backdrop-filter: blur(9px)(+-webkit-) /border-radius:16px; text#e7ecf6, muted#9fb0d0/#8aa0c8; accent#6ea8ff; green#34d399/#6ee7b7, amber#fbbf24, red#ef4444, media purple#c4b5fd. - Bubble-Card pop-up: a
type: custom:bubble-card/card_type: pop-up/hash: '#id'card placed in the view'scards:; opened by any tile withtap_action: { action: navigate, navigation_path: '#id' }. Pop-up content goes in itscards:. - Reference patterns already in the repo:
desk_companion.yaml(hero[[[ JS ]]]greeting+weather, glass chip row withchip(txt,col)helper) andplant_dashboard.yaml(glass tiles, conic rings). Implementers should open these for the exact working idioms. - Entity inventories are given per task. Slugs verified against
tests/fixtures/entities.txt.
Entity reference (verified)¶
- Climate:
climate.bedroom,climate.office; weatherweather.forecast_home,sensor.weather_forecast_daily; daylightsun.sun. - Occupancy (Ecobee room sensors):
binary_sensor.bedroom_1_occupancy,…_2_…,…_3_…,…_4_…,binary_sensor.bedroom_occupancy,binary_sensor.office_occupancy. - Power/energy:
sensor.whole_home_power,sensor.tou_period,sensor.tou_rate,input_boolean.peak_mode,sensor.energy_cost_today,sensor.energy_cost_this_month,sensor.energy_cost_accumulated; per-circuitsensor.em_power_*+ Refossem_channel_*(Panel 1 lighting) /a1..c6(Panel 2 appliances; AC condensers a5/a6/b5). - Garage:
cover.left_garage_door_opener_garage_door,cover.right_garage_door_opener_garage_door. - Leaks (~10):
binary_sensor.kitchen_sink_leak_detector,binary_sensor.office_sink_leak_detector,binary_sensor.master_bathroom_left_sink_leak_detector,binary_sensor.master_bathroom_right_sink_leak_detector,binary_sensor.bedroom_3_sink_leak_detector,binary_sensor.powder_room_leak_sensor,binary_sensor.laundry_room_leak_sensor,binary_sensor.attic_air_handler_leak_detector,binary_sensor.3rd_floor_furnace_leak_detector(ignore the*_openingduplicates). - Food flags:
input_boolean.food_alert_refrigerator,…_refrigerator_freezer,…_freezer,input_boolean.food_critical_refrigerator,…_refrigerator_freezer,…_freezer. - Cameras:
camera.garage_stickup_cam_1_live_view,camera.personnel_door_live_view,camera.wyze_cam_v3_1. - People:
person.louis,person.lindsay. - Media (~19): Sonos
media_player.bedroom_sonos_one,media_player.master_bathroom_sonos_one,media_player.master_bedroom_sonos_play_5; Apple TVmedia_player.office_apple_tv,media_player.master_bedroom_apple_tv,media_player.family_room_apple_tv; TVs/soundbarsmedia_player.family_room_tv,media_player.family_room_soundbar,media_player.office_soundbar,media_player.bedroom_tv,media_player.louis_office_samsung_smart_tv,media_player.henry_s_tv. - Plants (glance):
sensor.<slug>_moisture+input_number.plant_<slug>_red/_yellowfor the 7 plants (see plant dashboard spec); deep-link target dashboard path/plant-care.
File Structure¶
- Create
command_dashboard.yaml— the whole dashboard (5 views + shared pop-ups). Grows across Tasks 2–9. - Modify
configuration.yaml— register thecommand-centerdashboard. - Modify
scripts.yaml— quick-action scripts. - Modify
configuration_tou_addition.yaml— leak/food template sensors. - Modify
.github/workflows/ci.yml,tests/conftest.py,tests/fixtures/entities_allow.txt— wire the new file + forward-ref helpers. - Create
docs/command.md+ modifymkdocs.yml— manual page.
Task 1: Backend — template sensors + quick-action scripts¶
Files: - Modify: configuration_tou_addition.yaml (append template sensors) - Modify: scripts.yaml (append scripts) - Modify: tests/fixtures/entities_allow.txt (forward-ref the new entities)
- [ ] Step 1: Add the leak + food template sensors. Open
configuration_tou_addition.yaml, find its existingtemplate:block (the TOU sensors). Append these to the SAMEtemplate:list (as new- sensor:/- binary_sensor:entries matching the file's existing modern-template style). If the file uses the modern top-leveltemplate:list, add:
- binary_sensor:
- name: "Any Leak Wet"
unique_id: any_leak_wet
device_class: moisture
state: >
{{ [
states('binary_sensor.kitchen_sink_leak_detector'),
states('binary_sensor.office_sink_leak_detector'),
states('binary_sensor.master_bathroom_left_sink_leak_detector'),
states('binary_sensor.master_bathroom_right_sink_leak_detector'),
states('binary_sensor.bedroom_3_sink_leak_detector'),
states('binary_sensor.powder_room_leak_sensor'),
states('binary_sensor.laundry_room_leak_sensor'),
states('binary_sensor.attic_air_handler_leak_detector'),
states('binary_sensor.3rd_floor_furnace_leak_detector')
] | select('eq','on') | list | count > 0 }}
- sensor:
- name: "Leak Status"
unique_id: leak_status
state: >
{% set m = {
'binary_sensor.kitchen_sink_leak_detector':'Kitchen sink',
'binary_sensor.office_sink_leak_detector':'Office sink',
'binary_sensor.master_bathroom_left_sink_leak_detector':'Master bath L',
'binary_sensor.master_bathroom_right_sink_leak_detector':'Master bath R',
'binary_sensor.bedroom_3_sink_leak_detector':'Bedroom 3 sink',
'binary_sensor.powder_room_leak_sensor':'Powder room',
'binary_sensor.laundry_room_leak_sensor':'Laundry',
'binary_sensor.attic_air_handler_leak_detector':'Attic air handler',
'binary_sensor.3rd_floor_furnace_leak_detector':'3rd-floor furnace'
} %}
{% set names = m.items() | selectattr('0','is_state','on') | map(attribute='1') | list %}
{{ 'All dry' if names | count == 0 else names | join(', ') }}
- name: "Food Status"
unique_id: food_status
state: >
{% set crit = ['input_boolean.food_critical_refrigerator','input_boolean.food_critical_refrigerator_freezer','input_boolean.food_critical_freezer'] %}
{% set warn = ['input_boolean.food_alert_refrigerator','input_boolean.food_alert_refrigerator_freezer','input_boolean.food_alert_freezer'] %}
{% set anyc = crit | select('is_state','on') | list | count %}
{% set anyw = warn | select('is_state','on') | list | count %}
{{ 'Food OK' if anyc == 0 and anyw == 0 else ('Food CRITICAL' if anyc > 0 else 'Food warming') }}
If the file instead uses the legacy sensor:/binary_sensor: platform style, mirror that style instead — match the file. (is_state template fns make these refs visible to the test scanner; that's fine — all referenced entities exist.)
- [ ] Step 2: Add the quick-action scripts. Open
scripts.yaml, append:
all_lights_off:
alias: "All Lights Off"
icon: mdi:lightbulb-group-off
sequence:
- service: light.turn_off
target:
entity_id: all
good_night:
alias: "Good Night"
icon: mdi:weather-night
sequence:
- service: light.turn_off
target:
entity_id: all
- service: cover.close_cover
target:
entity_id:
- cover.left_garage_door_opener_garage_door
- cover.right_garage_door_opener_garage_door
movie_mode:
alias: "Movie Mode"
icon: mdi:movie-open
sequence:
- service: light.turn_off
target:
entity_id: all
(movie_mode keeps it simple/safe — all lights off; refine to a dim level once specific media-room light entities are confirmed. entity_id: all needs no enumeration and passes the entity-ref test.)
- [ ] Step 3: Forward-ref the new entities. Append to
tests/fixtures/entities_allow.txt:
# Command Dashboard backend (configuration_tou_addition.yaml template sensors +
# scripts.yaml) — created on next HA restart after deploy; allowlisted until the
# post-deploy snapshot. See docs/superpowers/specs/2026-06-17-command-dashboard-redesign-design.md
binary_sensor.any_leak_wet
sensor.leak_status
sensor.food_status
script.all_lights_off
script.good_night
script.movie_mode
-
[ ] Step 4: Gate. Run
make lint && make pytest. Expected: PASS. If a template references an entity the scanner flags, confirm the entity exists inentities.txt(all the leak/food/cover entities do). -
[ ] Step 5: Commit.
Task 2: Dashboard scaffold + shared hero¶
Files: - Modify: configuration.yaml:19-25 (register dashboard) - Create: command_dashboard.yaml
- [ ] Step 1: Register the dashboard. In
configuration.yaml, underlovelace.dashboards:(sibling ofdesk-companion:/plant-care:), add:
command-center:
mode: yaml
title: Command
icon: mdi:view-dashboard-variant
show_in_sidebar: true
filename: command_dashboard.yaml
- [ ] Step 2: Create
command_dashboard.yamlwith the header, kiosk block, and the Overview view containing ONLY the shared hero for now:
# Command — redesigned home dashboard (YAML mode, phone-first, Midnight Glass).
# Built in code. See docs/superpowers/specs/2026-06-17-command-dashboard-redesign-design.md
# Hybrid Hero: native HA tabs + hero + status chips; deep controls via Bubble-Card pop-ups.
# The old Home Command Dashboard (.storage) is intentionally left untouched.
title: Command
kiosk_mode:
kiosk: '{{ is_state("input_boolean.kiosk_mode", "on") }}'
views:
- title: Overview
path: overview
icon: mdi:home-variant
background: "radial-gradient(130% 80% at 50% -5%, #1a2440 0%, #0a0e1a 55%) fixed"
cards:
# ===================================================== HERO
- type: custom:button-card
entity: weather.forecast_home
show_icon: false
show_name: false
triggers_update: all
tap_action:
action: none
custom_fields:
body: |
[[[
const h = new Date().getHours();
const greet = h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
const ic = (c) => ({sunny:"☀️","clear-night":"🌙",partlycloudy:"⛅",cloudy:"☁️",rainy:"🌧️",pouring:"🌧️",lightning:"⛈️","lightning-rainy":"⛈️",snowy:"❄️","snowy-rainy":"🌨️",fog:"🌫️",hail:"🌨️",windy:"💨","windy-variant":"💨",exceptional:"☀️"}[c] || "⛅");
const f = states["weather.forecast_home"];
const out = Math.round(Number(f.attributes.temperature));
const fcs = states["sensor.weather_forecast_daily"];
const t = (fcs && fcs.attributes.forecast && fcs.attributes.forecast[0]) ? fcs.attributes.forecast[0] : null;
const hl = t ? `<span style="font-size:12px;color:#9fb0d0;margin-left:10px">H ${Math.round(t.temperature)}° · L ${Math.round(t.templow)}°</span>` : "";
return `<div style="padding:6px 4px">
<div style="font-size:24px;font-weight:300;letter-spacing:.5px;background:linear-gradient(120deg,#fff,#9db4ff);-webkit-background-clip:text;background-clip:text;color:transparent">${greet}, Louis</div>
<div style="display:flex;align-items:center;gap:10px;margin-top:7px"><span style="font-size:26px">${ic(f.state)}</span><span style="font-size:26px;font-weight:300;color:#e7ecf6">${out}°</span>${hl}</div>
</div>`;
]]]
styles:
card:
- background: rgba(255,255,255,0.10)
- border: 1px solid rgba(255,255,255,0.20)
- border-radius: 16px
- backdrop-filter: blur(9px)
- "-webkit-backdrop-filter": blur(9px)
- box-shadow: none
- padding: 12px
grid:
- grid-template-areas: '"body"'
custom_fields:
body:
- justify-self: stretch
-
[ ] Step 3: Gate.
make lint && make pytest→ PASS.python3 -c "import yaml; d=yaml.safe_load(open('command_dashboard.yaml')); print('views',len(d['views']))"→views 1. -
[ ] Step 4: Commit.
Task 3: Overview — status chip row (establishes chip + pop-up-trigger pattern)¶
Files: Modify command_dashboard.yaml (append a chip-row button-card after the hero in the Overview cards:).
The chip row is a single display-only custom:button-card whose chips custom_field renders status pills via a chip(txt,col) helper (adapted from desk_companion.yaml). It is glance-only — tap_action: none. (button-card can't route taps per-chip, so deep controls live on dedicated tappable tiles instead: the ♪ media launcher in Task 5 and the leak tile in the Security tab.) This keeps the chip row simple and robust.
- [ ] Step 1: Append the chip row (adapts the proven
desk_companion.yamlhouse-strip):
# ===================================================== STATUS CHIPS
- type: custom:button-card
show_icon: false
show_name: false
triggers_update: all
tap_action:
action: none
custom_fields:
chips: |
[[[
const chip = (txt, col) => `<span style="display:inline-flex;align-items:center;gap:6px;font-size:12.5px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.13);border-radius:20px;padding:7px 12px;color:#e7ecf6"><span style="color:${col}">●</span>${txt}</span>`;
const gOpen = ["cover.left_garage_door_opener_garage_door","cover.right_garage_door_opener_garage_door"].some(e => states[e] && states[e].state === "open");
const garage = chip(gOpen ? "Garage OPEN" : "Garage closed", gOpen ? "#ef4444" : "#6ee7b7");
const home = ["person.louis","person.lindsay"].filter(e => states[e] && states[e].state === "home").map(e => { const n = e.split(".")[1].split("_")[0]; return n.charAt(0).toUpperCase()+n.slice(1); });
const who = chip(home.length ? home.join(" + ") + " home" : "Away", home.length ? "#6ee7b7" : "#9fb0d0");
const leakS = states["sensor.leak_status"] ? states["sensor.leak_status"].state : "All dry";
const leak = chip(leakS === "All dry" ? "All dry" : leakS.toUpperCase(), leakS === "All dry" ? "#6ee7b7" : "#ef4444");
const foodS = states["sensor.food_status"] ? states["sensor.food_status"].state : "Food OK";
const food = chip(foodS, foodS === "Food OK" ? "#6ee7b7" : (foodS.includes("CRITICAL") ? "#ef4444" : "#fbbf24"));
const kwRaw = states["sensor.whole_home_power"] ? Number(states["sensor.whole_home_power"].state) : null;
const peak = states["sensor.tou_period"] && states["sensor.tou_period"].state === "peak";
const pwr = kwRaw == null || isNaN(kwRaw) ? chip("Power —","#9fb0d0") : chip(`${kwRaw.toFixed(1)} kW · ${peak ? "PEAK" : "off-peak"}`, peak ? "#fbbf24" : "#6ee7b7");
const playing = Object.keys(states).filter(e => e.startsWith("media_player.") && states[e] && states[e].state === "playing");
const media = playing.length ? chip(`♪ ${playing.length} playing`, "#c4b5fd") : "";
const thirsty = ["monstera_3","monstera_2","monstera_1","bird_of_paradise","fiddle_leaf_1","fiddle_leaf_2","maury_river_fiddle_leaf"].filter(s => { const m = states["sensor."+s+"_moisture"], r = states["input_number.plant_"+s+"_red"]; return m && r && Number(m.state) < Number(r.state); });
const plants = chip(thirsty.length ? `🪴 ${thirsty.length} thirsty` : "🪴 Plants OK", thirsty.length ? "#fbbf24" : "#6ee7b7");
return `<div style="display:flex;gap:9px;flex-wrap:wrap;align-items:center">${garage}${who}${leak}${food}${pwr}${media}${plants}</div>`;
]]]
styles:
card:
- background: none
- border: none
- box-shadow: none
- padding: 4px 2px
- margin-top: 10px
grid:
- grid-template-areas: '"chips"'
custom_fields:
chips:
- justify-self: stretch
-
[ ] Step 2: Gate.
make lint && make pytest→ PASS. (The chip JS referencessensor.leak_status/sensor.food_statusviastates["..."]bracket syntax — not caught by the scanner — and the bracket entities are covered by the allowlist/registry anyway.) -
[ ] Step 3: Commit.
Task 4: Overview — glances, quick actions, and Bubble pop-ups (thermostat ×2, garage)¶
Files: Modify command_dashboard.yaml (append to Overview cards:).
- [ ] Step 1: Append the Climate + Power glance grid. A 2-col
gridwith two thermostat ring tiles (tap →#thermo-bedroom/#thermo-office) and a power tile with a 24h sparkline. Build the thermostat tiles using the conic-ring tile pattern fromplant_dashboard.yaml(read it for the exactbutton-cardring idiom), bindingclimate.bedroom/climate.officeattributes.current_temperature, ring color#34d399(at/over setpoint band) else#fbbf24, sub-line fromattributes.hvac_action. Set each tile'stap_action: { action: navigate, navigation_path: '#thermo-bedroom' }(resp. office). The power tile showssensor.whole_home_power+ acustom:apexcharts-card24h sparkline (graph_span: 24h,span: {start: day}, sparkline mode,extend_to: now, fixed height ~70px) andtap_action: { action: navigate, navigation_path: '/command-center/energy' }.
# ===================================================== CLIMATE + POWER GLANCE
- type: grid
columns: 2
square: false
cards:
- type: custom:button-card
entity: climate.bedroom
show_icon: false
show_name: false
tap_action:
action: navigate
navigation_path: '#thermo-bedroom'
custom_fields:
body: |
[[[
const c = states["climate.bedroom"];
const t = Math.round(Number(c.attributes.current_temperature));
const act = c.attributes.hvac_action || c.state;
const col = act === "heating" ? "#fb923c" : act === "cooling" ? "#6ea8ff" : "#34d399";
return `<div style="position:relative;height:100%;display:flex;align-items:center;gap:12px">
<div style="width:54px;height:54px;border-radius:50%;background:conic-gradient(${col} 0 ${Math.min(t,100)}%,rgba(255,255,255,.15) ${Math.min(t,100)}%);display:flex;align-items:center;justify-content:center;flex:0 0 auto"><div style="width:42px;height:42px;border-radius:50%;background:#0d1426;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:600">${t}°</div></div>
<div><div style="font-size:10px;color:#8aa0c8;text-transform:uppercase;letter-spacing:1px">Bedroom</div><div style="font-size:11px;color:#9fb0d0;margin-top:3px">${act}</div></div>
</div>`;
]]]
styles:
card: [ {background: rgba(255,255,255,0.10)}, {border: 1px solid rgba(255,255,255,0.20)}, {border-radius: 16px}, {backdrop-filter: blur(9px)}, {"-webkit-backdrop-filter": blur(9px)}, {box-shadow: none}, {height: 92px}, {padding: 13px}, {box-sizing: border-box} ]
grid: [ {grid-template-areas: '"body"'} ]
custom_fields: { body: [ {justify-self: stretch}, {align-self: stretch} ] }
# Office thermostat tile — identical block, substitute climate.office / "Office" / hash '#thermo-office'
# Power tile — whole_home_power value + apexcharts 24h sparkline, tap → navigate '/command-center/energy'
climate.office, label Office, hash #thermo-office. Build the Power tile per the description above (full apexcharts sparkline idiom is in desk_companion.yaml's energy graph and plant_dashboard history — read those; use sensor.whole_home_power). - [ ] Step 2: Append the Quick Actions row — a 2×2 (or 1×4) grid of glass
button-cards calling the Task-1 scripts + garage. The morning/evening "prominent" swap: the first tile's label/icon/script switches bynew Date().getHours()in[[[ JS ]]](e.g. evening → Good Night, morning → All Lights Off as the hero action). Each tiletap_action: { action: call-service, service: script.<name> }; the garage tile usestap_action: navigate → '#garage'.
# ===================================================== QUICK ACTIONS
- type: grid
columns: 4
square: false
cards:
- type: custom:button-card
name: Good Night
icon: mdi:weather-night
tap_action: { action: call-service, service: script.good_night }
styles:
card: [ {background: rgba(255,255,255,0.06)}, {border: 1px solid rgba(255,255,255,0.13)}, {border-radius: 13px}, {box-shadow: none}, {height: 78px} ]
name: [ {font-size: 10px}, {color: "#cdd9f0"} ]
icon: [ {color: "#c4b5fd"}, {width: 22px} ]
# "All Lights Off" (mdi:lightbulb-group-off → script.all_lights_off),
# "Movie" (mdi:movie-open → script.movie_mode),
# "Garage" (mdi:garage → tap_action navigate '#garage') — same block, substitute name/icon/action.
script.all_lights_off; Movie → script.movie_mode; Garage → navigate #garage). - [ ] Step 3: Append the Bubble-Card pop-ups for
#thermo-bedroom,#thermo-office,#garageat the END of the Overviewcards:. Thermostat pop-ups are observe-only (Ecobee owns HVAC): show mode, current temp, setpoints, humidity, hvac_action via amushroom-climate-cardin read context or abutton-cardreadout — NO setpoint write services. Garage pop-up has twomushroom-cover-cards (open/close/stop) for the two garage covers.
# ===================================================== POP-UPS
- type: custom:bubble-card
card_type: pop-up
hash: '#thermo-bedroom'
show_header: true
name: Bedroom Thermostat
icon: mdi:thermometer
cards:
- type: custom:mushroom-climate-card
entity: climate.bedroom
hvac_modes: [] # empty = display modes read-only; no control row
show_temperature_control: false
collapsible_controls: false
- type: custom:bubble-card
card_type: pop-up
hash: '#thermo-office'
name: Office Thermostat
icon: mdi:thermometer
cards:
- type: custom:mushroom-climate-card
entity: climate.office
hvac_modes: []
show_temperature_control: false
- type: custom:bubble-card
card_type: pop-up
hash: '#garage'
name: Garage Doors
icon: mdi:garage
cards:
- type: custom:mushroom-cover-card
entity: cover.left_garage_door_opener_garage_door
name: Left Garage
show_buttons_control: true
- type: custom:mushroom-cover-card
entity: cover.right_garage_door_opener_garage_door
name: Right Garage
show_buttons_control: true
-
[ ] Step 4: Gate.
make lint && make pytest→ PASS.python3 -c "import yaml; yaml.safe_load(open('command_dashboard.yaml')); print('ok')". -
[ ] Step 5: Commit.
Task 5: Media-hub Bubble pop-up + ♪ trigger¶
Files: Modify command_dashboard.yaml (add a media pop-up + a small ♪ launcher tile to the Overview).
- [ ] Step 1: Add a
#mediaBubble pop-up (near the other pop-ups) containing groupedcustom:mini-media-playercards. Group by type with small title cards;mini-media-playergives transport/volume/source and (Sonos)speaker_group.
- type: custom:bubble-card
card_type: pop-up
hash: '#media'
name: Media
icon: mdi:speaker-multiple
cards:
- type: custom:mushroom-title-card
title: Sonos
- type: custom:mini-media-player
entity: media_player.master_bedroom_sonos_play_5
group: true
hide: { power: true }
speaker_group:
platform: sonos
show_group_count: true
entities:
- entity_id: media_player.master_bedroom_sonos_play_5
name: Play 5
- entity_id: media_player.bedroom_sonos_one
name: Bedroom
- entity_id: media_player.master_bathroom_sonos_one
name: Master Bath
- type: custom:mini-media-player
entity: media_player.bedroom_sonos_one
group: true
- type: custom:mini-media-player
entity: media_player.master_bathroom_sonos_one
group: true
- type: custom:mushroom-title-card
title: Apple TV / AirPlay
- type: custom:mini-media-player
entity: media_player.office_apple_tv
- type: custom:mini-media-player
entity: media_player.master_bedroom_apple_tv
- type: custom:mini-media-player
entity: media_player.family_room_apple_tv
- type: custom:mushroom-title-card
title: TVs & Soundbars
- type: custom:mini-media-player
entity: media_player.family_room_tv
- type: custom:mini-media-player
entity: media_player.family_room_soundbar
- type: custom:mini-media-player
entity: media_player.office_soundbar
- [ ] Step 2: Add a ♪ launcher tile to the Overview (a slim glass
button-card,tap_action: navigate → '#media', label "Media", showing what's playing via[[[ JS ]]]scanningmedia_player.*states). Place it right after the chip row.
- type: custom:button-card
show_icon: false
show_name: false
triggers_update: all
tap_action: { action: navigate, navigation_path: '#media' }
custom_fields:
body: |
[[[
const playing = Object.keys(states).filter(e => e.startsWith("media_player.") && states[e].state === "playing");
const label = playing.length ? `${playing.length} playing — tap to control` : "Nothing playing — tap for media";
return `<div style="display:flex;align-items:center;gap:9px;color:#cdd9f0;font-size:12.5px"><span style="color:#c4b5fd">♪</span><span>${label}</span><span style="margin-left:auto;color:#6b7a99">▸</span></div>`;
]]]
styles:
card: [ {background: rgba(167,139,250,0.10)}, {border: 1px solid rgba(167,139,250,0.30)}, {border-radius: 13px}, {box-shadow: none}, {padding: 11px}, {margin-top: 10px} ]
grid: [ {grid-template-areas: '"body"'} ]
custom_fields: { body: [ {justify-self: stretch} ] }
-
[ ] Step 3: Gate.
make lint && make pytest→ PASS. -
[ ] Step 4: Commit.
Task 6: Climate tab¶
Files: Modify command_dashboard.yaml (add a second view).
- [ ] Step 1: Add the Climate view after the Overview view (same
background,icon: mdi:thermometer,path: climate). Contents: - Reuse the shared hero (copy the hero
button-cardfrom Overview; greeting+weather is fine on every tab). - Per-room temp tiles — a 2-col
gridof conic-ring tiles forclimate.bedroomandclimate.office(reuse the ring tile from Task 4 Step 1, but withtap_action: navigateto the existing#thermo-bedroom/#thermo-officepop-ups — pop-ups are global to the dashboard file so they work from any view). Add an occupancy badge to each tile via[[[ JS ]]]: read the matchingbinary_sensor.*_occupancy(office →binary_sensor.office_occupancy; bedroom →binary_sensor.bedroom_occupancy) and render a small "● occupied"/"○ empty" pill. - Differential / "why" insight card — a glass
button-cardwhose[[[ JS ]]]computesbedroom.current_temperature - office.current_temperatureand annotates with occupancy + each thermostat'shvac_action, e.g.Bedroom 71° · Office 73° (+2°) · Office occupied · both idle. Observe-only. - Temp trend — a
custom:mini-graph-card(or apexcharts) plottingclimate.bedroom/climate.officecurrent_temperatureover 24h (mini-graph supportsattribute: current_temperature), glass-framed viacard_mod. -
Food-temp detail — glass tiles for the garage fridge/freezer temperature sensors (find the exact
sensor.*food-temp entities inentities.txt— searchfridge/freezer/food; bind current temp + a colored state). Summary chip already lives on Overview. Do NOT add any CO₂/VOC cards. -
[ ] Step 2: Gate.
make lint && make pytest→ PASS.grep -c "path:" command_dashboard.yaml→ 2 views. -
[ ] Step 3: Commit.
Task 7: Energy tab¶
Files: Modify command_dashboard.yaml (add a third view, path: energy, icon: mdi:lightning-bolt).
-
[ ] Step 1: Inventory the source. Read the current dashboard's Energy view to see exactly what's there:
ssh root@192.168.4.93 "cat /config/.storage/lovelace.home_command_dashboard"→python3to dumpdata.config.views[1](the Energy view) card list. The new tab keeps the same data, reskinned. -
[ ] Step 2: Build the Energy view (hero + glass sections), covering — TOU status (
sensor.tou_period,sensor.tou_rate,input_boolean.peak_mode), live + monthly cost (sensor.energy_cost_today,sensor.energy_cost_this_month,sensor.energy_cost_accumulated), whole-home power (sensor.whole_home_power) with an apexcharts day-graph, per-circuit breakdown (thesensor.em_power_*and Refossem_channel_*/a1..c6circuits — mirror the current tab's groupings), and peak-appliance status. Use glassbutton-cardtiles +apexcharts-card/mini-graph-cardfor graphs (idioms indesk_companion.yaml). Keep ALL the data — this tab is curation-exempt per the spec. -
[ ] Step 3: Gate + Commit.
make lint && make pytest→ PASS.
Task 8: Security tab¶
Files: Modify command_dashboard.yaml (add a fourth view, path: security, icon: mdi:shield-home).
- [ ] Step 1: Build the Security view (hero + sections):
- Room presence panel (featured) — a grid of glass tiles, one per occupancy sensor (
binary_sensor.bedroom_1_occupancy.._4_occupancy,binary_sensor.bedroom_occupancy,binary_sensor.office_occupancy), each green ● whenon. Design the grid so new presence sensors (future mmWave) drop in as more tiles — note this in a YAML comment. - Who's home —
person.louis,person.lindsayasmushroom-person-cards or glass tiles (home/away + since). - Doors & garage — a tile opening the existing
#garagepop-up; plus any door binary_sensors as status tiles. - Leak summary — a glass tile bound to
sensor.leak_status(green "All dry" / red wet list),tap_action: navigate → '#leaks'. - Add a
#leaksBubble pop-up listing all ~10 leak detectors (from the Entity reference) asmushroom-entity-card/entitiesrows with dry/wet state. -
Front-door camera glance — a
picture-glance/picture-entityofcamera.personnel_door_live_view,tap_action: navigate → '/command-center/cameras'. No battery cards. -
[ ] Step 2: Gate + Commit.
make lint && make pytest→ PASS.
Task 9: Cameras tab¶
Files: Modify command_dashboard.yaml (add a fifth view, path: cameras, icon: mdi:cctv).
- [ ] Step 1: Build the Cameras view (hero optional/omit — cameras want space):
- Live grid — a 2-col
gridof the 3 cameras (camera.garage_stickup_cam_1_live_view,camera.personnel_door_live_view,camera.wyze_cam_v3_1) aspicture-glancecards (live thumbnail, name overlay), eachtap_action: { action: more-info }(fullscreen live). Add a YAML comment that Tapo cameras append here as their entities are created. -
Recent-motion rail — a horizontal
grid/stack of the cameras' motion/last-event entities. Use what's available: Ring exposesbinary_sensor.*_motionand event image entities; render the most-recent-motion cameras with a timestamp via[[[ JS ]]]reading the relevantbinary_sensor.*_motionlast_changed. Cameras without event data still show in the live grid. -
[ ] Step 2: Gate.
make lint && make pytest→ PASS.grep -c "path:" command_dashboard.yaml→ 5 views. -
[ ] Step 3: Commit.
Task 10: Wire into CI + test suite¶
Files: Modify .github/workflows/ci.yml, tests/conftest.py.
- [ ] Step 1: ci.yml change-filter (~line 56): add
|command_dashboardto the alternation so it becomes…|desk_companion|plant_dashboard|command_dashboard)\.yaml$. - [ ] Step 2: ci.yml
HA_FILES(~line 113): appendcommand_dashboard.yaml. - [ ] Step 3: conftest.py
all_referenced_entities(): add"command_dashboard.yaml": extract_yaml_refs("command_dashboard.yaml", bare_scan=True),(bare_scan because it usesentity:/states['...'], like the other glass dashboards). - [ ] Step 4: Gate.
make lint && make pytest→ PASS. Iftest_entity_referencesflags amedia_player.*/camera.*/binary_sensor.*ref, confirm the slug matchesentities.txtexactly; if it's a new template/script entity, confirm it's inentities_allow.txt(Task 1). - [ ] Step 5: Commit.
Task 11: Manual page¶
Files: Create docs/command.md; modify mkdocs.yml (nav).
- [ ] Step 1: Read
mkdocs.ymlnav + one existing page (e.g.docs/climate.md) to match voice/structure. - [ ] Step 2: Write
docs/command.md— a household-facing page: what the Command dashboard is, the 5 tabs, the status chips (what each color means), and the pop-ups (tap a thermostat/garage/leak/♪ media). Plain language, tables,!!! tipadmonitions; no broken internal links. - [ ] Step 3: Add to
mkdocs.ymlnav under the appropriate section. - [ ] Step 4: Build.
mkdocs build --strict(orpython -m mkdocs build --strict). Must PASS. If mkdocs isn't installed locally, note it;docs.ymlCI validates on push. - [ ] Step 5: Commit.
Task 12: Deploy, verify, snapshot¶
Files: none (push + verify + snapshot).
- [ ] Step 1: Full suite.
make test(ormake lint && make pytestif Docker absent) → PASS. - [ ] Step 2: Push + watch CI. Expected:
git push gh run watch $(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId') --exit-statustestgreen;deploy-haruns (config changed → restart). Also confirmdocs.ymlis green. - [ ] Step 3: Confirm HA up.
ssh root@192.168.4.93 "ha core logs --lines 20"→ no fatal errors; core running. Spot-check a new entity loaded:mcp__homeassistant__ha_get_stateforsensor.leak_status. - [ ] Step 4: On-device visual check (Louis). Open Command (
/command-center) on a phone: all 5 tabs render in Midnight Glass; hero + chips correct; thermostat/garage/leak/media pop-ups open and (media) control players; chips color by state; no CO₂/VOC or batteries anywhere; old dashboard still intact. Iterate oncommand_dashboard.yamlfor any pixel issues and re-push. - [ ] Step 5: Snapshot + drop allowlist. Once HA restarted and the new template/script entities are live:
make snapshot, then remove the 6 Command-backend lines fromtests/fixtures/entities_allow.txt.make pytest→ PASS. - [ ] Step 6: Commit + push.
Self-Review (against the spec)¶
Spec coverage: Midnight Glass/Hybrid Hero/native tabs → Tasks 2–9. Hero + chip system → Tasks 2–3. Overview (glances, time-aware quick actions, food/leak/plant/media chips, pop-ups) → Tasks 3–5. Climate (temps+trends+differentials+occupancy, no CO₂/VOC, observe-only thermostats, food-temp detail) → Task 6. Energy (all data) → Task 7. Security (Ecobee presence featured + future headroom, who's home, doors/garage, leak summary+pop-up, cam glance, no batteries) → Task 8. Cameras (grid + motion rail, scales to Tapo) → Task 9. Media hub (Sonos+AppleTV+AirPlay pop-up) → Task 5. Backend scripts + leak/food templates → Task 1. YAML-mode/kiosk/registration → Task 2. CI/test/allowlist/snapshot → Tasks 1,10,12. Manual → Task 11. Old dashboard untouched (only adds a new file + new sibling registration) → ✓. Deferred items (Media tab, Garden, batteries, CO₂/VOC, Health/Morning/Sleep, smart-lock action) correctly absent.
Placeholder scan: Tasks 6–9 specify content by exact entities + which in-file pattern to reuse rather than re-pasting the full hero/tile/pop-up code (which is committed in Tasks 2–5 and read from the file) — this is deliberate reuse, not a placeholder; each such task names the source pattern, the exact entities, and the gates. The one genuine lookup ("find the exact food-temp sensor.* entities", "dump the current Energy view") is an explicit, bounded discovery step with the command to run, not a vague TODO.
Type/name consistency: Dashboard path command-center, file command_dashboard.yaml, pop-up hashes #thermo-bedroom/#thermo-office/#garage/#media/#leaks, template entities sensor.leak_status/sensor.food_status/binary_sensor.any_leak_wet, scripts script.good_night/script.all_lights_off/script.movie_mode — used identically across Tasks 1–12. Navigation paths use /command-center/<view-path> for cross-tab and #hash for pop-ups, consistently.