Skip to content

Plant Dashboard ("Lindsay's Plants") 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 standalone, iPhone-portrait, Midnight-Glass Home Assistant dashboard for indoor-plant watering, where each plant tile's moisture ring shows absolute moisture by fill and per-plant urgency by color, with a settings drawer to tune each plant's thresholds.

Architecture: A new YAML-mode Lovelace dashboard (plant_dashboard.yaml) registered in configuration.yaml, backed by 14 input_number + 1 input_boolean helpers (also in configuration.yaml). Tiles are custom:button-cards (photo/gradient background + HTML ring/name overlay) stacked with a custom:apexcharts-card sparkline; the drawer uses custom:range-slider-card per plant. Deployed via the existing CI pipeline (push to main → restart).

Tech Stack: Home Assistant YAML, Lovelace, custom:button-card, custom:apexcharts-card, custom:card-mod, custom:range-slider-card, GitHub Actions CI, pytest test suite.

Reference spec: docs/superpowers/specs/2026-06-17-plant-dashboard-design.md


Plant Roster (the single source of truth for all tasks)

Every per-plant task substitutes these values. <slug>, <Name> (display), <hue> (placeholder gradient), <red>/<yellow> (default thresholds):

# slug Name hue red yellow
1 monstera_3 Monstera 3 130 20 30
2 monstera_2 Monstera 2 135 20 30
3 monstera_1 Monstera 1 140 20 30
4 bird_of_paradise Bird of Paradise 95 25 35
5 fiddle_leaf_1 Fiddle Leaf 1 120 30 40
6 fiddle_leaf_2 Fiddle Leaf 2 115 30 40
7 maury_river_fiddle_leaf Maury River Fiddle 110 30 40

Moisture entity per plant: sensor.<slug>_moisture (these already exist in tests/fixtures/entities.txt). Threshold helpers: input_number.plant_<slug>_red, input_number.plant_<slug>_yellow. Drawer toggle: input_boolean.plant_settings_open.

File Structure

  • Create plant_dashboard.yaml — the entire dashboard (header, gallery grid, settings drawer). One file, ~250 lines.
  • Modify configuration.yaml — add 14 input_number + 1 input_boolean helpers; register the plant-care dashboard.
  • Modify .github/workflows/ci.yml — add plant_dashboard to the change-filter regex and HA_FILES.
  • Modify tests/conftest.py — add plant_dashboard.yaml to the entity-reference scan.
  • Modify tests/fixtures/entities_allow.txt — allowlist the 15 new helpers until the post-deploy snapshot.
  • Modify docs/garden.md — household-facing manual section for the new dashboard.

Conventions for every task

  • Run lint/tests from the repo root: /Users/pk/code/homeassistant.
  • YAML indent is 2 spaces; yamllint runs in make lint. No tabs, no trailing whitespace.
  • Commit after each task with a auto: <what changed> subject and the trailer: Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • Do not push until Task 9. Each prior task commits locally only.

Task 1: Add the threshold + drawer helpers to configuration.yaml

Files: - Modify: configuration.yaml (the input_boolean: block starts at line 39; add a new input_number: block after it) - Modify: tests/fixtures/entities_allow.txt (append)

  • [ ] Step 1: Add the drawer toggle to the existing input_boolean: block

In configuration.yaml, inside the existing input_boolean: block (after the last food helper, currently food_critical_freezer: ending at line 72), add:

  # Plant Dashboard (plant_dashboard.yaml) — drawer toggle. On = show the
  # per-plant watering-threshold sliders below the gallery. Glance-only otherwise.
  plant_settings_open:
    name: "Plant Settings Drawer"
    icon: mdi:tune
  • [ ] Step 2: Add a new input_number: block for the 14 threshold helpers

There is currently no input_number: top-level key. Add this block immediately after the input_boolean: block (before input_button: at line 74):

# Plant Dashboard watering thresholds — per-plant red (water now) and yellow
# (water soon) moisture percentages. Lindsay tunes these in the dashboard drawer
# (plant_dashboard.yaml). Ring color = red below red%, yellow below yellow%, else green.
input_number:
  plant_monstera_3_red:
    name: "Monstera 3  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 20
  plant_monstera_3_yellow:
    name: "Monstera 3  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 30
  plant_monstera_2_red:
    name: "Monstera 2  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 20
  plant_monstera_2_yellow:
    name: "Monstera 2  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 30
  plant_monstera_1_red:
    name: "Monstera 1  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 20
  plant_monstera_1_yellow:
    name: "Monstera 1  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 30
  plant_bird_of_paradise_red:
    name: "Bird of Paradise  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 25
  plant_bird_of_paradise_yellow:
    name: "Bird of Paradise  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 35
  plant_fiddle_leaf_1_red:
    name: "Fiddle Leaf 1  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 30
  plant_fiddle_leaf_1_yellow:
    name: "Fiddle Leaf 1  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 40
  plant_fiddle_leaf_2_red:
    name: "Fiddle Leaf 2  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 30
  plant_fiddle_leaf_2_yellow:
    name: "Fiddle Leaf 2  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 40
  plant_maury_river_fiddle_leaf_red:
    name: "Maury River Fiddle  Water below"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 30
  plant_maury_river_fiddle_leaf_yellow:
    name: "Maury River Fiddle  Water soon"
    min: 0
    max: 100
    step: 1
    mode: slider
    unit_of_measurement: "%"
    icon: mdi:water-percent
    initial: 40
  • [ ] Step 3: Allowlist the 15 new helpers (they don't exist in the registry until deploy)

Append to tests/fixtures/entities_allow.txt:

# Plant Dashboard helpers (configuration.yaml) — created on next HA restart after
# deploy; allowlisted until they exist in the live registry (then `make snapshot`
# and remove these). See docs/superpowers/specs/2026-06-17-plant-dashboard-design.md
input_boolean.plant_settings_open
input_number.plant_monstera_3_red
input_number.plant_monstera_3_yellow
input_number.plant_monstera_2_red
input_number.plant_monstera_2_yellow
input_number.plant_monstera_1_red
input_number.plant_monstera_1_yellow
input_number.plant_bird_of_paradise_red
input_number.plant_bird_of_paradise_yellow
input_number.plant_fiddle_leaf_1_red
input_number.plant_fiddle_leaf_1_yellow
input_number.plant_fiddle_leaf_2_red
input_number.plant_fiddle_leaf_2_yellow
input_number.plant_maury_river_fiddle_leaf_red
input_number.plant_maury_river_fiddle_leaf_yellow
  • [ ] Step 4: Lint the YAML

Run: make lint Expected: PASS (yamllint clean; no JSON/Grafana guard regressions). If yamllint complains about the new block, fix indentation (2 spaces) until clean.

  • [ ] Step 5: Commit
git add configuration.yaml tests/fixtures/entities_allow.txt
git commit -m "auto: configuration.yaml — add 14 plant-threshold input_numbers + plant_settings_open toggle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 2: Register the plant-care dashboard in configuration.yaml

Files: - Modify: configuration.yaml:19-25 (the lovelace.dashboards: block)

  • [ ] Step 1: Add the dashboard entry

The current block (lines 17–25) is:

lovelace:
  mode: storage
  dashboards:
    desk-companion:
      mode: yaml
      title: Desk
      icon: mdi:monitor-dashboard
      show_in_sidebar: true
      filename: desk_companion.yaml

Add a sibling entry under dashboards: (the URL path must contain a hyphen, so plant-care):

    plant-care:
      mode: yaml
      title: Plants
      icon: mdi:flower-tulip
      show_in_sidebar: true
      filename: plant_dashboard.yaml
  • [ ] Step 2: Lint

Run: make lint Expected: PASS.

  • [ ] Step 3: Commit
git add configuration.yaml
git commit -m "auto: configuration.yaml — register plant-care YAML dashboard (Plants)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 3: Create plant_dashboard.yaml skeleton (view + background + header)

Files: - Create: plant_dashboard.yaml

  • [ ] Step 1: Create the file with the header comment, view, background, and a glass header card

Create plant_dashboard.yaml with exactly:

# Plant Dashboard — Lindsay's indoor-plant watering kiosk (YAML mode, iPhone portrait).
# Built in code, not the UI. See docs/superpowers/specs/2026-06-17-plant-dashboard-design.md
# Glance-only: each tile's ring fill = absolute moisture, ring color = that plant's
# red/yellow/green thresholds (input_number.plant_<slug>_red / _yellow). The bottom
# drawer (input_boolean.plant_settings_open) exposes per-plant range sliders.
# Design language: Midnight Glass (matches desk_companion.yaml).
title: Plants
views:
  - title: Plants
    path: plants
    background: "radial-gradient(130% 80% at 50% -5%, #1a2440 0%, #0a0e1a 55%) fixed"
    cards:
      # =====================================================  HEADER
      - type: custom:button-card
        show_icon: false
        show_name: false
        tap_action:
          action: none
        custom_fields:
          body: |
            [[[
              return `<div style="padding:6px 4px 2px">
                <div style="font-size:26px;font-weight:300;letter-spacing:1px;background:linear-gradient(120deg,#fff,#9db4ff);-webkit-background-clip:text;background-clip:text;color:transparent">Plants</div>
                <div style="font-size:12px;color:#8aa0c8;margin-top:2px;letter-spacing:2px;text-transform:uppercase">🌿 Soil moisture at a glance</div>
              </div>`;
            ]]]
        styles:
          card:
            - background: none
            - border: none
            - box-shadow: none
            - padding: 0
          grid:
            - grid-template-areas: '"body"'
          custom_fields:
            body:
              - justify-self: stretch
  • [ ] Step 2: Lint

Run: make lint Expected: PASS (yamllint clean).

  • [ ] Step 3: Commit
git add plant_dashboard.yaml
git commit -m "auto: plant_dashboard.yaml — scaffold view, Midnight Glass background, header

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

This task establishes the exact tile pattern. Task 5 replicates it for the other 6 plants.

Files: - Modify: plant_dashboard.yaml (append a grid card after the header card, inside cards:)

  • [ ] Step 1: Append the gallery grid containing tile #1

After the header button-card (still inside the view's cards: list), add:

      # =====================================================  GALLERY (2-col grid)
      - type: grid
        columns: 2
        square: false
        cards:
          # ---------- Monstera 3
          - type: vertical-stack
            card_mod:
              style: |
                ha-card {
                  background: rgba(255,255,255,0.10);
                  border: 1px solid rgba(255,255,255,0.20);
                  border-radius: 16px;
                  overflow: hidden;
                  backdrop-filter: blur(9px);
                  -webkit-backdrop-filter: blur(9px);
                }
            cards:
              - type: custom:button-card
                entity: sensor.monstera_3_moisture
                show_icon: false
                show_name: false
                tap_action:
                  action: none
                custom_fields:
                  body: |
                    [[[
                      const m = Math.round(Number(states['sensor.monstera_3_moisture'].state));
                      const red = Number(states['input_number.plant_monstera_3_red'].state);
                      const yellow = Number(states['input_number.plant_monstera_3_yellow'].state);
                      const c = m < red ? '#ef4444' : m < yellow ? '#fbbf24' : '#34d399';
                      return `<div style="position:relative;height:100%;width:100%">
                        <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:54px;opacity:.16">🌿</div>
                        <div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(8,12,22,0) 40%,rgba(8,12,22,.92) 100%)"></div>
                        <div style="position:absolute;top:9px;right:9px;width:44px;height:44px;border-radius:50%;background:conic-gradient(${c} 0 ${m}%,rgba(255,255,255,.2) ${m}%);display:flex;align-items:center;justify-content:center">
                          <div style="width:34px;height:34px;border-radius:50%;background:rgba(8,12,22,.82);display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;font-weight:600">${m}%</div>
                        </div>
                        <div style="position:absolute;left:11px;bottom:9px;color:#fff;font-weight:600;font-size:13.5px;text-shadow:0 1px 4px rgba(0,0,0,.6)">Monstera 3</div>
                      </div>`;
                    ]]]
                styles:
                  card:
                    - background: |
                        [[[
                          const hasPhoto = false; // set true when /local/plant-photos/monstera_3.jpg exists
                          const hue = 130;
                          return hasPhoto
                            ? `url("/local/plant-photos/monstera_3.jpg") center/cover no-repeat`
                            : `radial-gradient(120% 90% at 50% 25%, hsl(${hue} 40% 32%), hsl(${hue} 45% 12%) 75%)`;
                        ]]]
                    - border: none
                    - border-radius: 0
                    - box-shadow: none
                    - height: 150px
                    - box-sizing: border-box
                    - overflow: hidden
                  grid:
                    - grid-template-areas: '"body"'
                  custom_fields:
                    body:
                      - align-self: stretch
                      - justify-self: stretch
              - type: custom:apexcharts-card
                graph_span: 48h
                span:
                  end: minute
                card_mod:
                  style: |
                    ha-card { background: transparent; box-shadow: none; border: none; height: 44px; }
                header:
                  show: false
                chart_type: line
                apex_config:
                  chart:
                    sparkline:
                      enabled: true
                    height: 40
                    parentHeightOffset: 0
                  grid:
                    padding:
                      top: 0
                      bottom: 0
                      left: 0
                      right: 0
                  stroke:
                    width: 2
                    curve: smooth
                    lineCap: round
                  tooltip:
                    enabled: false
                series:
                  - entity: sensor.monstera_3_moisture
                    color: "#7fd4e8"
                    stroke_width: 2
                    group_by:
                      func: avg
                      duration: 30min
                    show:
                      in_header: false
  • [ ] Step 2: Lint

Run: make lint Expected: PASS.

  • [ ] Step 3: Commit
git add plant_dashboard.yaml
git commit -m "auto: plant_dashboard.yaml — add gallery grid + first plant tile (Monstera 3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 5: Add the remaining 6 plant tiles

Files: - Modify: plant_dashboard.yaml (add 6 more tiles inside the grid card's cards:, after the Monstera 3 tile)

Each tile is identical to the Monstera 3 tile from Task 4 except for four substitutions, taken from the Plant Roster table: - the comment # ---------- <Name> - every monstera_3<slug> (appears 5×: entity:, 3× in the body template, entity: in the series, and in the photo URL/comment in the background template) - every display string Monstera 3<Name> (in the body name div) - const hue = 130;const hue = <hue>;

  • [ ] Step 1: Append tile #2 — Monstera 2 (monstera_2, hue 135)

Copy the entire - type: vertical-stack ... in_header: false tile block from Task 4 as the next item in the grid's cards: list, applying the substitutions: slug monstera_2, name Monstera 2, hue 135, photo comment monstera_2.jpg. The threshold helper refs become input_number.plant_monstera_2_red / _yellow.

  • [ ] Step 2: Append tile #3 — Monstera 1 (monstera_1, hue 140)

Same block, substitutions: slug monstera_1, name Monstera 1, hue 140, helpers plant_monstera_1_red/_yellow.

  • [ ] Step 3: Append tile #4 — Bird of Paradise (bird_of_paradise, hue 95)

Same block, substitutions: slug bird_of_paradise, name Bird of Paradise, hue 95, helpers plant_bird_of_paradise_red/_yellow.

  • [ ] Step 4: Append tile #5 — Fiddle Leaf 1 (fiddle_leaf_1, hue 120)

Same block, substitutions: slug fiddle_leaf_1, name Fiddle Leaf 1, hue 120, helpers plant_fiddle_leaf_1_red/_yellow.

  • [ ] Step 5: Append tile #6 — Fiddle Leaf 2 (fiddle_leaf_2, hue 115)

Same block, substitutions: slug fiddle_leaf_2, name Fiddle Leaf 2, hue 115, helpers plant_fiddle_leaf_2_red/_yellow.

  • [ ] Step 6: Append tile #7 — Maury River Fiddle (maury_river_fiddle_leaf, hue 110)

Same block, substitutions: slug maury_river_fiddle_leaf, name Maury River Fiddle, hue 110, helpers plant_maury_river_fiddle_leaf_red/_yellow.

  • [ ] Step 7: Verify all 7 tiles are present and lint

Run:

grep -c "type: custom:apexcharts-card" plant_dashboard.yaml   # expect 7
grep -c "vertical-stack" plant_dashboard.yaml                  # expect 7
make lint
Expected: counts are 7 and 7; make lint PASS.

  • [ ] Step 8: Commit
git add plant_dashboard.yaml
git commit -m "auto: plant_dashboard.yaml — add remaining 6 plant tiles (all 7 plants live)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 6: Add the settings entry tile + threshold drawer

Files: - Modify: plant_dashboard.yaml (append after the grid card, inside the view's cards:)

The drawer holds one row per plant. Each row is a vertical-stack of: (a) a button-card zone-preview bar (since range-slider-card has no documented track coloring) showing red/yellow/green, and (b) the range-slider-card bound to that plant's two helpers.

  • [ ] Step 1: Append the settings entry tile (toggles the drawer)

After the grid card (still in the view's cards:), add:

      # =====================================================  SETTINGS ENTRY TILE
      - type: custom:button-card
        entity: input_boolean.plant_settings_open
        show_icon: false
        show_name: false
        tap_action:
          action: toggle
        custom_fields:
          body: |
            [[[
              const open = states['input_boolean.plant_settings_open'].state === 'on';
              return `<div style="display:flex;align-items:center;justify-content:center;gap:8px;color:#cdd9f0;font-size:13px;letter-spacing:1px">
                <span>⚙</span><span>Adjust watering levels</span><span style="transform:rotate(${open ? 180 : 0}deg);transition:transform .2s">▾</span>
              </div>`;
            ]]]
        styles:
          card:
            - background: rgba(255,255,255,0.06)
            - border: 1px solid rgba(255,255,255,0.13)
            - border-radius: 14px
            - box-shadow: none
            - padding: 13px
            - margin-top: 4px
          grid:
            - grid-template-areas: '"body"'
          custom_fields:
            body:
              - justify-self: stretch
  • [ ] Step 2: Append the conditional drawer containing the first row (Monstera 3)

After the settings entry tile, add the drawer. This step adds the drawer wrapper + the first plant row (Monstera 3). The row pattern repeats in Step 3.

      # =====================================================  SETTINGS DRAWER
      - type: conditional
        conditions:
          - entity: input_boolean.plant_settings_open
            state: "on"
        card:
          type: vertical-stack
          card_mod:
            style: |
              ha-card {
                background: rgba(255,255,255,0.05);
                border: 1px solid rgba(255,255,255,0.12);
                border-radius: 14px;
                padding: 12px;
                margin-top: 10px;
              }
          cards:
            # ---------- Monstera 3
            - type: custom:button-card
              entity: sensor.monstera_3_moisture
              show_icon: false
              show_name: false
              tap_action:
                action: none
              custom_fields:
                body: |
                  [[[
                    const m = Math.round(Number(states['sensor.monstera_3_moisture'].state));
                    const red = Number(states['input_number.plant_monstera_3_red'].state);
                    const yellow = Number(states['input_number.plant_monstera_3_yellow'].state);
                    return `<div>
                      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:7px">
                        <span style="color:#e7ecf6;font-size:13px;font-weight:600">Monstera 3</span>
                        <span style="font-size:10px;color:#8aa0c8">now ${m}%</span>
                      </div>
                      <div style="height:12px;border-radius:7px;background:linear-gradient(90deg,#ef4444 0 ${red}%,#fbbf24 ${red}% ${yellow}%,#34d399 ${yellow}% 100%)"></div>
                      <div style="font-size:10px;color:#9fb0d0;margin-top:5px;display:flex;gap:12px">
                        <span>Water below ${red}%</span><span>Soon below ${yellow}%</span><span>Happy above ${yellow}%</span>
                      </div>
                    </div>`;
                  ]]]
              styles:
                card:
                  - background: none
                  - border: none
                  - box-shadow: none
                  - padding: 4px 2px 2px
                grid:
                  - grid-template-areas: '"body"'
                custom_fields:
                  body:
                    - justify-self: stretch
            - type: custom:range-slider-card
              entity_min: input_number.plant_monstera_3_red
              entity_max: input_number.plant_monstera_3_yellow
              min: 0
              max: 100
              step: 1
              unit: "%"
              card_mod:
                style: |
                  ha-card { background: transparent; border: none; box-shadow: none; }
  • [ ] Step 3: Append the other 6 drawer rows

For each remaining plant (Monstera 2, Monstera 1, Bird of Paradise, Fiddle Leaf 1, Fiddle Leaf 2, Maury River Fiddle), add — inside the drawer vertical-stack's cards: list, after the Monstera 3 row — a button-card zone-preview + range-slider-card pair identical to Step 2's row except substituting monstera_3<slug> and Monstera 3<Name> (per the Plant Roster). The button-card references sensor.<slug>_moisture, input_number.plant_<slug>_red, input_number.plant_<slug>_yellow; the range-slider-card uses entity_min: input_number.plant_<slug>_red and entity_max: input_number.plant_<slug>_yellow.

Add them in roster order: monstera_2, monstera_1, bird_of_paradise, fiddle_leaf_1, fiddle_leaf_2, maury_river_fiddle_leaf.

  • [ ] Step 4: Verify drawer completeness and lint

Run:

grep -c "custom:range-slider-card" plant_dashboard.yaml   # expect 7
grep -c "entity_min: input_number.plant_" plant_dashboard.yaml  # expect 7
make lint
Expected: both counts 7; make lint PASS.

  • [ ] Step 5: Commit
git add plant_dashboard.yaml
git commit -m "auto: plant_dashboard.yaml — add settings drawer (per-plant range sliders + zone bars)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 7: Wire plant_dashboard.yaml into CI and the test suite

Files: - Modify: .github/workflows/ci.yml:56 (change-filter regex) and :113 (HA_FILES) - Modify: tests/conftest.py:139-148 (all_referenced_entities map)

  • [ ] Step 1: Add plant_dashboard to the change-filter regex

In .github/workflows/ci.yml, line 56 currently reads:

            echo "ha_config=$(m '^(automations|configuration|configuration_tou_addition|homekit|influxdb_ha|scenes|scripts|desk_companion)\.yaml$')"

Add |plant_dashboard to the alternation:

            echo "ha_config=$(m '^(automations|configuration|configuration_tou_addition|homekit|influxdb_ha|scenes|scripts|desk_companion|plant_dashboard)\.yaml$')"
  • [ ] Step 2: Add plant_dashboard.yaml to HA_FILES

Line 113 currently reads:

          HA_FILES="automations.yaml configuration.yaml configuration_tou_addition.yaml homekit.yaml influxdb_ha.yaml scenes.yaml scripts.yaml desk_companion.yaml"

Append plant_dashboard.yaml:

          HA_FILES="automations.yaml configuration.yaml configuration_tou_addition.yaml homekit.yaml influxdb_ha.yaml scenes.yaml scripts.yaml desk_companion.yaml plant_dashboard.yaml"

(Note: plant_dashboard.yaml is not automations.yaml, so the existing NON_AUTO logic correctly classifies it as restart-required — no further change needed.)

  • [ ] Step 3: Add plant_dashboard.yaml to the entity-reference scan

In tests/conftest.py, the all_referenced_entities() map (lines 139–148) ends with:

        "desk_companion.yaml": extract_yaml_refs("desk_companion.yaml"),
    }

Add a line for the new dashboard:

        "desk_companion.yaml": extract_yaml_refs("desk_companion.yaml"),
        "plant_dashboard.yaml": extract_yaml_refs("plant_dashboard.yaml"),
    }
  • [ ] Step 4: Run the full suite to confirm green

Run: make lint && make pytest Expected: PASS. In particular tests/test_entity_references.py passes — every entity referenced by plant_dashboard.yaml that the scanner catches either exists in tests/fixtures/entities.txt (the sensor.<slug>_moisture set) or is allowlisted (the 15 helpers from Task 1).

If test_entity_references fails naming a sensor.<slug>_moisture, confirm the slug matches the roster exactly (e.g. maury_river_fiddle_leaf, not maury_fiddle). If it names a helper, confirm it's in entities_allow.txt from Task 1.

  • [ ] Step 5: Commit
git add .github/workflows/ci.yml tests/conftest.py
git commit -m "auto: CI/tests — wire plant_dashboard.yaml into deploy + entity-ref scan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 8: Document the dashboard in the household manual

Files: - Modify: docs/garden.md (add a "Plant Dashboard" section)

  • [ ] Step 1: Read the current garden manual page to match tone/structure

Run: sed -n '1,40p' docs/garden.md and skim existing headings so the new section's heading level and voice match (plain language, tables, admonitions). Note the existing top-level title so the new section is an ## (or appropriate) sibling.

  • [ ] Step 2: Add the Plant Dashboard section

Append a section (adjust the heading level to match the page — use ## if the page title is a single #):

## Plant Dashboard

A dedicated dashboard for the indoor plants, in the sidebar under **Plants**. It's designed for a phone in portrait — open it for a quick read on who needs water.

### How to read a tile

Each plant is a tile with a **ring** in the corner:

- **How full the ring is** = how much moisture is in the soil right now (0–100%).
- **The ring's color** = whether *that* plant is happy:
  - 🔴 **Red** — water it now.
  - 🟡 **Yellow** — water it soon.
  - 🟢 **Green** — happy, leave it be.

Different plants like different amounts of water, so a 40% reading can be green for a Monstera but red for a fiddle leaf. The little line under each plant's name is its moisture trend over the last two days.

### Adjusting watering levels

Tap **⚙ Adjust watering levels** at the bottom to open the drawer. Each plant has a slider with two handles:

- The **left handle** sets the **red** point (water below this).
- The **right handle** sets the **yellow** point (water soon below this).
- Everything above the right handle is **green** (happy).

Slide them to match what you know about each plant. The rings update instantly.

!!! tip "Photos"
    The tiles start with leaf artwork. Send Louis a photo of each plant and they'll fill the tiles in.
  • [ ] Step 3: Build the docs strictly to catch broken links/anchors

Run: mkdocs build --strict Expected: PASS ("Documentation built"). If it fails on a broken link/anchor, fix the offending Markdown until the strict build passes. (If mkdocs isn't installed locally, instead run python -m mkdocs build --strict; if neither is available, note it and rely on the docs.yml CI run after push.)

  • [ ] Step 4: Commit
git add docs/garden.md
git commit -m "auto: docs/garden.md — add Plant Dashboard section (ring meaning + drawer)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 9: Deploy, verify, and post-deploy snapshot

Files: none (push + verification + snapshot)

  • [ ] Step 1: Final full local suite

Run: make test Expected: PASS (all tiers: lint, pytest, and — if Docker is available — check-config on the pinned HA image). If check-config flags the new lovelace dashboard entry or helpers, fix the YAML before pushing. A bad config will be caught here and (if pushed) will block deploy.

  • [ ] Step 2: Push and watch CI

git push
gh run list --workflow=ci.yml --limit 1
Wait for the run, then:
gh run watch $(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')
Expected: test job green; deploy-ha job runs (because configuration.yaml and plant_dashboard.yaml changed → restart mode). The docs workflow also runs (because docs/garden.md changed) — confirm it's green:
gh run list --workflow=docs.yml --limit 1

  • [ ] Step 3: Confirm HA came back up after the restart

Run: ssh root@192.168.4.93 "ha core logs --lines 20" Expected: no fatal config errors; core running. (Per CLAUDE.md, HAOS LAN address.)

  • [ ] Step 4: On-device visual check (Louis)

On the iPhone (or any browser), open the Plants dashboard (/plant-care). Confirm: - 7 tiles render in a 2-column grid; each shows a moisture ring (fill = %, color per thresholds) and a name; sparklines draw a 48h trend. - Tapping ⚙ Adjust watering levels opens the drawer; each plant has a working dual-handle slider; dragging a handle changes that plant's threshold and the ring color updates. - No "needs water" summary text anywhere; layout looks right in portrait.

If anything is visually off (tile height, ring position, sparkline height), iterate on plant_dashboard.yaml and re-push (CSS/JS-only tweaks → restart again). This is expected polish, not a failure.

  • [ ] Step 5: Refresh the entity snapshot now that the helpers exist

Once HA has restarted and the 15 helpers are live:

make snapshot   # needs HA_URL + HA_TOKEN in env
Then remove the 15 allowlist lines added in Task 1 from tests/fixtures/entities_allow.txt (the block under the "Plant Dashboard helpers" comment), since the real entities are now in entities.txt.

Run: make pytest Expected: PASS (helpers now resolve via the snapshot, not the allowlist).

  • [ ] Step 6: Commit the snapshot + allowlist cleanup
git add tests/fixtures/entities.txt tests/fixtures/entities_allow.txt
git commit -m "auto: tests — snapshot plant-threshold helpers, 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: - New standalone YAML dashboard at plant-care, sidebar "Plants" → Task 2 + Task 3. ✓ - Photo tiles, 2-col, iPhone portrait → Task 4/5 (grid columns 2, fixed tile heights). ✓ - Ring fill = absolute moisture, color = per-plant red/yellow/green → Task 4 body template. ✓ - No "needs water" summary → header has none (Task 3); verified in Task 9 Step 4. ✓ - 48h trend sparkline per tile → Task 4/5 apexcharts. ✓ - Settings drawer toggled by input_boolean, dual-handle slider per plant → Task 6 (range-slider-card + conditional). ✓ - 14 input_number + 1 input_boolean helpers → Task 1. ✓ - Photo path /local/plant-photos/<slug>.jpg with illustration placeholder + hasPhoto flag → Task 4 background template. ✓ - Dependencies (button-card, apexcharts, card-mod, range-slider-card) → used; range-slider-card schema confirmed (entity_min/entity_max). ✓ - CI wiring (regex + HA_FILES, restart mode) → Task 7. ✓ - conftest scan + allowlist + post-deploy snapshot → Task 1 + Task 7 + Task 9. ✓ - Manual page docs/garden.md + mkdocs --strict → Task 8. ✓ - Out-of-scope items (battery/temp, notifications, hard-clamp) correctly omitted. ✓

Placeholder scan: No "TBD/TODO". The range-slider-card track-coloring uncertainty from the spec is resolved deterministically (always render the zone-preview button-card bar; don't rely on the card coloring its own track). The hasPhoto = false flag is intentional config, not a placeholder. ✓

Type/name consistency: Helper names input_number.plant_<slug>_red/_yellow and input_boolean.plant_settings_open are identical across Tasks 1, 4, 5, 6, 7. Slugs match tests/fixtures/entities.txt exactly (maury_river_fiddle_leaf, bird_of_paradise, fiddle_leaf_1/2, monstera_1/2/3). range-slider-card keys (entity_min/entity_max/min/max/step/unit) match the card's documented schema. ✓