Merge pull request #1567 from CCOSTAN/dashboard-designer-skill

Add Codex Home Assistant dashboard designer skill
pull/1573/head
Carlo Costanzo 2 months ago committed by GitHub
commit 89bd05c490
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -22,6 +22,7 @@ Live, personal Home Assistant configuration shared for **browsing and inspiratio
- You are here: `/` (root repo guide)
- [Blog](https://www.vcloudinfo.com) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) | [Diagram](config/www/custom_ui/floorplan/images/branding/Bear-Stone-Docker-Diagram.jpg) | [YouTube](https://youtube.com/vCloudInfo)
- Config readmes: [Config index](config/README.md) | [Packages](config/packages/README.md) | [Automations](config/automation/README.md) | [Scripts](config/script/README.md) | [Scenes](config/scene/README.md) | [Sounds](config/sounds/README.md) | [Package triggers](config/packages/triggers/README.md)
- Codex skills (optional): [codex_skills](codex_skills)
![Home Assistant header](https://i.imgur.com/vjDH1LJ.png)
@ -82,6 +83,7 @@ https://amzn.to/48jVzZ3
### Gear and affiliate links
- Browsing or purchasing through the affiliate links above helps support this project; thanks!
- Full gear list: https://www.vcloudinfo.com/gear-list
**All of my configuration files are tested against the most stable version of home-assistant.**

@ -0,0 +1,46 @@
<h1 align="center">
<a name="logo" href="https://www.vCloudInfo.com/tag/iot"><img src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/x_profile.png" alt="Bear Stone Smart Home" width="200"></a>
<br>
Bear Stone Smart Home Documentation
</h1>
<h4 align="center">Be sure to :star: my configuration repo so you can keep up to date on any daily progress!</h4>
<div align="center">
[![X Follow](https://img.shields.io/static/v1?label=talk&message=3k&color=blue&logo=twitter&style=for-the-badge)](https://x.com/ccostan)
[![YouTube Subscribe](https://img.shields.io/youtube/channel/subscribers/UC301G8JJFzY0BZ_0lshpKpQ?label=VIEW&logo=Youtube&logoColor=%23DF5D44&style=for-the-badge)](https://www.youtube.com/vCloudInfo?sub_confirmation=1)
[![GitHub Stars](https://img.shields.io/github/stars/CCOSTAN/Home-AssistantConfig?label=STARS&logo=Github&style=for-the-badge)](https://github.com/CCOSTAN) <br>
[![HA Version Badge](https://raw.githubusercontent.com/ccostan/home-assistantconfig/master/ha-version-badge.svg)](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION)
[![Last Commit](https://img.shields.io/github/last-commit/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
[![Commit Activity](https://img.shields.io/github/commit-activity/y/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
</div>
Codex skills stored in-repo so they can be shared with the community. These are documentation + helper scripts only (no secrets).
### Quick navigation
- You are here: `codex_skills/`
- [Repo overview](../README.md) | [Dashboards](../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
## Skills
- `homeassistant-dashboard-designer/`: Constrained, button-card-first Lovelace dashboard design system + YAML lint helper.
### Notes
- Codex loads skills from your local Codex home. See each skill folder for install steps.
**All of my configuration files are tested against the most stable version of home-assistant.**
<a name="bottom" href="https://github.com/CCOSTAN/Home-AssistantConfig#logo"><img align="right" border="0" src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/config/www/custom_ui/floorplan/images/branding/up_arrow.png" width="25" ></a>
**Still have questions on my Config?** <br>
**Message me on X :** [![Follow CCostan](https://img.shields.io/twitter/follow/CCostan)](https://www.x.com/ccostan)
<p align="center">
<a target="_blank" href="https://www.buymeacoffee.com/vCloudInfo"><img src="https://www.buymeacoffee.com/assets/img/BMC-btn-logo.svg" alt="Buy me a coffee"><span style="margin-left:5px">You can buy me a coffee</span></a><a target="_blank" href="https://www.buymeacoffee.com/vCloudInfo"><img src="https://www.buymeacoffee.com/assets/img/BMC-btn-logo.svg" alt="Buy me a coffee"></a>
<br>
<a href="https://eepurl.com/dmXFYz"><img align="center" border="0" src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/config/www/custom_ui/floorplan/images/branding/email_link.png" height="50" ></a><br>
<a href="https://www.vCloudInfo.com/p/affiliate-disclosure.html">
Affiliate Disclosure
</a></p>

@ -0,0 +1,113 @@
<h1 align="center">
<a name="logo" href="https://www.vCloudInfo.com/tag/iot"><img src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/x_profile.png" alt="Bear Stone Smart Home" width="200"></a>
<br>
Bear Stone Smart Home Documentation
</h1>
<h4 align="center">Be sure to :star: my configuration repo so you can keep up to date on any daily progress!</h4>
<div align="center">
[![X Follow](https://img.shields.io/static/v1?label=talk&message=3k&color=blue&logo=twitter&style=for-the-badge)](https://x.com/ccostan)
[![YouTube Subscribe](https://img.shields.io/youtube/channel/subscribers/UC301G8JJFzY0BZ_0lshpKpQ?label=VIEW&logo=Youtube&logoColor=%23DF5D44&style=for-the-badge)](https://www.youtube.com/vCloudInfo?sub_confirmation=1)
[![GitHub Stars](https://img.shields.io/github/stars/CCOSTAN/Home-AssistantConfig?label=STARS&logo=Github&style=for-the-badge)](https://github.com/CCOSTAN) <br>
[![HA Version Badge](https://raw.githubusercontent.com/ccostan/home-assistantconfig/master/ha-version-badge.svg)](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION)
[![Last Commit](https://img.shields.io/github/last-commit/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
[![Commit Activity](https://img.shields.io/github/commit-activity/y/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
</div>
# Home Assistant Dashboard Designer (Codex Skill)
This directory contains the `homeassistant-dashboard-designer` Codex skill, stored in-repo so it can be shared with the community.
### Quick navigation
- You are here: `codex_skills/homeassistant-dashboard-designer/`
- [Repo overview](../../README.md) | [Codex skills](../README.md) | [Dashboards](../../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
## What This Skill Does
- Enforces a constrained Lovelace design system (button-card first, minimal card-mod, grid/vertical-stack layout rules).
- Encourages centralized templates and deterministic YAML output.
- Treats Stitch MCP output as inspiration only and translates ideas into safe Lovelace YAML.
Skill source of truth inside this folder:
- `SKILL.md`
- `agents/openai.yaml`
- `references/dashboard_rules.md`
- `scripts/validate_lovelace_view.py`
## Install (Local Codex)
Codex loads skills from your local Codex skills directory.
1. Copy this folder into your Codex skills directory as:
- `~/.codex/skills/homeassistant-dashboard-designer/` (Linux/macOS)
- `%USERPROFILE%\\.codex\\skills\\homeassistant-dashboard-designer\\` (Windows)
2. Restart your Codex session/editor so it re-indexes skills.
## Stitch MCP Install (Design Inspiration)
Stitch is optional and used only for design inspiration. To enable it, add a Stitch MCP server entry to your Codex config.
1. Set an environment variable with your API key:
- `STITCH_API_KEY=<your_key>`
2. Add this to your `~/.codex/config.toml`:
```toml
[mcp_servers.stitch]
url = "https://stitch.googleapis.com/mcp"
env_http_headers = { "X-Goog-Api-Key" = "STITCH_API_KEY" }
http_headers = { "Accept" = "application/json" }
```
## Usage
Invoke in chat:
- `$homeassistant-dashboard-designer`
Then describe what you want in natural language (what to change + where + any constraints). The skill will infer the structured intent internally and enforce the button-card-first / layout constraints defined in `SKILL.md`.
Examples:
- "Refactor `config/dashboards/infrastructure/partials/mariadb_sections.yaml` to match the existing Infrastructure design language. Preserve existing templates and keep diffs small."
- "Add a new Infrastructure view for Docker containers using the same layout rules as the other views (4 columns desktop / 2 columns mobile)."
Optional:
- If you already have an entity list, include it.
- If you do not, enable the Home Assistant MCP so Codex can validate entity IDs/services against your live HA instance (recommended).
## Home Assistant MCP (Built-In) Enablement
The Home Assistant MCP lets Codex validate entity IDs and service calls against your *real* HA instance before it edits YAML.
1. Create a Home Assistant Long-Lived Access Token (Profile page in HA).
2. Set an environment variable (do not commit tokens):
- `HOMEASSISTANT_MCP_AUTH=Bearer <your_long_lived_access_token>`
3. Add this to your `~/.codex/config.toml`:
```toml
[mcp_servers.homeassistant]
url = "http://<your-ha-host>:8123/api/mcp"
env_http_headers = { "Authorization" = "HOMEASSISTANT_MCP_AUTH" }
```
Notes:
- Use `https://` if your HA is behind TLS.
- Keep tokens in environment variables, not in files under git.
### Notes
- This skill intentionally contains no secrets. Configure MCP credentials via environment variables in your local Codex setup.
**All of my configuration files are tested against the most stable version of home-assistant.**
<a name="bottom" href="https://github.com/CCOSTAN/Home-AssistantConfig#logo"><img align="right" border="0" src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/config/www/custom_ui/floorplan/images/branding/up_arrow.png" width="25" ></a>
**Still have questions on my Config?** <br>
**Message me on X :** [![Follow CCostan](https://img.shields.io/twitter/follow/CCostan)](https://www.x.com/ccostan)
<p align="center">
<a target="_blank" href="https://www.buymeacoffee.com/vCloudInfo"><img src="https://www.buymeacoffee.com/assets/img/BMC-btn-logo.svg" alt="Buy me a coffee"><span style="margin-left:5px">You can buy me a coffee</span></a><a target="_blank" href="https://www.buymeacoffee.com/vCloudInfo"><img src="https://www.buymeacoffee.com/assets/img/BMC-btn-logo.svg" alt="Buy me a coffee"></a>
<br>
<a href="https://eepurl.com/dmXFYz"><img align="center" border="0" src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/config/www/custom_ui/floorplan/images/branding/email_link.png" height="50" ></a><br>
<a href="https://www.vCloudInfo.com/p/affiliate-disclosure.html">
Affiliate Disclosure
</a></p>

@ -0,0 +1,172 @@
---
name: homeassistant-dashboard-designer
description: "Design, update, and refactor Home Assistant Lovelace dashboards (YAML views/partials) with a constrained, machine-safe design system: button-card-first structure, minimal card-mod styling, optional flex-horseshoe + mini-graph telemetry, strict grid/vertical-stack layout rules, centralized templates, deterministic ordering, and config validation. Use for Home Assistant dashboard work (especially config/dashboards/**), when refactoring views, adding infra/home/energy/environment panels, or translating Stitch design inspiration into safe Lovelace YAML."
---
# Home Assistant Dashboard Designer
Design dashboards as systems: predictable structure, reusable templates, minimal drift. Treat Stitch as inspiration only; translate to safe Lovelace YAML.
## Primary Input: Natural Language (Default)
You can give natural-language guidance. The skill must infer the structured intent internally (dashboard intent, view name, entity mapping, constraints) while enforcing the design system and validating entities/services via the Home Assistant MCP when available.
Minimum helpful info to include in natural language:
- What to change (add/remove/refactor) and why (the goal).
- Where to change it: the exact view/partial path(s) under `config/dashboards/**`.
- Any constraints you care about: desktop/mobile columns, time window preferences for graphs, "do not touch" templates/sections.
If any of the above is missing, ask targeted clarifying questions (do not demand a full "intent block").
Example natural-language request:
- "Refactor `config/dashboards/infrastructure/views/06_mariadb.yaml` to match the button-card-first system used in other Infrastructure views. Keep the same entities, no new templates, 4 columns desktop / 2 columns mobile."
## Optional Input: Structured Intent (Allowed, Not Required)
If the user provides structured intent, accept it. Do not require it.
```yaml
dashboard_type: infra # infra|home|energy|environment
view_name: "Infrastructure Overview"
target_paths:
dashboard_yaml: "config/dashboards/infrastructure/dashboard.yaml"
view_yaml: "config/dashboards/infrastructure/views/07_docker_containers.yaml"
entities:
- name: "Proxmox01"
cpu: "sensor.proxmox01_cpu"
memory: "sensor.proxmox01_mem"
constraints:
desktop_columns: 4
mobile_columns: 2
notes:
- "Preserve existing templates; minimize diff."
```
If updating an existing view and not obvious from context, ask:
- Which view file (single view dict) is the source of truth?
- Any "don't touch" areas/templates?
- Any fallback cards already in use that must remain?
## Output Contract (Always Produce)
- One valid Lovelace view (single view dict)
- Deterministic ordering (stable ordering of cards/sections)
- Section comments (short, consistent)
- Zero per-card styling variance (no one-off styles; use templates/snippets)
- Fallback usage explicitly justified (YAML comments adjacent to the fallback card)
## Card Priority Model
Attempt Tier 1 first. Use Tier 2 only if Tier 1 cannot express the requirement safely.
Tier 1 (primary):
- `custom:button-card` (structure + tiles + headers + containers)
- `card-mod` (shared/global polish only; shared snippets OK)
- `custom:flex-horseshoe-card` (single-metric radial telemetry)
- `custom:mini-graph-card` (single-metric trends)
Tier 2 (fallback, must justify in comments):
- `entities`, `markdown`, `history-graph`, `conditional`
- `iframe` (last resort)
## Layout Rules (Hard Constraints)
- Allowed layout containers: `grid`, `vertical-stack`
- Maximum nesting depth: 2 (example: `grid` -> `vertical-stack` -> cards)
- No `horizontal-stack` inside grid cells (prefer: more grid columns, or vertical-stack)
- No freeform positioning
- No layout logic embedded in `card-mod`
Note: If the repo/view uses Home Assistant `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section (sections should contain only `grid`/`vertical-stack` cards and their children).
## Design System Rules (Implementation Discipline)
### Button Card (Primary Renderer)
- Use `custom:button-card` as the structural primitive.
- Templates required. Do not inline per-card `styles:`; keep styling inside centralized templates/snippets.
- Logic via variables or template JS only (avoid duplicating logic per-card).
### card-mod (Styling Constraint Layer)
- Allowed only for shared/global polish:
- padding normalization
- border radius control
- shadow suppression
- typography consistency
- No entity-specific styling
- No per-card visual experimentation
### Flex Horseshoe Card
- One primary metric per card.
- Explicit color thresholds.
- No mixed units.
### Mini Graph Card
- One metric per graph.
- Consistent time windows per dashboard.
- Minimal legends (only when required).
## Template Architecture (Required)
- Views reference templates only (for `custom:button-card`: every instance must specify `template:`).
- Templates accept variables; views pass variables.
- Do not create new templates unless explicitly instructed.
Repo reality check (important):
- If the repo already has established templates (for example, Infrastructure uses `bearstone_*`), use what exists.
- If the spec's "required template categories" are not present, map to the closest existing template and call out the gap, or ask for explicit permission to add templates.
## Stitch MCP (Inspiration Only)
Use Stitch only to learn:
- visual hierarchy (headers, grouping, density)
- spacing rhythm (padding/gaps)
- grouping patterns (panels, sections)
Never treat Stitch output as implementation code.
When prompting Stitch, include these constraints verbatim:
```
Grid-only layout. No absolute positioning. No JS frameworks. No custom HTML/CSS outside card-mod.
Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, custom:mini-graph-card, and HA core fallback cards only if unavoidable.
Max layout nesting depth: 2. No horizontal-stack inside grid cells.
```
## Workflow (Do This In Order)
1. Read the target dashboard/view/partials/templates to understand existing patterns and avoid drift.
2. Determine intent from the user's request and existing dashboard context: `infra` (NOC), `home`, `energy`, or `environment`. Keep one intent per view.
3. Validate entities and services before editing:
- Prefer the Home Assistant MCP for live entity/service validation (required when available).
- If MCP is not available, ask the user to confirm entity IDs and services (do not guess).
4. Draft layout with constraints: a top-level `grid` and optional `vertical-stack` groups.
5. Implement using Tier 1 cards first; reuse existing templates; avoid one-off styles.
6. If fallback cards are necessary, add an inline comment explaining why Tier 1 cannot satisfy the requirement.
7. Validate:
- Run Home Assistant config validation (`ha core check` or `homeassistant --script check_config`) when available.
- Optionally run `scripts/validate_lovelace_view.py` from this skill against the changed view file to catch violations early.
8. Failure behavior:
- If requirements can't be met: state the violated rule and propose a compliant alternative.
- If validation fails: stop, surface the error output, and propose corrected YAML. Do not leave invalid config applied.
## References
- Read `references/dashboard_rules.md` when you need the full constraint set and repo-specific mapping notes.
## Home Assistant MCP Validation (Required When Available)
Before writing Lovelace YAML, confirm:
- Every `entity:` (and any entities referenced via variables) exists.
- Every service you plan to call exists and the payload shape is correct (especially `service_data`).
When Home Assistant MCP is enabled, use it to:
- List/confirm entities (avoid typos like `sensor.foo_bar` vs `sensor.foobar`).
- Confirm the dashboard card references match real entities.
- Validate service names and fields before committing YAML changes.
If MCP is not enabled or not reachable, stop and ask for the missing entity/service IDs rather than inventing them.

@ -0,0 +1,4 @@
interface:
display_name: "Home Assistant Dashboard Designer"
short_description: "Design/refactor HA Lovelace dashboards safely"
default_prompt: "Use $homeassistant-dashboard-designer to design, update, or refactor a Home Assistant Lovelace dashboard view using button-card templates + safe grid/stack layouts."

@ -0,0 +1,127 @@
# Dashboard Rules (Full)
This file is reference material for $homeassistant-dashboard-designer. Prefer following the workflow in `../SKILL.md`, and read this only when you need details.
## Card Priority Model
Tier 1 (primary, attempt first):
- `custom:button-card`
- `card-mod` (shared/global styling only)
- `custom:flex-horseshoe-card`
- `custom:mini-graph-card`
Tier 2 (fallback, only when Tier 1 cannot satisfy the requirement; justify inline):
- `entities`
- `markdown`
- `history-graph`
- `conditional`
- `iframe` (last resort)
## Layout Rules (Hard Constraints)
- Allowed layout containers: `grid`, `vertical-stack`
- Maximum nesting depth: 2
- No `horizontal-stack` inside grid cells
- No freeform positioning
- No layout logic embedded in `card-mod`
Sections-mode note:
- If a view uses `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section.
## Template Architecture (Required)
Intent: centralize visuals and logic into templates; views pass only variables and remain uniform.
Rules:
- `custom:button-card` must use templates (no one-off per-card styles).
- Templates accept variables.
- Do not hardcode entity IDs inside templates.
- Do not create new templates without explicit instruction.
Reality note for CCOSTAN/Home-AssistantConfig:
- Infrastructure templates already exist in:
- `config/dashboards/infrastructure/templates/button_card_templates.yaml`
- Existing template naming uses `bearstone_*` and includes styling inside templates (this is OK).
- When asked to implement new conceptual categories (e.g., "status_pill"), first map to existing templates (e.g., `bearstone_infra_chip*`). If no suitable match exists, ask before adding templates.
## Design System (Discipline)
### Button Card
Use it for:
- Infrastructure tiles
- Status pills
- Section headers
- Control footers
- Card containers
Rules:
- Templates required.
- Avoid per-card `styles:` keys.
- Avoid per-card `card_mod:` blocks.
### card-mod
Allowed only for shared/global polish:
- padding normalization
- border radius control
- shadow suppression
- typography consistency
Not allowed:
- entity-specific styling
- per-card visual experimentation
### Flex Horseshoe Card
Use it for:
- Temperature
- Battery
- Load
- Utilization
- Percentage-based metrics
Rules:
- One primary metric per card.
- Explicit color thresholds.
- No mixed units.
### Mini Graph Card
Rules:
- One metric per graph.
- Consistent time windows per dashboard (keep uniform unless user asks otherwise).
- Minimal legends (only if required for interpretation).
## Stitch MCP Integration (Inspiration Only)
Stitch is advisory, never authoritative.
When using Stitch:
- Provide feasibility constraints (cards + layout)
- Use outputs only to inform hierarchy/grouping/density/spacing
- Translate into approved Lovelace YAML
Required constraints to include in Stitch prompt:
```
Grid-only layout. No absolute positioning. No JS frameworks. No custom HTML/CSS outside card-mod.
Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, custom:mini-graph-card, and HA core fallback cards only if unavoidable.
Max layout nesting depth: 2. No horizontal-stack inside grid cells.
```
## Repo-Specific Dashboard YAML Rules (config/dashboards/**)
If working in this repo's `config/dashboards/` tree:
- Do not edit `config/.storage` (runtime state).
- Includes must use absolute container paths starting with `/config/`.
- Views are one file per view, and the dashboard file uses `!include_dir_list`.
- Files under `config/dashboards/**/*.yaml` must include the standard `@CCOSTAN` header block.
## Validation (Home Assistant MCP)
When available, use the Home Assistant MCP to validate:
- Entity IDs referenced by Lovelace cards/templates.
- Service names and payload fields used by actions (for example, `button.press`, `script.*`, etc.).
If MCP is not available, do not guess entity IDs. Ask the user to confirm them.

@ -0,0 +1,273 @@
#!/usr/bin/env python
"""
Lightweight validator for Lovelace view YAML files.
Goal: catch common violations of the homeassistant-dashboard-designer constraints
before Home Assistant reloads.
This is NOT a full Lovelace schema validator.
"""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
class _Tagged(str):
pass
class _Loader(yaml.SafeLoader):
pass
def _construct_undefined(loader: _Loader, node: yaml.Node) -> Any:
# Preserve unknown tags (e.g., !include, !include_dir_list) as opaque strings.
if isinstance(node, yaml.ScalarNode):
return _Tagged(f"{node.tag} {node.value}")
if isinstance(node, yaml.SequenceNode):
seq = loader.construct_sequence(node)
return _Tagged(f"{node.tag} {seq!r}")
if isinstance(node, yaml.MappingNode):
mapping = loader.construct_mapping(node)
return _Tagged(f"{node.tag} {mapping!r}")
return _Tagged(f"{node.tag}")
_Loader.add_constructor(None, _construct_undefined) # type: ignore[arg-type]
@dataclass(frozen=True)
class Finding:
level: str # "ERROR" | "WARN"
path: str
message: str
ALLOWED_LAYOUT = {"grid", "vertical-stack"}
DISALLOWED_CARD_TYPES = {"horizontal-stack"}
# Cards allowed without justification (design-system tiering is enforced in SKILL.md; this is a lint).
ALLOWED_CARD_TYPES = {
"grid",
"vertical-stack",
"custom:button-card",
"custom:flex-horseshoe-card",
"custom:mini-graph-card",
# Tier-2 fallbacks (validator does not enforce justification comments).
"entities",
"markdown",
"history-graph",
"conditional",
"iframe",
}
def _is_mapping(v: Any) -> bool:
return isinstance(v, dict)
def _is_sequence(v: Any) -> bool:
return isinstance(v, list)
def _card_type(card: dict[str, Any]) -> str | None:
t = card.get("type")
if isinstance(t, str):
return t
return None
def _validate_layout_depth(
*,
node: Any,
cur_depth: int,
max_depth: int,
node_path: str,
findings: list[Finding],
) -> None:
if not _is_mapping(node):
return
ctype = _card_type(node)
if ctype in ALLOWED_LAYOUT:
cur_depth += 1
if cur_depth > max_depth:
findings.append(
Finding(
level="ERROR",
path=node_path,
message=f"Layout nesting depth {cur_depth} exceeds max {max_depth}.",
)
)
cards = node.get("cards")
if _is_sequence(cards):
for idx, child in enumerate(cards):
_validate_layout_depth(
node=child,
cur_depth=cur_depth,
max_depth=max_depth,
node_path=f"{node_path}.cards[{idx}]",
findings=findings,
)
def _walk_cards(node: Any, node_path: str, findings: list[Finding]) -> None:
if _is_mapping(node):
ctype = _card_type(node)
if ctype in DISALLOWED_CARD_TYPES:
findings.append(
Finding(
level="ERROR",
path=node_path,
message=f"Disallowed card type: {ctype}",
)
)
if ctype and ctype not in ALLOWED_CARD_TYPES:
findings.append(
Finding(
level="WARN",
path=node_path,
message=f"Unknown/unlisted card type: {ctype}",
)
)
if ctype == "custom:button-card":
tmpl = node.get("template")
if tmpl is None:
findings.append(
Finding(
level="ERROR",
path=node_path,
message="custom:button-card must set template: (string or list).",
)
)
if "styles" in node:
findings.append(
Finding(
level="ERROR",
path=node_path,
message="Per-card styles: are not allowed; move styling into centralized templates.",
)
)
if "card_mod" in node:
findings.append(
Finding(
level="ERROR",
path=node_path,
message="Per-card card_mod: is not allowed on button-card instances; use shared snippets or templates.",
)
)
cards = node.get("cards")
if _is_sequence(cards):
for idx, child in enumerate(cards):
_walk_cards(child, f"{node_path}.cards[{idx}]", findings)
elif _is_sequence(node):
for idx, child in enumerate(node):
_walk_cards(child, f"{node_path}[{idx}]", findings)
def _load_yaml(path: Path) -> Any:
txt = path.read_text(encoding="utf-8")
return yaml.load(txt, Loader=_Loader)
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("view_yaml", type=Path, help="Path to a Lovelace view YAML file")
ap.add_argument("--max-layout-depth", type=int, default=2)
ap.add_argument("--strict", action="store_true", help="Treat WARN findings as errors (non-zero exit).")
args = ap.parse_args(argv)
if not args.view_yaml.exists():
print(f"ERROR: file not found: {args.view_yaml}", file=sys.stderr)
return 2
try:
doc = _load_yaml(args.view_yaml)
except Exception as e:
print(f"ERROR: failed to parse YAML: {e}", file=sys.stderr)
return 2
findings: list[Finding] = []
# The repo uses "one YAML file per view (single view dict)".
if not _is_mapping(doc):
findings.append(Finding(level="ERROR", path="$", message="View file must be a single YAML mapping (one view dict)."))
else:
# Support both classic views (`cards:`) and sections views (`type: sections`, `sections:`).
if "cards" in doc:
_validate_layout_depth(
node=doc,
cur_depth=0,
max_depth=args.max_layout_depth,
node_path="$",
findings=findings,
)
_walk_cards(doc.get("cards", []), "$.cards", findings)
elif doc.get("type") == "sections" and "sections" in doc:
sections = doc.get("sections")
if isinstance(sections, _Tagged):
findings.append(
Finding(
level="WARN",
path="$.sections",
message="sections is an !include/unknown tag; validator cannot inspect included sections content.",
)
)
elif _is_sequence(sections):
for sidx, section in enumerate(sections):
spath = f"$.sections[{sidx}]"
if not _is_mapping(section):
findings.append(Finding(level="ERROR", path=spath, message="Section must be a mapping."))
continue
_validate_layout_depth(
node=section,
cur_depth=0,
max_depth=args.max_layout_depth,
node_path=spath,
findings=findings,
)
_walk_cards(section.get("cards", []), f"{spath}.cards", findings)
else:
findings.append(
Finding(
level="ERROR",
path="$.sections",
message="sections must be a list (or an !include tag).",
)
)
else:
findings.append(
Finding(
level="ERROR",
path="$",
message="View dict must contain cards: or be type: sections with sections:.",
)
)
errors = [f for f in findings if f.level == "ERROR"]
warns = [f for f in findings if f.level == "WARN"]
for f in errors + warns:
print(f"{f.level}: {f.path}: {f.message}")
if errors:
return 1
if warns and args.strict:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Loading…
Cancel
Save

Powered by TurnKey Linux.