Skip to content

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. .yamllint disables line-length (long [[[ JS ]]] lines are fine).
  • Gates per task: make lint (yamllint + JSON/Grafana guards) and make pytest (20+ structural/entity-ref tests) must pass before commit. make test additionally runs container check_config (needs Docker; if unavailable locally, CI runs it).
  • Commit after each task: auto: <what changed> + trailer Co-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; glass rgba(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's cards:; opened by any tile with tap_action: { action: navigate, navigation_path: '#id' }. Pop-up content goes in its cards:.
  • Reference patterns already in the repo: desk_companion.yaml (hero [[[ JS ]]] greeting+weather, glass chip row with chip(txt,col) helper) and plant_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; weather weather.forecast_home, sensor.weather_forecast_daily; daylight sun.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-circuit sensor.em_power_* + Refoss em_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 *_opening duplicates).
  • 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 TV media_player.office_apple_tv, media_player.master_bedroom_apple_tv, media_player.family_room_apple_tv; TVs/soundbars media_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/_yellow for 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 the command-center dashboard.
  • 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 + modify mkdocs.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 existing template: block (the TOU sensors). Append these to the SAME template: list (as new - sensor: / - binary_sensor: entries matching the file's existing modern-template style). If the file uses the modern top-level template: 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 in entities.txt (all the leak/food/cover entities do).

  • [ ] Step 5: Commit.

    git add configuration_tou_addition.yaml scripts.yaml tests/fixtures/entities_allow.txt
    git commit -m "auto: command backend — leak_status/food_status/any_leak templates + Good Night/All Off/Movie scripts
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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, under lovelace.dashboards: (sibling of desk-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.yaml with 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.

    git add configuration.yaml command_dashboard.yaml
    git commit -m "auto: command_dashboard.yaml — scaffold + register command-center dashboard + shared hero
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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.yaml house-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 references sensor.leak_status/sensor.food_status via states["..."] bracket syntax — not caught by the scanner — and the bracket entities are covered by the allowlist/registry anyway.)

  • [ ] Step 3: Commit.

    git add command_dashboard.yaml
    git commit -m "auto: command Overview — status chip row (garage/presence/leak/food/power/media/plants)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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 grid with 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 from plant_dashboard.yaml (read it for the exact button-card ring idiom), binding climate.bedroom/climate.office attributes.current_temperature, ring color #34d399 (at/over setpoint band) else #fbbf24, sub-line from attributes.hvac_action. Set each tile's tap_action: { action: navigate, navigation_path: '#thermo-bedroom' } (resp. office). The power tile shows sensor.whole_home_power + a custom:apexcharts-card 24h sparkline (graph_span: 24h, span: {start: day}, sparkline mode, extend_to: now, fixed height ~70px) and tap_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'
Build the Office tile by copying the Bedroom tile and substituting 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 by new Date().getHours() in [[[ JS ]]] (e.g. evening → Good Night, morning → All Lights Off as the hero action). Each tile tap_action: { action: call-service, service: script.<name> }; the garage tile uses tap_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.
Build the other three tiles per the comment (All Lights Off → script.all_lights_off; Movie → script.movie_mode; Garage → navigate #garage).

  • [ ] Step 3: Append the Bubble-Card pop-ups for #thermo-bedroom, #thermo-office, #garage at the END of the Overview cards:. Thermostat pop-ups are observe-only (Ecobee owns HVAC): show mode, current temp, setpoints, humidity, hvac_action via a mushroom-climate-card in read context or a button-card readout — NO setpoint write services. Garage pop-up has two mushroom-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.

    git add command_dashboard.yaml
    git commit -m "auto: command Overview — climate/power glances, quick actions, thermostat+garage pop-ups
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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 #media Bubble pop-up (near the other pop-ups) containing grouped custom:mini-media-player cards. Group by type with small title cards; mini-media-player gives 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 ]]] scanning media_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.

    git add command_dashboard.yaml
    git commit -m "auto: command — media-hub Bubble pop-up (Sonos+AppleTV+AirPlay) + ♪ launcher tile
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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-card from Overview; greeting+weather is fine on every tab).
  • Per-room temp tiles — a 2-col grid of conic-ring tiles for climate.bedroom and climate.office (reuse the ring tile from Task 4 Step 1, but with tap_action: navigate to the existing #thermo-bedroom/#thermo-office pop-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 matching binary_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-card whose [[[ JS ]]] computes bedroom.current_temperature - office.current_temperature and annotates with occupancy + each thermostat's hvac_action, e.g. Bedroom 71° · Office 73° (+2°) · Office occupied · both idle. Observe-only.
  • Temp trend — a custom:mini-graph-card (or apexcharts) plotting climate.bedroom/climate.office current_temperature over 24h (mini-graph supports attribute: current_temperature), glass-framed via card_mod.
  • Food-temp detail — glass tiles for the garage fridge/freezer temperature sensors (find the exact sensor.* food-temp entities in entities.txt — search fridge/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.

    git add command_dashboard.yaml
    git commit -m "auto: command — Climate tab (room temps+occupancy, differential insight, trends, food-temp; no CO2/VOC)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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"python3 to dump data.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 (the sensor.em_power_* and Refoss em_channel_*/a1..c6 circuits — mirror the current tab's groupings), and peak-appliance status. Use glass button-card tiles + apexcharts-card/mini-graph-card for graphs (idioms in desk_companion.yaml). Keep ALL the data — this tab is curation-exempt per the spec.

  • [ ] Step 3: Gate + Commit. make lint && make pytest → PASS.

    git add command_dashboard.yaml
    git commit -m "auto: command — Energy tab (all TOU/cost/power/per-circuit data, reskinned to glass)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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 ● when on. Design the grid so new presence sensors (future mmWave) drop in as more tiles — note this in a YAML comment.
  • Who's homeperson.louis, person.lindsay as mushroom-person-cards or glass tiles (home/away + since).
  • Doors & garage — a tile opening the existing #garage pop-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 #leaks Bubble pop-up listing all ~10 leak detectors (from the Entity reference) as mushroom-entity-card/entities rows with dry/wet state.
  • Front-door camera glance — a picture-glance/picture-entity of camera.personnel_door_live_view, tap_action: navigate → '/command-center/cameras'. No battery cards.

  • [ ] Step 2: Gate + Commit. make lint && make pytest → PASS.

    git add command_dashboard.yaml
    git commit -m "auto: command — Security tab (room presence, who's home, doors, leak summary+pop-up, cam glance)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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 grid of the 3 cameras (camera.garage_stickup_cam_1_live_view, camera.personnel_door_live_view, camera.wyze_cam_v3_1) as picture-glance cards (live thumbnail, name overlay), each tap_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 exposes binary_sensor.*_motion and event image entities; render the most-recent-motion cameras with a timestamp via [[[ JS ]]] reading the relevant binary_sensor.*_motion last_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.

    git add command_dashboard.yaml
    git commit -m "auto: command — Cameras tab (live grid + recent-motion rail; scales to Tapo)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    


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_dashboard to the alternation so it becomes …|desk_companion|plant_dashboard|command_dashboard)\.yaml$.
  • [ ] Step 2: ci.yml HA_FILES (~line 113): append command_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 uses entity:/states['...'], like the other glass dashboards).
  • [ ] Step 4: Gate. make lint && make pytest → PASS. If test_entity_references flags a media_player.*/camera.*/binary_sensor.* ref, confirm the slug matches entities.txt exactly; if it's a new template/script entity, confirm it's in entities_allow.txt (Task 1).
  • [ ] Step 5: Commit.
    git add .github/workflows/ci.yml tests/conftest.py
    git commit -m "auto: CI/tests — wire command_dashboard.yaml into deploy + bare_scan entity-ref check
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    

Task 11: Manual page

Files: Create docs/command.md; modify mkdocs.yml (nav).

  • [ ] Step 1: Read mkdocs.yml nav + 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, !!! tip admonitions; no broken internal links.
  • [ ] Step 3: Add to mkdocs.yml nav under the appropriate section.
  • [ ] Step 4: Build. mkdocs build --strict (or python -m mkdocs build --strict). Must PASS. If mkdocs isn't installed locally, note it; docs.yml CI validates on push.
  • [ ] Step 5: Commit.
    git add docs/command.md mkdocs.yml
    git commit -m "auto: docs — add Command dashboard manual page (tabs, chips, pop-ups)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    

Task 12: Deploy, verify, snapshot

Files: none (push + verify + snapshot).

  • [ ] Step 1: Full suite. make test (or make lint && make pytest if Docker absent) → PASS.
  • [ ] Step 2: Push + watch CI.
    git push
    gh run watch $(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId') --exit-status
    
    Expected: test green; deploy-ha runs (config changed → restart). Also confirm docs.yml is 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_state for sensor.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 on command_dashboard.yaml for 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 from tests/fixtures/entities_allow.txt. make pytest → PASS.
  • [ ] Step 6: Commit + push.
    git add tests/fixtures/entities.txt tests/fixtures/entities_allow.txt
    git commit -m "auto: tests — snapshot command backend entities, drop their allowlist entries
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
    git 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.