Skip to content

Commit d713246

Browse files
ploebbclaude
andcommitted
feat(tile): add invert_condition option for inverted attention rendering
Tile widgets gain invert_condition — a list of Lovelace card conditions (same format and evaluator as the visibility field). When non-empty and all conditions are met, the tile renders as a solid black card with white text/icon, with the icon forced to the flat no-circle glyph. On grayscale e-ink panels a full-tile inversion is an unmissable attention signal, where the default active-state icon tint reads as lower contrast than the inactive state. The editor gains an Invert conditions panel on tile widgets, built on the same ha-card-conditions-editor wrapper as the Visibility panel (generalized into _buildConditionsPanel). The ha-form value-changed handler now also preserves invert_condition, which ha-form would otherwise drop on any edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RdcF9h4rCMcnKeHWMBdrW
1 parent 409bad4 commit d713246

8 files changed

Lines changed: 637 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- **`invert_condition`** option on the Tile widget: a list of
13+
Lovelace condition dicts, same format as `visibility`. When the
14+
conditions are met the tile renders inverted (solid black card,
15+
white text/icon) as an e-ink "needs attention" signal.
16+
817
## [0.6.0] - 2026-07-07
918

1019
### Added

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,14 @@ The component ships a WYSIWYG Lovelace card for editing the dashboard layout.
169169
| [Waste Schedule](docs/waste_schedule.md) | Upcoming waste collection dates (today, tomorrow, in N days) |
170170

171171
All widgets support `x`, `y` positioning. Most support a `w` (width) override
172-
to constrain rendering to a sub-region of the display.
172+
to constrain rendering to a sub-region of the display. All widgets also
173+
support a `visibility` list of Lovelace conditions to show or hide the
174+
widget based on entity state or other conditions.
175+
176+
The Tile widget additionally supports `invert_condition` — a list of
177+
conditions in the same format as `visibility`. When met, the tile renders
178+
inverted (solid black card, white text/icon) as a "needs attention" signal
179+
on e-ink.
173180

174181
## Device setup
175182

custom_components/eink_dashboard/frontend/src/eink-dashboard-editor.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,17 @@ class EinkDashboardEditor extends HTMLElement {
22652265
if (!("visibility" in data) && cur.visibility) {
22662266
data.visibility = cur.visibility;
22672267
}
2268+
// Preserve invert_condition — managed by
2269+
// ha-card-conditions-editor, not by ha-form, so the spread
2270+
// would drop it. cur.invert_condition is never [] because
2271+
// _buildConditionsPanel deletes the key when conditions are
2272+
// cleared (empty array).
2273+
if (
2274+
!("invert_condition" in data)
2275+
&& cur.invert_condition
2276+
) {
2277+
data.invert_condition = cur.invert_condition;
2278+
}
22682279
this._widgets[index] = data as unknown as Widget;
22692280
this._fireWidgetChange();
22702281
this._updateSummary(index);
@@ -2285,6 +2296,17 @@ class EinkDashboardEditor extends HTMLElement {
22852296
this._buildEntriesEditor(container, index);
22862297
}
22872298

2299+
if (widget.type === "tile") {
2300+
this._buildConditionsPanel(container, index, {
2301+
key: "invert_condition",
2302+
title: "Invert",
2303+
icon: "mdi:invert-colors",
2304+
hint:
2305+
"Render the tile inverted (dark card, light text) when" +
2306+
" the conditions below are met.",
2307+
});
2308+
}
2309+
22882310
this._buildVisibilityEditor(container, index);
22892311

22902312
return container;
@@ -2507,58 +2529,88 @@ class EinkDashboardEditor extends HTMLElement {
25072529
container.appendChild(section);
25082530
}
25092531

2510-
// ── Visibility conditions editor ────────────────────────────────
2532+
// ── Visibility / invert conditions editor ──────────────────────
25112533

25122534
/**
25132535
* Build a visibility conditions editor using HA's built-in
25142536
* `<ha-card-conditions-editor>` component. Appended to every
25152537
* widget form regardless of widget type.
25162538
*
2517-
* Reads the current widget's `visibility` array, passes it to the
2518-
* conditions editor, and writes changes back on `value-changed`.
2519-
* An empty conditions array is stored as `undefined` (field
2520-
* removed) to avoid serialising empty arrays.
2539+
* Thin wrapper around `_buildConditionsPanel` for the `visibility`
2540+
* field — see that method for the shared behaviour.
25212541
*
25222542
* @param container - Parent div to append the section to.
25232543
* @param index - Widget index in the widget array.
25242544
*/
25252545
private _buildVisibilityEditor(
25262546
container: HTMLElement,
25272547
index: number,
2548+
): void {
2549+
this._buildConditionsPanel(container, index, {
2550+
key: "visibility",
2551+
title: "Visibility",
2552+
icon: "mdi:eye",
2553+
hint: "Show this widget only when the conditions below are met.",
2554+
});
2555+
}
2556+
2557+
/**
2558+
* Build a conditions editor panel using HA's built-in
2559+
* `<ha-card-conditions-editor>` component, backed by an arbitrary
2560+
* widget field (e.g. `visibility` or `invert_condition`).
2561+
*
2562+
* Reads the current widget's condition array at `opts.key`, passes
2563+
* it to the conditions editor, and writes changes back on
2564+
* `value-changed`. An empty conditions array is stored as
2565+
* `undefined` (field removed) to avoid serialising empty arrays.
2566+
*
2567+
* @param container - Parent div to append the section to.
2568+
* @param index - Widget index in the widget array.
2569+
* @param opts - Field key plus panel title/icon/hint text.
2570+
*/
2571+
private _buildConditionsPanel(
2572+
container: HTMLElement,
2573+
index: number,
2574+
opts: {
2575+
key: "visibility" | "invert_condition";
2576+
title: string;
2577+
icon: string;
2578+
hint: string;
2579+
},
25282580
): void {
25292581
const panel = document.createElement("ha-expansion-panel");
25302582
(panel as unknown as Record<string, unknown>).outlined = true;
2531-
// ha-form sections have internal spacing; the visibility panel
2583+
// ha-form sections have internal spacing; the conditions panel
25322584
// is appended outside ha-form so it needs an explicit top gap.
25332585
panel.style.marginTop = "8px";
25342586

25352587
// Match ha-form-expandable: icon in "leading-icon" slot,
25362588
// title in "header" slot.
25372589
const icon = document.createElement("ha-icon");
25382590
icon.setAttribute("slot", "leading-icon");
2539-
(icon as unknown as Record<string, unknown>).icon = "mdi:eye";
2591+
(icon as unknown as Record<string, unknown>).icon = opts.icon;
25402592
panel.appendChild(icon);
25412593

25422594
const panelHeader = document.createElement("div");
25432595
panelHeader.setAttribute("slot", "header");
2544-
panelHeader.textContent = "Visibility";
2596+
panelHeader.textContent = opts.title;
25452597
panel.appendChild(panelHeader);
25462598

25472599
const hint = document.createElement("div");
25482600
hint.className = "visibility-hint";
2549-
hint.textContent =
2550-
"Show this widget only when the conditions below are met.";
2601+
hint.textContent = opts.hint;
25512602
panel.appendChild(hint);
25522603

25532604
if (!customElements.get("ha-card-conditions-editor")) {
25542605
console.warn(
25552606
"eink-dashboard: ha-card-conditions-editor not registered;" +
2556-
" visibility editing unavailable",
2607+
` ${opts.title.toLowerCase()} editing unavailable`,
25572608
);
25582609
const unavailable = document.createElement("div");
25592610
unavailable.className = "visibility-hint";
25602611
unavailable.textContent =
2561-
"Visibility conditions require a newer Home Assistant version.";
2612+
`${opts.title} conditions require a newer Home Assistant` +
2613+
" version.";
25622614
panel.appendChild(unavailable);
25632615
container.appendChild(panel);
25642616
return;
@@ -2570,7 +2622,7 @@ class EinkDashboardEditor extends HTMLElement {
25702622
const cur = this._widgets[index] as
25712623
unknown as Record<string, unknown>;
25722624
const conditions =
2573-
(cur.visibility as (Condition | LegacyCondition)[] | undefined)
2625+
(cur[opts.key] as (Condition | LegacyCondition)[] | undefined)
25742626
?? [];
25752627
(editor as unknown as Record<string, unknown>).hass = this._hass;
25762628
(editor as unknown as Record<string, unknown>).conditions =
@@ -2586,9 +2638,9 @@ class EinkDashboardEditor extends HTMLElement {
25862638
const w = this._widgets[index] as
25872639
unknown as Record<string, unknown>;
25882640
if (updated.length > 0) {
2589-
w.visibility = updated;
2641+
w[opts.key] = updated;
25902642
} else {
2591-
delete w.visibility;
2643+
delete w[opts.key];
25922644
}
25932645
// Write back so the Lit component re-renders immediately,
25942646
// showing newly added condition fields without a save+reopen.

custom_components/eink_dashboard/frontend/src/types/ha.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,13 @@ export interface TileWidget extends WidgetBase {
528528
icon_style?: IconStyle;
529529
/** When true, renders the secondary state line in bold. */
530530
bold_value?: boolean;
531+
/**
532+
* HA conditions that trigger inverted rendering (black card,
533+
* white text) as an e-ink attention signal. Same condition
534+
* format as `visibility`. The tile renders inverted when the
535+
* conditions are met.
536+
*/
537+
invert_condition?: (Condition | LegacyCondition)[];
531538
}
532539

533540
/**

0 commit comments

Comments
 (0)