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 14input_number+ 1input_booleanhelpers; register theplant-caredashboard. - Modify
.github/workflows/ci.yml— addplant_dashboardto the change-filter regex andHA_FILES. - Modify
tests/conftest.py— addplant_dashboard.yamlto 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;
yamllintruns inmake 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>"
Task 4: Add the gallery grid with the first plant tile (Monstera 3)¶
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
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
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_dashboardto 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.yamltoHA_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.yamlto the entity-reference scan
In tests/conftest.py, the all_referenced_entities() map (lines 139–148) ends with:
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
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: - [ ] 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:
Then remove the 15 allowlist lines added in Task 1 fromtests/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. ✓