This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A single-file Home Assistant Lovelace custom card distributed via HACS. It renders an indoor air-quality dashboard with gradient SVG graphs and WHO/EPA-based recommendations.
There is no build step and no node_modules. The shipped artifact is air-quality-card.js itself.
node test.js # run the full unit test suite (only available command)Tests pass/fail via a tiny inline assert helper; the script process.exit(1)s on any failure. There is no test runner, no linter, no formatter, no bundler. Don't add one unless asked.
To test in Home Assistant: copy air-quality-card.js to /config/www/air-quality-card/ and reload the dashboard. Manual UI testing is the only way to verify rendering — the test suite covers thresholds and logic, not the DOM.
air-quality-card.js registers two web components at the bottom:
-
air-quality-card—class AirQualityCard extends HTMLElement(vanilla, shadow DOM, no framework). -
air-quality-card-editor—class AirQualityCardEditor extends LitElement. LitElement is not imported; it's grabbed at runtime by walking up the prototype of an HA built-in:const LitElement = Object.getPrototypeOf( customElements.get("hui-masonry-view") || customElements.get("hui-view") );
This is intentional — the card has no dependencies and must run as a single file dropped into
/config/www/. Don't try toimportlit; it would break the distribution model.
setConfig(config)— validates that at least one sensor entity is configured, merges defaults, resets_renderedand_historyLoadedso the nexthasssetter rebuilds the card.set hass(hass)— runs_initialRender()+_loadHistory()exactly once, then_updateStates()on every state change._initialRender()writes the entireshadowRoot.innerHTML(HTML +<style>) based on which entities are configured. Sections are conditionally included via${showCO2 ? '...' : ''}template literals._updateStates()does targeted DOM updates byid— it does not re-render. When adding a new field, both_initialRender(to add the element) and_updateStates(to update it) need changes._renderGraphs()calls_renderGraph()per sensor, building SVG paths into pre-existing<svg>elements.
_getRecommendation() is a priority cascade — order matters and is load-bearing:
- CO life-safety first (
> 100→ Danger,> 35→ Warning). These are never suppressed by the outdoor-air override. - CO2 / PM2.5 / PM10 / HCHO / tVOC / humidity rules in priority order.
- Outdoor override: if an outdoor PM2.5 or CO2 entity reads worse than indoor, ventilation recs are swapped for "Run Air Purifier" or "Keep Windows Closed". The
ventilationRecsallowlist intentionally excludes CO recommendations — don't add CO to it.
_getOverallStatus() is a separate cascade for the header badge color (CO and radon take priority over CO2/PM2.5).
_getRadonAdvisory() is its own banner, completely separate from the main recommendation. Radon changes over days/weeks and needs professional mitigation, not "open a window," so it has its own info/warning/danger tiers based on the EPA action level (4.0 pCi/L = 148 Bq/m³). It uses max(short-term, long-term) in Bq/m³.
- Radon: stored/compared internally in Bq/m³.
_getRadonBqm3(value)converts based on_isRadonPciL()(1 pCi/L = 37 Bq/m³). User can force units viaradon_unitconfig; otherwise auto-detect from the sensor'sunit_of_measurement. - Temperature:
_isCelsius()checkstemperature_unitconfig first, falls back tohass.config.unit_system.temperature. Color thresholds are duplicated for °C and °F branches. - tVOC:
_isVOCIndex()distinguishes Sensirion VOC Index (unitless 0–500) from absolute ppb. Auto-detect treats missing/emptyunit_of_measurementor"voc index"as VOC Index. Different threshold tables apply to each.
A new sensor type touches many places. Use this checklist:
constructor— add key tothis._history = { ... }.setConfig— add tohasEntityvalidation OR-chain.getCardSize— add+= 1._loadHistory— push promise + key._getXColor()helper._initialRender— addshowXflag and the conditional HTML block (status row + graph card)._updateStates— read state, update DOM by id._renderGraphs— call_renderGraphwith min/max/unit._getRecommendationand/or_getOverallStatusif it should drive recs.- Editor
_schema()and_computeLabel(). test.js— theexpectedKeyslist and color/recommendation tests.
The "outdoor mirror" pattern: most pollutants have an outdoor_<x>_entity counterpart that's plumbed through the same machinery (history key outdoor_<x>, dashed line in _renderGraph, optional input to outdoor-override logic).
CARD_VERSIONis a const at the top ofair-quality-card.js(line 9). Bump it when shipping changes — it's logged to the browser console on load and is the canonical version for HACS.hacs.jsondeclares the filename and min HA version (2024.1.0). Don't renameair-quality-card.jswithout updatinghacs.json..github/workflows/validate.ymlruns HACS validation on every push/PR — there's no CI fornode test.js, so run it locally before committing.
test.js mocks HTMLElement, customElements, window, and document before require('./air-quality-card.js') so the file can register itself in a Node environment. It then pulls the registered class out of registeredElements and instantiates it with a fake _hass. The editor (air-quality-card-editor) only registers when customElements.get("hui-masonry-view") returns a class — the mock provides MockHuiView for that. If you change the LitElement-grabbing code, update the mock accordingly.