Releases: jpettitt/smart-icons
Release list
v0.3.1a1
First alpha of the v0.3.1 line. Two threads of work: a code-review
sweep that tightens the integration's robustness without changing
user-visible behavior, and two UX fixes for the rule editor that
came out of dogfooding the v0.3.0 GA.
What's new
-
Panel YAML view: Save / "Show visual editor" are visible again
on long configs.ha-code-editor's hostmax-heightdoesn't
constrain the internal CodeMirror surface — the editor grew with
content and pushed the action row off-screen. Switched to
--code-mirror-max-height(the CSS variable HA's own
automation editor uses), which makes the editor scroll
internally and keeps the action row visible. Same fix applied
to the per-rule code-mode editor. -
Rule editor: backdrop click and ESC no longer silently throw
away unsaved edits. The editor now tracks a dirty flag; when
the user has unsaved work and clicks off the dialog (or presses
ESC), a "Discard changes? [Keep editing] [Discard]" confirm
appears. Save success and clean dismissal pass through
unchanged. Implementation note: HA's modernha-dialogwraps
a native<dialog>and bindspointerdown(notclick) for
backdrop close, so the guard is a document-capture
pointerdownlistener — bubble-phase listeners on the element
are too late.
Internal hardening (from code-review sweep)
- Validation size caps: mapping at 200 entries, thresholds at
50,source_attributelength at 255,replace_allbatch at
1000. Bounds the synchronous validation/scan loops against
pathologically large payloads. - Validation tightening: empty
thresholdslists explicitly
rejected; non-string mapping keys rejected (YAML's bare1:
was silently coerced viastr(), producing rules that didn't
fire); non-stringsource_attributerejected (used to be
vol.Any(str, None)which accepted weird types via voluptuous
coercion). - Setup race fix:
hass.data[DOMAIN]and the store/injector
pointers are now wired beforeinjector.async_start()and WS
command registration, so a WS client racing setup can't see a
partially-populatedhass.data. - Injector attr pre-check: state-update attribute writes only
allocate a new dict when at least one of color / icon /
background actually changed. Eliminates a per-event allocation
for the common no-op case. - Painter memory hygiene: light-DOM and shadow-DOM
removedNodesfrom the MutationObserver now prune
knownHostsimmediately, instead of letting the set grow
until the next full sweep. Fixes a slow leak in busy
Lovelace dashboards with cards rotating in and out. - State watcher:
repaintAll()now only fires when the
smart_icons:decoration attrs were involved (either present
on the old state or present on the new). Filters out the long
tail of unrelated entity updates. - Frontend glob matching parity:
matchGlobnow translates
fnmatch's[!abc]negation to JS regex[^abc], matching the
backendfnmatch.translatesemantics. Previously a glob
pattern with[!rendered targets visually mismatched between
the table and the live injector. - Frontend source-default parity: serialize matches backend's
rule of "only a pure single-literal target defaultssourceto
the target." Multi-target rules with blank source no longer
collapse to a single-source rule on save. - Version-from-manifest:
INTEGRATION_VERSIONis now read
frommanifest.jsonat import time instead of duplicated as a
hardcoded string inwebsocket_api.py. A regression test
locks the two together.
Internal
- Build-time banner in the panel bundle (
[smart-icons] panel bundle build <timestamp>) prints to the browser console on
mount, so future "did my rebuild land?" investigations end with
one console line instead of a guess.
Full changelog: v0.3.0...v0.3.1a1
v0.3.0
GA release of the v0.3 line. Ships the consolidated content of
the v0.3.0a1, a2, and a3 alphas — see those entries below for the
incremental development history. This entry is the upgrade summary
for users coming from v0.2.x.
Breaking changes from v0.2
-
Template mode (
mode: template) is removed. Template-mode
evaluation was inert through v0.2 / v0.3 alpha (the evaluator
always returnedNone); v0.3.0 drops the dead code, the schema
field, the editor template view, and the YAML round-trip.
Stored rules withmode: templatefail validation on load and
are silently dropped by the store's per-rulevol.Invalid
catch. Migration: build the same logic out of stacked
mapping and threshold rules with the new field-level merge —
seedocs/examples.mdfor patterns
including the sun rising/setting + elevation-banding example
that demonstrates the dead-zone fallthrough trick. -
Priority is now field-level, not rule-level. The v0.2
"highest-priority rule erases everything else" semantic is
replaced by per-field merging. The highest-priority rule that
addresses a given field (color, icon, or background) wins
that field; lower-priority rules fill in fields the winner
doesn't touch. Concretely: a chip-only rule at priority 99
coexists with a color-by-state rule at priority 10 — chip from
the high rule, color from the low rule. The v0.2 behavior
dropped the color. Migration: any rule chain that relied on
the implicit "winner takes all" behavior should set explicit
null /""/"inherit"/"unset"sentinels on fields the
high-priority rule needs to release — sentinels actively block
lower-priority contributions, distinguishing "I have no opinion"
from "I want this cleared." See
DESIGN.md § 4.2
for the full semantics.
What's new since v0.2
- Per-rule background chip (
background_color). Mushroom-
style colored circle rendered behind the icon. Set on a mapping
entry or threshold entry; eithercolor,background_color,
both, or neither may be present per entry. Accepts every CSS
color string includingrgba()for translucent chips. - Field-level priority merging. Replaces v0.2's winner-takes-
all rule (see breaking-changes section above). - YAML editing on
ha-code-editor. The per-rule YAML view in
the rule editor and the whole-config YAML view in the panel
both use HA's CodeMirror 6 surface — same one the automation
editor uses. Syntax highlighting, search/replace, entity- and
icon-name autocompletion, and a Ctrl+S / Cmd+S save shortcut.
Per-rule validation errors remain clickable to jump-to-line. - Rule editor: HA-native form elements throughout. Every text
/ number field isha-input; pickers areha-selectorand
ha-icon-picker; primary buttons areha-button. Mapping and
threshold rows show Icon color + Background side-by-side in
a paired color picker. Threshold comparators got plain-English
labels (< Less than,≤ Less than or equal, etc.). - Glob-target resolution cache in the injector. v0.2 ran
fnmatch.filteragainsthass.states.async_entity_ids()on
every relevant state change per glob rule —O(rules × globs × entities)per state change. v0.3 caches resolved sets per rule
with surgical invalidation on rule changes, entity-registry
events, and new-entity appearance. Cache hits are O(1) — a
meaningful win on large installs (5k+ entities, many globs). - Visual examples in
docs/examples.md. The doors and sun
examples now show actual rendered icons in side-by-side tables
with the YAML that produces them. Sun icons are theme-aware
via<picture>+prefers-color-schemeso the deep-night
blues remain legible in dark mode. - README "See it in action" section near the top with a
preview of the sun outcome at six representative angles, linked
to the full example. - Notes for integration developers in the README documenting
Smart Icons' syntheticstate_changedwrites (attribute-only
updates firestate_changedwithstateunchanged; downstream
listeners that don't care about attribute updates should filter
onnew_state.state != old_state.state).
Bug fixes since v0.2
- Stale icons no longer stick after a rule drops the
icon
field. v0.2 wrote the icon attribute but never cleared it,
leaving the previous glyph in place until HA restart. The
injector now tracks the lasticonit wrote per target and
pops the attribute when the rule no longer addresses it — only
if the current value still matches our last write, so source
integrations that overwrote the icon with their own value are
left alone. - Background-only rules now paint. Earlier v0.3 alphas gated
the painter onsmart_icons_colorbeing non-empty, so a rule
setting onlybackground_colorwrote the attribute server-side
but the painter ignored it. The painter's two paint paths
(setter patch + DOM-crawler) are now deduped into a single
decideAndPainthelper that triggers on either attribute. - Editor: thresholds-mode validation properly checks for
meaningful entries. Earlier the "needs at least one entry"
check used a broken comparison that never fired; now it
properly requires a comparator or at least one decoration
field per row. - Editor: degenerate mapping rows no longer save. A row with
a key but no color / bg / icon used to serialize as{key: {}}
— schema-valid but inert at runtime. These rows are now
dropped on save with a validation error.
Internals
pick_winnerretired in favor ofmerge_decorations(Python)
/mergeDecorations(TypeScript). Sparse positions returned
fromevaluate_thresholds/evaluate_mappinglet the merger
distinguish "no position" from "explicit release."- Painter deduped into a single
decideAndPaint(host, color, bg)
helper shared by thestateObjsetter patch and the
paintHostcrawler — eliminates the source of the bg-only
paint bug. - TypeScript evaluator at parity with Python: handles
background_colorand mirrors the merge semantic exactly. Not
on the paint path (the backend injector is authoritative) but
the divergence would have been a latent trap for any future
preview UI. - Backend test coverage: 118 tests covering the merger, bg-only
paths, icon-clear contract, source-integration-overwrites
safety, the glob cache, and template-mode rejection. - Frontend test coverage: rule-editor unit tests added (was
previously only covered by Playwright e2e smoke). Web-test-
runner config updated to pass the project tsconfig through so
Lit's legacy decorators load under test.
Upgrade from v0.2
Drop-in upgrade with two behavior changes to verify after install:
-
Template-mode rules silently drop on load. If you have any
rules withmode: templatein your storage doc, they will fail
validation on load and be removed from the panel. Template
mode was inert at runtime since v0.2 (the evaluator returned
None), so functionally nothing changes — but the rule
disappears. Rebuild the logic with stacked mapping / threshold
rules first if you need to preserve the behavior. -
"Winner takes all" → field-level merge. Any installation
that depended on the v0.2 "highest-priority rule erases
everything else" semantic should be re-checked. If a
high-priority rule needs to actively hide a lower-priority
rule's contribution to a field, set that field tonull/
""/"inherit"/"unset"explicitly. Sentinels block
lower-priority contributions; absence allows them to flow
through.
v0.3.0a3
Alpha 3 on the v0.3 line. Pivots the v0.3 line away from the
contrasting outline approach to a per-rule Mushroom-style background
chip, replaces the v0.2 "winner takes all" priority semantic with
field-level merging, removes template mode entirely after it
spent two minor versions as inert storage-only code, and migrates the
YAML editing surface from bare textareas to HA's ha-code-editor.
The post-review pass also fixed two real bugs surfaced in v0.3.0a1/a2
(stale icons after a rule drops its icon; bg-only rules not painting)
and added a glob-target resolution cache to the injector so large HA
installs don't burn CPU re-running fnmatch on every state change.
Breaking changes
-
Template mode is gone.
mode: templateand thetemplate
decoration field had been demoted to "storage-only / demand-driven"
since v0.2 — the evaluator returnedNone, so any rule with
mode: templatewas inert at runtime. Rather than keep dead code
around (and a deprecated dropdown option in the editor, plus a
"deprecated" legend in the template fieldset), v0.3.0a3 removes
the schema entry, the editor's template view, and all related
plumbing. Stored rules withmode: templatefail validation on
load and are silently dropped by the store's per-rule
vol.Invalidcatch. Migration: convert any template logic to
stacked mapping / threshold rules (see
docs/examples.md) before upgrading. If a
stored rule with a straytemplatefield on a non-template-mode
rule survived a prior upgrade, that rule also fails to load now
(the rule schema is PREVENT_EXTRA by default) — re-create it via
the editor. -
Installation-wide outline toggle is gone. The
smart_icons/get_optionsandsmart_icons/update_options
WebSocket commands, thesmart_icons_options_updatedbus event,
theoutline_enabledstorage field, and the Contrasting outline
on painted icons checkbox in the panel are all removed. Rules
now opt in to a chip per-rule via the newbackground_color
decoration field instead. Existing installs with
outline_enabled: falsesaved from v0.3.0a1/a2 silently lose the
setting on upgrade (it's no longer read); no migration is needed
because nothing was rendered against the old toggle in this
branch's contents. -
Priority is now field-level, not rule-level. A high-priority
rule no longer erases lower-priority rules' contributions to
fields it doesn't address. Concretely: a chip-only rule
(background_coloronly) at priority 99 now coexists with a
color-by-state rule at priority 10 — chip from the high rule,
color from the low rule. The v0.2 behavior dropped the color.
Users who depended on the old "winner takes all" semantic to
hide lower-priority decorations should now set explicit
null /""/"inherit"/"unset"sentinels on the fields
they want released; sentinels in a high-priority rule block
lower-priority contributions to that field. See
DESIGN.md § 4.2
for the full semantic.
What's new
-
Per-rule background chip (
background_color). Replaces the
v0.3.0a1 installation-wide outline. The chip renders as a colored
circle behind the icon, à la Mushroom —background-color+
border-radius: 50%+box-shadow: 0 0 0 5px <color>on the
<ha-state-icon>host. The shadow technique extends the visible
chip past the host's 24×24 box without taking layout space
(~34 px visible chip on a 24 px icon, Mushroom's ~1.42× ratio).
Available on mapping entries and threshold entries; accepts any
CSS color string includingrgba()for translucent chips:mapping: 'on': color: '#ffeb3b' background_color: '#b71c1c' 'off': background_color: '#1b5e20'
Either
color,background_color, both, or neither may be set
per entry. A bg-only entry leaves the icon's natural color alone
and just paints the chip. -
Field-level priority merging. Decorations are now merged per
field instead of per rule. The injector walks matching rules in
priority order and, for each ofcolor/icon/
background_color, takes the value from the highest-priority
rule that addresses it. Equal priorities resolve in declaration
order (matches v0.2). Explicit sentinels (null,"",
"inherit","unset") in a high-priority rule are positions
that explicitly release a field — they block lower-priority
rules from contributing that field, distinguishing "I have no
opinion" from "I want this cleared." See the new
merge_decorationsin
evaluator.py. -
Rule editor: foreground + background colors on one row. The
rule editor renders a paired color picker per decoration row —
Icon color on the left, Background on the right — for both
mapping and threshold entries. The native<input type="color">
swatch sits next to a free-form text field so users can paste
rgba()orvar(--…)values the swatch can't represent. The
threshold Comparator dropdown gained plain-English labels
(< Less than,≤ Less than or equal, etc.) and the
comparator + value share a single row. -
YAML editing now uses
ha-code-editor(HA's CodeMirror 6
surface — the same one the automation editor, blueprint inspector,
and trace viewer use). Both the per-rule YAML view inside the rule
editor and the whole-config YAML view in the panel switched from
bare<textarea>to<ha-code-editor mode="yaml">. Brings syntax
highlighting, search/replace (Ctrl+F), entity- and icon-name
completion, and a Ctrl+S / Cmd+S shortcut that fires the panel's
Save handler. Jump-to-rule and jump-to-line (clicking a per-rule
validation error) still work — rewritten on top of CodeMirror's
dispatch({ selection })API. The element is a lazy-loaded HA
chunk; the panel registers acustomElements.whenDefinedupgrade
so the YAML surface paints correctly on first navigation. -
Developer notes section in README.md. Documents
that Smart Icons fires syntheticstate_changedevents when
writing its three attributes, what filters downstream listeners
can use to ignore them, and how the icon-clear safety contract
interacts with other integrations writing the same target.
Bug fixes
-
Stale icons no longer stick after a rule drops the
icon
field. Before this release the injector would write the icon
attribute but never clear it: editing a rule to remove the icon
left the previous glyph in place until HA restart. The injector
now tracks the lasticonit wrote per target and pops the
attribute when the rule no longer addresses it, only when the
current value still matches our last write (so source
integrations that overwrite our icon with their own value aren't
clobbered). See_apply_target/_release_targetin
injector.py. -
Background-only rules now paint. The painter's two paint
paths (theha-state-icon.stateObjsetter patch and the DOM-
crawler fallback) both gated the entire decoration call on
smart_icons_colorbeing non-empty. A rule that set only
background_colorwrote the attribute server-side but the
painter ignored it. Both paths now trigger paint when either
attribute is set, and the gating logic is centralized in a
singledecideAndPainthelper so the two paths can't drift
again. -
Rule editor: thresholds-mode validation was broken. The
"needs at least one entry" check compared the comparator
function's return value with!== null, but the function
returns''for "no comparator selected" — so every threshold
row, including a completely blank one, looked valid and the
check never fired. Now properly checks each entry has either a
comparator or at least one decoration field (color, icon, or
the new background). -
Rule editor: degenerate mapping rows no longer save. A
mapping row with a key but no color / bg / icon used to
serialize as{key: {}}— schema-valid, but the evaluator
treated the empty decoration as "no match" and did nothing at
runtime, so the rule looked stored but had no effect. These
rows are now dropped on save with a validation error
(Mapping mode needs at least one state → decoration entry).
Internals
-
pick_winnerretired in favor ofmerge_decorations
(Python) /mergeDecorations(TS). The old name is kept as an
alias for back-compat. Both evaluators now return sparse
position objects fromevaluate_thresholds/evaluate_mapping
— only fields the matching entry positively addressed appear —
so the merger can distinguish "no position" from "explicit
release." -
Painter deduped: one
decideAndPaint(host, color, bg)helper
is the single source of truth for the "given these resolved
attrs, what do we do?" decision. Both thestateObjsetter
patch and thepaintHostcrawler call it. Reduces the chance
of one path drifting from the other as new attributes get added. -
TypeScript evaluator at parity with Python: the v0.3.0a1/a2 TS
normalizeDecorationsilently droppedbackground_color. It
now handles all three fields and mirrors the merge semantic
exactly. Not currently on the paint path (the backend injector
is authoritative) but the divergence would have been a latent
trap for any future preview UI — and tests now catch a
divergence here. -
Frontend tests gained a
rule-editor.test.tssuite (the editor
had no unit tests until this release; only the playwright e2e
smoke). The web-test-runner config was updated to pass
tsconfig.jsonthrough so Lit's legacy@customElement/
@propertydecorators load under test (the dev bundle's
experimentalDecoratorsflag wasn't visible to wtr's esbuild
before). -
Glob-target resolution cache in the injector. v0.2 and the
v0.3 a...
v0.3.0a2
Alpha 2 on the v0.3 line. Closes the rule-editor bare-form-elements
debt called out in v0.3.0a1's release notes. No user-visible feature
changes — the rule editor looks slightly more polished (HA-native
field styling instead of bare-input fallback) but every existing rule
edits and saves the same way.
What's new
- Rule editor migrated to HA-native form elements. Every text /
number input that drove the rule form now renders asha-input
(HA's current input, which replacedha-textfieldon 2026-04-01).
The Mode dropdown and threshold-comparator dropdown both render as
ha-selectorwith{ select: { ..., mode: 'dropdown' } }config.
The "+ Add entry" / "+ Add state" / "+ Add pattern" action buttons
render asha-button variant="neutral". Bare HTML survives only in
the cases the newdocs/ha-elements-guide.mddecision tree
endorses: the<input type="color">swatch (no first-class HA
color element), the per-rule and whole-config<textarea>YAML
editors (ha-code-editoris overkill), and icon-button-style
affordances (<button class="btn-icon">×</button>row delete,
<button class="text-toggle">code-editor toggle,
<button class="action-error-dismiss">). Each carries an inline
comment naming the guide. - Drag-to-reorder thresholds. The per-row ↑ / ↓ buttons in
threshold rules are replaced with a single drag handle on the
left of each row, using HA'sha-sortable(the same wrapper
HA's automation, dashboard, and area editors use). Six-dot
mdi:dragglyph, grab/grabbing cursor, drop-anywhere
reordering. The threshold-row layout is now a 2-column grid
(handle column + indented content column) so the per-row
fields stay aligned regardless of how many lines they wrap to. docs/ha-elements-guide.md(new) — internal/contributor
reference for whichha-*elements to use, when bare HTML is OK,
defensive patterns for lazy-load timing, and HA design tokens.
Adapted from the sibling reference inweather-radar-card; the
two docs are kept in sync.- Playwright e2e smoke tests for the rule editor. Five specs
covering the silent zero-height failure mode the lazy-load guide
warns about (if anha-*element didn't register, the rendered
box collapses to zero — no console error). Runs against the
docker testbed, sub-second per spec after auth is cached. See
frontend/test-e2e/README.mdfor the setup workflow.
Internals
- 112 pytest + 85 Web Test Runner + 5 Playwright e2e + typecheck
green; bundle drift clean. frontend/src/panel/rule-editor.ts: ~22 element conversions plus
the connectedCallbackwhenDefineddefensive list extended to
coverha-input,ha-button,ha-switchalongside the existing
ha-icon-pickerandha-selector.AGENTS.mdCode conventions section refreshed to point at the
new guide.- INTEGRATION_VERSION + manifest + frontend/package versions bumped
to 0.3.0a2.
Upgrade
Drop-in from v0.3.0a1 — no behavior change, no schema migration.
Full Changelog: v0.3.0a1...v0.3.0a2
v0.3.0a1
Alpha. First release on the v0.3 line. Ships the
contrasting-outline feature and the supporting options storage +
WS plumbing. Drop-in upgrade from v0.2.2; no schema migration.
HACS users on the beta channel will pick this up; regular-channel
users stay on v0.2.2 until v0.3.0 GA.
What's new
- Contrasting outline on painted icons. Smart Icons now draws a
thin black or white outline around every icon it paints, picked
automatically for contrast against the painted color (W3C relative
luminance). Fixes the "yellow icon on a light theme card" /
"dark-blue icon on a dark card" readability failure mode that
motivated this feature. Implemented as a native SVG
paint-order: stroke fillon the inner glyph path — composited
on the GPU in a single render pass alongside the fill, no CSS
filter overhead. See
docs/icon-outline-prototype-results.md
for the prototype variants tested and why this approach won. - Installation-wide outline toggle. New checkbox above the rules
table in the Smart Icons panel: Contrasting outline on painted
icons (default on). Admin-only, persisted in the integration's
storage alongside the rules, applies live to every painted icon
across the install via the newsmart_icons_options_updated
bus event. Disable if you have a theme or design language that
prefers unstyled icons. - Options storage + WS commands. New top-level
optionsdict in
smart_icons.rulesstorage doc — future installation-wide
preferences (e.g. an "outline every icon" mode) will live here
without a schema bump. Two new WS commands:smart_icons/get_options
(any authenticated user, so the painter bundle can read defaults
for non-admin viewers) andsmart_icons/update_options(admin
gate).
Internals
- 112 pytest + 85 Web Test Runner tests green (+12 backend, +12
frontend over v0.2.2); typecheck clean. frontend/src/outline-proto.tsremoved; replaced by the
shipped-qualityfrontend/src/outline.ts(rename preserved in
git history).- Template-mode evaluation moved from a v0.3 commitment to
TODO.md's "Followups & ideas" parking lot — rule
stacking (priority + selective matching) already covers the
use cases template mode was meant for. See the new note in
TODO.md and the worked example in
docs/examples.md.
Known debt
- The rule editor uses plain HTML
<input>/<select>/
<textarea>/<button>styled with HA CSS variables (a
workaround dating to a historicalha-textfieldlazy-load
bug). Targeted for conversion toha-textfield/ha-select/
ha-buttonin v0.3.0a2 per the project's
no-bare-form-elements rule. Doesn't affect the alpha's
user-visible behavior; flagged here for transparency. See
TODO.md.
Upgrade
Drop-in from v0.2.2 — no schema migration. The outline is on by
default for the readability improvement; disable from the panel
toggle if you prefer unstyled icons.
Full Changelog: v0.2.2...v0.3.0a1
v0.2.2
GA on the v0.2.2 line — promotes the painter-reliability fixes from
the two betas, adds a missing-button fix in the delete confirm dialog,
ships a starter pack of paste-ready example rules, and overhauls how
releases are published. Drop-in upgrade from v0.2.1, b1, or b2; no
schema changes.
Bug fixes since v0.2.1
- Painter walk-up for
<state-badge>surfaces (from b1). The
painter was readingentity_iddirectly from each
<ha-state-icon>'sstateObj, which is missing in some HA
surfaces —<state-badge>wrappers (entities-card rows, more-info
dialog headers) carry thestateObjon the wrapper and pass only
attribute hints down. Painter now walks up through parents (across
shadow boundaries) to find the first ancestor with astateObj,
with a 12-hop guard. - Empty watcher cache on slow-loading dashboards (from b2). The
watcher previously seeded itself from<home-assistant>.hass.states,
which is filled asynchronously and was empty on some setups at our
bootstrap. It now fetches authoritative initial state via the
get_statesWS command, buffers anystate_changedevents that
arrive during the fetch, and drains them on top of the snapshot. - Icons un-painted after Lovelace view switches (from b2). The
MutationObserver crawler couldn't see entity bindings Lit was about
to make on view swaps. The painter now also patches
ha-state-icon'sstateObjproperty setter at bootstrap
(idempotent, prototype-chain-walking), so every binding HA
establishes flows through the painter synchronously. The crawler
stays as a defensive fallback. - Delete and discard confirm dialogs had no buttons. Modern
<ha-dialog>dropped theprimaryAction/secondaryActionslots
the confirm modals were targeting, so the dialog opened with text
but no way to confirm or cancel — leaving "edit the YAML" as the
only escape hatch. Buttons now live in the dialog body with a
dedicated action row.
What's new
- Cache-busted bundle URLs.
smart_icons.jsand
smart_icons_panel.jsnow ship with?v=<mtime>query strings so
every release (and every local rebuild) busts the browser cache
automatically. No more "I updated but I'm still seeing old
behavior" after a HACS upgrade. See
custom_components/smart_icons/frontend.py
for the mechanism. - Packaged release zip, attached as a GitHub release asset. A new
release.ymlworkflow runs pytest + the frontend test suite on tag
push, then builds and attaches a HACS-conventional
smart_icons.zip.hacs.jsondeclares the filename via
zip_releaseso HACS pulls the asset directly — which lets the
release page show real download counts rather than the source
tarball's blank counter. Drop-in for existing HACS users; no manual
action required. - Example-rules doc.
docs/examples.mdis a
growing collection of paste-ready rules — door/window contact
sensors, locks (with a cross-source door-open override), NWS
temperature color scale + stale-data warning, and sun-position
variants (elevation banded, direction aware, and a combined
two-rule pattern that demonstrates how priority + selective
matching achieves "templated" behavior without templates). Each
example explains the mechanics that make it work.
Internals
- 100 pytest + 73 Web Test Runner tests green (+4 new for the
cache-buster URL shape); typecheck clean. - Frontend version files bumped:
frontend/package.jsonand
frontend/package-lock.jsonaligned to0.2.2.
Upgrade
Drop-in from v0.2.1, v0.2.2b1, or v0.2.2b2 via HACS. The
cache-buster query strings ensure existing browser sessions pick up
the new bundles on next page load without a hard refresh.
Full Changelog: v0.2.1...v0.2.2
v0.2.2b2 — get_states bootstrap + ha-state-icon stateObj patch (beta)
Second beta on the v0.2.2 line. Closes out the remaining "icons not painted" reports from b1. Drop-in upgrade from b1 or v0.2.1 — no schema change.
Bug fixes
Empty watcher cache after bootstrap on slow-loading dashboards
The watcher was copying initial state from `.hass.states` inside `start()`. That map is populated asynchronously by HA's connection layer; on slower setups it was empty when our bundle reached `start()`, and any `state_changed` events that arrived during the subscribe-handshake await were lost. The result was slow-moving entities (temperatures, locks — anything that doesn't fire another state_changed for hours) staying permanently invisible to the cache.
The watcher now fetches authoritative initial state via the `get_states` WS command (same one HA's own connection extension uses), buffers any events that arrive during the fetch, and drains them on top of the snapshot. No race, no lost events, regardless of when `hass.states` happens to populate.
Icons un-painted after Lovelace view switches
The MutationObserver crawler was firing thousands of times during a view swap (~3400 callbacks for a 100-icon dashboard) but our scan logic walked past the new `` elements before Lit rendered into their shadow roots, so they never landed in `knownHosts` and never got painted. Symptom: a 133-icon view would drop to 7 known hosts after switching views.
The painter now also patches `ha-state-icon`'s `stateObj` property setter at bootstrap. Every entity binding HA establishes — in any card, in any surface, regardless of which wrapper holds the entity — flows through the patched setter and applies `smart_icons_color` synchronously, inside Lit's property pipeline. No DOM walking, no MutationObserver chain, no race against Lit's render.
The patch is idempotent (a `Symbol.for(...)` marker prevents double-wrapping) and walks the prototype chain so it works regardless of whether HA places the accessor on the class itself or a base. The DOM-crawler stays as a defensive fallback for HA versions where the prototype shape may change; a one-line `console.warn` is emitted if we can't find the accessor to wrap.
Internals
- Drop the unused `StateWatcher.getState()` method and `IconHost` re-export from painter.ts — both were artifacts of earlier iterations.
- New `applyStateObjPatch(klass)` exported from painter.ts (pure form of `patchHaStateIcon`) for unit-testing against a fake Lit-shaped class without touching the global `customElements` registry.
- 96 pytest + 73 Web Test Runner tests green (+5 new for `applyStateObjPatch`); typecheck clean.
- Painter bundle: 3.6 KB → 5.0 KB. Panel bundle unchanged.
Upgrade
Drop-in from v0.2.2b1 or v0.2.1. Enable Show beta versions in HACS to pick it up.
Full Changelog: v0.2.2b1...v0.2.2b2
v0.2.2b1 — painter walk-up fix (beta)
Beta. One fix on top of v0.2.1, shipped early to verify with HACS beta-channel users before promoting to GA. Drop-in upgrade — no schema change, no migration.
Bug fix
Some icons weren't being painted
The painter was reading `entity_id` directly from each ``'s `stateObj` property — fine for most surfaces (tile cards, more-info bodies, dev tools) but missing in others. Specifically, entities-card rows and more-info dialog headers wrap their icon in a `` that holds the entity's `stateObj` on the wrapper itself, passing only `data-domain` / `data-state` attributes down to the inner ``. The painter would find the icon, see no `stateObj.entity_id`, and bail — leaving the icon stuck on whatever color HA's default render produced.
Painter now walks up through parents (across shadow-root boundaries) to find the first ancestor with a populated `stateObj` when the immediate icon has none. A 12-hop guard keeps the lookup cheap; the wrapping element is always 1–2 hops away in practice.
The watcher-based color path was unaffected for cases that already worked (it was already keyed by entity_id), but the same walk now provides the entity_id input regardless of which surface the icon sits in.
What this means in practice
If you had Smart Icons rules whose colors only sometimes showed up — working on tile cards but not in the entities-card list, for example — they should now work everywhere.
Internals
- New `resolveStateObj()` helper in the painter.
- Regression test reproduces the user-reported DOM shape (state-badge with stateObj on the host, ha-state-icon inside its shadow root with only data-* attributes).
- Painter bundle: 3.3 KB → 3.6 KB. Panel bundle unchanged at 115.5 KB.
- 96 pytest + 68 Web Test Runner tests green; typecheck clean.
Upgrade
Drop-in from v0.2.1. Enable Show beta versions in HACS to pick it up.
Full Changelog: v0.2.1...v0.2.2b1
v0.2.1 — in-panel YAML editing
Point release. Drop-in upgrade from v0.2.0 — no schema change, no migration. After updating, restart HA.
What's new
In-editor YAML view
Each rule now has a Show code editor / Show visual editor text toggle next to Cancel + Save in the rule editor — same pattern as HA's automation editor. The toggle round-trips: the form serializes to YAML on the way in, parses + re-hydrates back to the form on the way out. Save in code mode parses the YAML; the rule id is preserved so existing rules update in place rather than creating duplicates.
Use case: sharing a single rule. Edit the rule → Show code editor → Cmd-A, Cmd-C → paste into a gist.
Whole-config YAML view
A matching Show code editor toggle at the bottom of the panel itself. Entering code mode dumps every rule as a top-level rules: list, ready to paste into a gist. Save replaces the whole config atomically.
Use case: backing up the whole Smart Icons setup, importing a friend's setup, bulk-editing.
Atomic save
Whole-config saves go through a new smart_icons/replace_all WS command. The server validates every rule before touching storage — either the entire new set lands or nothing changes. No partial-update states on failure.
Clickable error highlighting
Every save failure renders as a clickable item under the textarea:
- YAML syntax errors (line/col from the parser) — clicking jumps the textarea caret to the reported position.
- Shape errors that name a rule (e.g. "Rule 2 is not a mapping") — clicking selects that rule's lines.
- Server per-rule validation failures — each rule's error is its own clickable item.
The first error is auto-selected on display so you land directly on the problem. Every failure block leads with "Rules unchanged." so atomicity is explicit.
Discard-changes confirm
Toggling from code back to visual with unsaved YAML edits now opens a confirmation modal. A misclick can't silently drop your edits.
Configuration example
The format the whole-config view produces — paste this into a fresh editor's code view to add a temperature color scale:
```yaml
targets:
- sensor.outdoor_temperature
mode: thresholds
thresholds: - lt: 32
color: "#0000FF"
icon: mdi:snowflake - lt: 75
color: "#33cc66" - lt: 90
color: "#FFA500" - color: "#FF0000"
icon: mdi:fire
```
Internals
- New `BulkReplaceError` exception + `RuleStore.async_replace_all` method. Admin-gated `smart_icons/replace_all` WS handler emits a custom error frame carrying `rule_errors: [{ index, message }]` for per-rule feedback.
- Frontend panel bundle gains `js-yaml`. Panel bundle: ~60 KB → ~115 KB. Painter bundle untouched at 3.3 KB (still WS-free, still works for non-admin users).
- 96 pytest + 67 Web Test Runner tests green; typecheck clean.
Design
Full design doc, including the as-designed vs as-shipped reconciliation: `docs/yaml-editing.md`.
Upgrade
Drop-in from v0.2.0. Rules in storage are unchanged; the new YAML view is purely a UI surface over the existing data.
Full Changelog: v0.2.0...v0.2.1
v0.2.0 — multi-target, glob, per-target source, admin-gating
First general-availability release of the v0.2 line. Rolls up everything from v0.2.0b1 and v0.2.0b2 and adds the admin-gating work that landed after b2.
This is a drop-in upgrade from v0.1.x. Existing rules keep working — the singular target: <entity_id> form is auto-migrated to the new targets: [<entity_id>] list on load. No manual migration, no YAML changes.
Highlights
Multi-target rules and glob target patterns
One rule can now apply to many entities at once. Pick them in the panel via HA's native multi-entity selector, or list them in YAML / JSON:
```jsonc
{ "targets": ["light.kitchen", "light.dining_room"] }
```
Targets can also include shell-style globs — `*`, `?`, `[set]`:
```jsonc
{ "targets": ["light.kitchen_*", "sensor.temp_?"] }
```
Patterns are resolved against `hass.states` at apply time. The panel shows a live "Matches N entities" preview as you type. Newly-added entities that match an existing glob pick up the rule automatically.
Per-target source semantics
For a multi-target or glob rule with no explicit `source`, each matched target reacts to its own state and `source_attribute`. One rule, every kitchen light colored by its own brightness:
```jsonc
{
"targets": ["light.kitchen_*"],
"source_attribute": "brightness",
"mode": "thresholds",
"thresholds": [
{ "lt": 64, "color": "#552200" },
{ "lt": 192, "color": "#ffaa00" },
{ "color": "#ffffaa" }
]
}
```
(No `source` field — that's per-target semantics: each matched light reads its own `brightness`.)
Single-literal-target rules still default `source` to the target, as before.
Mapping-state autocomplete
Each mapping-key cell in the editor offers a `` of states the resolved source entity has actually been observed in — the last seven days of recorder history plus its current state. Cached per-entity. Falls back to just the current state when the recorder is disabled.
Speeds up authoring mapping rules for entities with non-obvious state vocabularies (`lock`'s `locking` / `unlocking`, `alarm_control_panel`'s mode names, etc.).
Admin-only management
Both the WebSocket API and the sidebar panel are admin-gated. Non-admin users:
- Don't see the Smart Icons sidebar entry
- Get `unauthorized` from every `smart_icons/*` WS command
- Still see correctly painted icons on their dashboards — the painter bundle reads `smart_icons_color` directly from each entity's state attributes, no WS calls
The painter bundle shrunk from 4.4 KB → 3.3 KB after removing the rule store from it.
Rule editor UX overhaul
Section-grouped layout (Apply to / React to / Decoration / Options), inline error placement, sticky save bar at the bottom of the dialog, validation-gated Save button, Duplicate action, reorderable threshold entries (↑ / ↓), HA-native dialog-style delete confirmation. Action error banner for toggle/delete failures.
Responsive panel layout
The rules table reformats as a labeled card stack on narrow widths using a CSS container query against the panel card. Unlike viewport-based media queries, this correctly fires when the HA sidebar opens or closes (which changes the panel width independently of the viewport).
Integration brand icon
Painted-favicon brand mark under `custom_components/smart_icons/brand/` following HA's brands-proxy convention (HA 2026.3+). No manifest changes needed — the icon shows up automatically in Settings → Devices & services and HACS.
Bug fixes from the betas
- Painter color race eliminated. The painter and HA's card Lit re-render were both microtask-scheduled in the same tick with no ordering guarantee — the painter often read a stale `stateObj`, so colors appeared stuck until the user navigated away and back. The state-watcher now caches the full attribute bag synchronously inside the event dispatch; the painter reads from there, removing the race.
- Glob rules survive HA restart. Entities whose owning integration publishes them seconds after Smart Icons loads (the classic post-restart case for MQTT / Zigbee / lock integrations) are now caught by a `state_changed` listener filtered for `old_state is None`. The matching glob rule is applied immediately rather than only after a dashboard switch.
- mwc-button migrated to ha-button. `mwc-*` elements are unregistered in modern HA and render as invisible unknown elements — `ha-button` with `variant=brand|neutral|danger` is the right primitive.
- ha-selector replaces ha-entity-picker. Direct `ha-entity-picker` has a CSS click-area bug in dialog contexts (only a thin band at the top of each row was clickable). `ha-selector` with an entity selector config (the same dispatcher HA's own options flows use) hands off to the right picker variant per HA version.
- Mobile table overflow. The rules table now reformats as a labeled card stack below ~860 px.
Upgrade
Drop-in. No manual migration. After updating, restart HA. Existing rules keep working and the singular-`target` form is rewritten to `targets: [...]` on first load.
HACS users: if you previously opted in to beta versions, you can turn that off — v0.2.0 is the stable release of the v0.2 line.
Compatibility
- Home Assistant 2024.7 or newer (`StaticPathConfig` requirement — declared as `min_ha_version` in the integration manifest).
- Works on any card that uses ``: all default cards (entities, tile, glance, more-info) and most third-party cards (mushroom, button-card via its default icon path).
- Out of scope: cards that draw their own SVG icons (mini-graph-card, apex-charts-card custom paths).
Internals
- `manifest.json` declares `min_ha_version: "2024.7"`.
- WS version endpoint reads `homeassistant.version` directly.
- `store.async_load` exception catch narrowed to `(vol.Invalid, ValueError)` so unrelated bugs surface in the log.
- 86 pytest + 36 Web Test Runner tests green; typecheck clean.
Known limitations (unchanged from v0.1.1)
- Template-mode rules are stored but not evaluated at runtime — v0.3 work.
- Releasing a rule clears the color override but leaves the last injected icon on the target's state until the source integration pushes a fresh state update.
What's next (v0.3)
- Template-mode evaluation — Jinja rendered server-side, with a `smart_icons/render_template` WS command for live preview in the panel.
- Door 1 — entity settings dialog injection so individual entity pages get a "Smart Icon" section.
- YAML loader — for users who want to keep rules in `configuration.yaml` alongside the rest of their config.
- Translations framework.
Full Changelog: v0.1.1...v0.2.0