Imported from rsr5/lovelace-echarts-raw-card (
AGENTS.md). Install upstream withnpx skills add rsr5/lovelace-echarts-raw-card. Copyright stays with the author.
AGENTS.md
Agent guide for lovelace-echarts-raw-card
A Home Assistant Lovelace card that renders Apache ECharts with entity binding, history queries, statistics aggregation, and dark-mode support.
Project Overview
This is a TypeScript + Lit + Vite project that builds a custom Home Assistant Lovelace card. The card allows users to write raw ECharts option objects with Home Assistant entity tokens ($entity, $history, $statistics) that get resolved at runtime.
Tech stack:
- TypeScript 5.6+ (strict mode, ES2022 target)
- Lit 3.2 (web components)
- Vite 5.4 (build + dev server)
- Vitest 4 (unit tests)
- ESLint 9 (flat config) + Prettier
- ECharts 5.5
Commands
All commands use npm scripts defined in package.json:
Development
npm run dev # Start Vite dev server on port 5173
npm run preview # Preview production build
Testing & Quality
npm test # Run tests once (Vitest)
npm run test:watch # Run tests in watch mode
npm run typecheck # TypeScript type-checking (no emit)
npm run lint # ESLint
npm run lint:fix # ESLint with auto-fix
npm run format # Prettier format all files
npm run format:check # Prettier check (CI)
Build
npm run build # Production build → dist/echarts-raw-card.js
Build produces a single-file JS module (dist/echarts-raw-card.js) with inline source maps. The build is minified in CI (BUILD_MODE=production or CI=true), but NOT minified in local dev builds for debugging.
CI Workflow
.github/workflows/ci.yml runs on push/PR to main:
- ESLint (
npm run lint) - Prettier check (
npm run format:check) - TypeScript typecheck (
npm run typecheck) - Tests (
npm test) - Build (
npm run build)
All checks must pass before merge.
Code Organization
src/
├── echarts-raw-card.ts # Main LitElement card class
├── index.ts # Entry point (registers custom element)
├── types.ts # TypeScript types for card config & tokens
├── ha-types.ts # Home Assistant types (HomeAssistant, LovelaceCardConfig)
├── card/
│ └── watched.ts # Entity fingerprinting & change detection
├── echarts/
│ └── instance.ts # ECharts instance lifecycle (init, resize, dispose)
├── history/
│ ├── fetch.ts # History API fetching & caching
│ ├── decode.ts # Decode HA history responses (normal + minimal_response)
│ ├── downsample.ts # LTTB downsampling for large datasets
│ ├── cache-ttl.ts # Compute minimum cache TTL from option tree
│ └── lru-map.ts # LRU cache implementation
├── statistics/
│ └── fetch.ts # Statistics API fetching & caching
└── tokens/
├── resolve.ts # Deep async token resolution
├── guards.ts # Type guards for token objects
├── entity.ts # Entity spec normalization
├── transforms.ts # Apply transforms ($scale, $round, $coerce, etc.)
└── index.ts # Token exports
tests/
├── history-decode.test.ts
├── history-downsample.test.ts
├── history-lru-map.test.ts
├── statistics-resolve.test.ts
├── tokens-guards.test.ts
└── tokens-transforms.test.ts
Code Style & Conventions
TypeScript
- Strict mode enabled (
strict: trueintsconfig.json) - Use explicit types for public APIs
- Avoid
any(ESLint warns on@typescript-eslint/no-explicit-any) - Prefer
unknownoveranywhen type is truly unknown - Unused vars prefixed with
_are allowed (e.g.,_config)
Naming
- Private class members: prefix with
_(e.g.,_config,_chart,_error) - Types: PascalCase (e.g.,
TokenObject,HistoryGenerator) - Functions: camelCase (e.g.,
deepResolveTokensAsync,fetchHistory) - Constants: camelCase or UPPER_SNAKE_CASE for true constants
- File names: kebab-case (e.g.,
echarts-raw-card.ts,history-decode.test.ts)
Formatting
- Prettier config (
.prettierrc):- 2 spaces indentation
- Double quotes
- Semicolons required
- Trailing commas everywhere
- Max line width: 100 characters
- LF line endings
- Always run
npm run formatbefore committing - ESLint integrates with Prettier (
eslint-config-prettierdisables conflicting rules)
Lit Components
- Use Lit decorators via static properties (e.g.,
static properties = { hass: { attribute: false } }) - Private reactive state uses
{ state: true } - Avoid one-letter variable names unless in tight loops
- ESLint plugin for Lit is enabled (
eslint-plugin-lit)
Error Handling
- Throw clear errors with context (e.g.,
throw new Error("Missing required 'option'")) - Prefix internal errors with
[echarts-raw-card]for debugging - The card renders errors inline in the UI (see
_errorstate) - Use tagged errors for downgradable warnings (e.g., invalid time ranges become warnings instead of hard failures)
Comments
- Prefer code clarity over comments
- Use
// ---------------------------------------------------------------------------to separate test sections (see test files) - JSDoc not heavily used; rely on TypeScript types
- Comments are allowed when explaining non-obvious logic (e.g., cache bucketing, fingerprinting)
Testing
Framework
- Vitest (compatible with Vite config)
- Tests live in
tests/directory - Test files:
*.test.ts
Running Tests
npm test # Run once
npm run test:watch # Watch mode
Test Patterns
- Use
describefor grouping related tests - Use
itfor individual test cases - Clear test names:
it("reads entity_id (normal format)", ...) - Section separators:
// --------------------------------------------------------------------------- - Tests focus on units (functions, utilities) not full integration (card lifecycle)
Example Test Structure
import { describe, it, expect } from "vitest";
import { histEntityId } from "../src/history/decode";
describe("histEntityId", () => {
it("reads entity_id (normal format)", () => {
expect(histEntityId({ entity_id: "sensor.temp" })).toBe("sensor.temp");
});
it("returns undefined when no key present", () => {
expect(histEntityId({ state: "on" })).toBeUndefined();
});
});
Token Resolution System
The card's main feature is resolving Home Assistant tokens in ECharts options.
Token Types
1. $entity (single entity binding)
$entity: sensor.living_room_temp
$coerce: number
$round: 1
$default: 20
Supported transforms:
$coerce:auto|number|string|bool$attr: read attribute instead of state$default: fallback value$abs,$scale,$offset: numeric transforms$min,$max,$clamp: bounds$round: decimal places$map:log|sqrt|pow
2. $data (bulk entity extraction)
$data:
entities: [sensor.a, sensor.b, sensor.c]
mode: pairs # or "names" or "values"
name_from: friendly_name
coerce: number
exclude_unavailable: true # default
exclude_zero: false
sort: desc # or "asc" or "none"
limit: 10
Outputs:
pairs:{ name, value }[](for pie charts, etc.)names:string[]values:unknown[](usecoerce: numberfor numeric output)
3. $history (historical data)
$history:
entities: [sensor.temp]
hours: 24
mode: values # or "series"
coerce: number
cache_seconds: 30
sample:
max_points: 300
method: mean # or "min", "max", "first", "last"
- Fetches from Home Assistant history API
- LRU cache with configurable TTL (default 30s)
- Automatic downsampling (LTTB algorithm)
- Supports both normal and
minimal_responseformats
4. $statistics (aggregated statistics)
$statistics:
entities: [sensor.energy_daily]
period: day # or "hour", "week", "month", "5minute"
stat_type: change # or "mean", "min", "max", "sum", "state"
days: 14
mode: values # or "series", "pairs"
cache_seconds: 300
- Uses HA long-term statistics API
- Perfect for daily totals, weekly averages, etc.
- Higher default cache TTL (300s) since stats change slowly
Resolution Flow
- Card receives config (
setConfig()) - Deep token resolution (
deepResolveTokensAsync()intokens/resolve.ts)- Recursively walks option tree
- Identifies token objects via type guards (
isTokenObject,isHistoryGenerator, etc.) - Resolves each token type
- Tracks watched entities for change detection
- Apply transforms (in
tokens/transforms.ts)- Coercion → abs → scale → offset → clamp → round → map
- Set ECharts option (
chart.setOption(resolvedOption))
Change Detection & Caching
Entity Watching
- The card tracks all entities referenced in tokens (
_watchedEntities) - On
hassupdate, it fingerprints watched entities (state|last_updated) - Only re-resolves if fingerprints change (see
card/watched.ts)
History Cache
- LRU cache with 100 entries (
_historyCache) - Cache key includes: entities, time range, transforms, mode, downsampling
- Prevents re-fetching on every state change
- Default TTL: 30s (configurable via
cache_seconds)
Statistics Cache
- Separate LRU cache with 50 entries (
_statisticsCache) - Longer default TTL: 300s (stats change slowly)
Throttling
- Card throttles history re-fetches to prevent storms (
_nextHistoryAllowedMs) - Minimum cache window computed from option tree (
minHistoryCacheSecondsInOptionTree())
ECharts Integration
Instance Lifecycle
Managed in echarts/instance.ts:
- Init:
initChart(container, theme, renderer)– creates ECharts instance - Resize:
safeResize(chart)– debounced resize on container size change - Dispose:
disposeChart(chart)– cleanup on disconnect - Size check:
hasSize(container)– ensures container has non-zero dimensions before rendering
Theme Support
- Automatically switches between
undefined(light) and"dark"theme based on HA'shass.themes.darkMode - Chart is re-initialized when theme changes
Renderer
- Default:
canvas - Configurable:
renderer: "svg"in card config
Build System
Vite Config (vite.config.ts)
Dev server:
- Port 5173
- CORS enabled (for HA dev)
- Host exposed (
host: true)
Build:
- Target: ES2022
- Single-file output (
inlineDynamicImports: true) - Source maps always included
- Minify only in CI/production (
BUILD_MODE=productionorCI=true) - Output:
dist/echarts-raw-card.js
Why single-file?
Home Assistant prefers single-file JS modules for Lovelace cards. Vite's inlineDynamicImports ensures all dependencies (ECharts, Lit) are bundled into one file.
Debugging
Card Debug Mode
Add debug to card config (top-level, not inside option):
type: custom:echarts-raw-card
debug: true # or { show_resolved_option: true, log_resolved_option: true }
option:
...
Debug outputs:
show_resolved_option: renders debug panel in card UIlog_resolved_option: logs to browser console (Verbose level)max_chars: limit debug output size (default: 10000)
Common Issues
- Edit fails with "old_string not found": Whitespace mismatch. View file, copy EXACT text including indentation.
- Tests fail: Check that transforms are applied in correct order (see
tokens/transforms.ts) - Card not updating: Check entity fingerprints in
card/watched.ts - History empty: Check cache TTL, time range validity, entity availability
Git Workflow
- Main branch:
main - Clean working directory at conversation start (see git status in env)
- Don't commit unless user explicitly asks
- Don't push to remote unless explicitly asked
Important Gotchas
-
Whitespace matters in edits: Always view file first, copy EXACT text including spaces/tabs/newlines. Include 3-5 lines of context.
-
Build output is single-file: Don't be surprised by large
dist/echarts-raw-card.js– ECharts is fully bundled. -
History time ranges must be finite: Invalid
start/endwill throw. Card catches and downgrades to warning. -
Entity tokens default to
coerce: "auto": History tokens default tocoerce: "number". -
Unavailable entities excluded by default:
$dataexcludes unavailable/unknown entities unlessinclude_unavailable: true(legacy). -
Cache keys are time-bucketed: Implicit
endtimes are bucketed to cache window to avoid cache thrashing. -
ESLint + Prettier integration: Don't manually resolve formatting conflicts. Run
npm run lint:fix && npm run format. -
Tests use Vitest, not Jest: Syntax is similar but config is in
vite.config.ts(implicit). -
Binary sensor coercion: Binary sensors (
on/off) are coerced to1/0whencoerce: numberis used (seetokens/transforms.ts). -
LRU cache eviction: Caches are bounded (100 history, 50 statistics). Old entries are evicted automatically.
Related Documentation
- README.md: User-facing docs, installation, usage examples
- PLAN.md: Development roadmap (if exists)
- CODE_REVIEW.md: Code review notes (if exists)
- FUTURE_PLANS.md: Future feature ideas (if exists)
- Documentation site: Full reference, recipes, examples
When Making Changes
- Read before editing: Always view files before modifying. Note exact whitespace.
- Run tests after changes:
npm test - Check types:
npm run typecheck - Lint & format:
npm run lint:fix && npm run format - Test locally:
npm run devand test in Home Assistant dev setup - Build before committing:
npm run build(ensure it succeeds) - Follow existing patterns: Check similar code in the same module
- Update tests: Add/update tests for new functionality
Quick Reference
| Task | Command |
|---|---|
| Start dev server | npm run dev |
| Run tests | npm test |
| Watch tests | npm run test:watch |
| Type-check | npm run typecheck |
| Lint | npm run lint |
| Format | npm run format |
| Build | npm run build |
| Full CI check | npm run lint && npm run format:check && npm run typecheck && npm test && npm run build |
Last updated: 2026-02-07
Project version: 0.1.0
Node version: 20+