Skip to content

Commit 98525d2

Browse files
authored
Lovelace condition live test (#52027)
* Add lovelace condition live test * Add live card status * Add empty text
1 parent ec98b21 commit 98525d2

10 files changed

Lines changed: 482 additions & 163 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type {
2+
ReactiveController,
3+
ReactiveControllerHost,
4+
} from "@lit/reactive-element/reactive-controller";
5+
import type {
6+
Condition,
7+
ConditionContext,
8+
} from "../../panels/lovelace/common/validate-condition";
9+
import type { HomeAssistant } from "../../types";
10+
import { setupConditionListeners } from "../condition/listeners";
11+
12+
/**
13+
* Reactive controller that manages the media-query and time-based listeners
14+
* needed to keep a set of lovelace visibility conditions evaluated live.
15+
*
16+
* The host is responsible for the actual evaluation (e.g. computing visible /
17+
* hidden / invalid state); the controller only triggers it via the supplied
18+
* `onUpdate` callback when something the conditions depend on changes. Call
19+
* `setup()` whenever the conditions change; the controller clears previous
20+
* listeners and re-subscribes. Listeners are automatically released when the
21+
* host disconnects.
22+
*/
23+
export class ConditionListenersController implements ReactiveController {
24+
private _unsubs: (() => void)[] = [];
25+
26+
constructor(host: ReactiveControllerHost) {
27+
host.addController(this);
28+
}
29+
30+
public hostDisconnected(): void {
31+
this.clear();
32+
}
33+
34+
public setup(
35+
conditions: Condition[],
36+
hass: HomeAssistant,
37+
onUpdate: () => void,
38+
getContext?: () => ConditionContext
39+
): void {
40+
this.clear();
41+
if (!conditions.length) {
42+
return;
43+
}
44+
setupConditionListeners(
45+
conditions,
46+
hass,
47+
(unsub) => this._unsubs.push(unsub),
48+
() => onUpdate(),
49+
getContext
50+
);
51+
}
52+
53+
public clear(): void {
54+
for (const unsub of this._unsubs) {
55+
unsub();
56+
}
57+
this._unsubs = [];
58+
}
59+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { LitElement, css, html, nothing } from "lit";
2+
import { customElement, property } from "lit/decorators";
3+
import "../ha-tooltip";
4+
5+
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
6+
7+
/**
8+
* @element ha-automation-row-live-test
9+
*
10+
* @summary
11+
* Small status indicator dot used in automation/condition rows to surface the
12+
* live evaluation result. Renders an optional tooltip with details on hover.
13+
*
14+
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
15+
* @attr {string} label - Accessible label announced by assistive technology.
16+
* @attr {string} message - Optional tooltip body shown on hover/focus.
17+
*/
18+
@customElement("ha-automation-row-live-test")
19+
export class HaAutomationRowLiveTest extends LitElement {
20+
@property({ reflect: true }) public state: LiveTestState = "unknown";
21+
22+
@property() public label = "";
23+
24+
@property() public message?: string;
25+
26+
protected render() {
27+
return html`
28+
<div
29+
id="indicator"
30+
role="status"
31+
tabindex="0"
32+
aria-label=${this.label}
33+
></div>
34+
${this.message
35+
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
36+
: nothing}
37+
`;
38+
}
39+
40+
static styles = css`
41+
:host {
42+
position: absolute;
43+
inset-inline-end: -6px;
44+
display: inline-block;
45+
}
46+
#indicator {
47+
width: 12px;
48+
height: 12px;
49+
border-radius: var(--ha-border-radius-circle);
50+
border: 3px solid;
51+
box-sizing: border-box;
52+
background-color: var(--card-background-color);
53+
transition: all var(--ha-animation-duration-normal) ease-in-out;
54+
}
55+
:host([state="pass"]) #indicator {
56+
background-color: var(--ha-color-fill-success-loud-resting);
57+
border-color: var(--ha-color-fill-success-loud-resting);
58+
}
59+
:host([state="pass"]) #indicator:hover {
60+
background-color: var(--ha-color-fill-success-loud-hover);
61+
border-color: var(--ha-color-fill-success-loud-hover);
62+
}
63+
:host([state="fail"]) #indicator {
64+
border-color: var(--ha-color-fill-warning-loud-resting);
65+
}
66+
:host([state="fail"]) #indicator:hover {
67+
background-color: var(--ha-color-fill-warning-loud-hover);
68+
border-color: var(--ha-color-fill-warning-loud-hover);
69+
}
70+
:host([state="invalid"]) #indicator {
71+
border-color: var(--ha-color-fill-danger-loud-resting);
72+
}
73+
:host([state="invalid"]) #indicator:hover {
74+
background-color: var(--ha-color-fill-danger-loud-hover);
75+
border-color: var(--ha-color-fill-danger-loud-hover);
76+
}
77+
:host([state="unknown"]) #indicator {
78+
border-color: var(--ha-color-fill-neutral-loud-resting);
79+
}
80+
:host([state="unknown"]) #indicator:hover {
81+
background-color: var(--ha-color-fill-neutral-loud-hover);
82+
border-color: var(--ha-color-fill-neutral-loud-hover);
83+
}
84+
`;
85+
}
86+
87+
declare global {
88+
interface HTMLElementTagNameMap {
89+
"ha-automation-row-live-test": HaAutomationRowLiveTest;
90+
}
91+
}

src/panels/config/automation/condition/ha-automation-condition-row.ts

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type {
2323
} from "home-assistant-js-websocket";
2424
import { dump } from "js-yaml";
2525
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
26-
import { LitElement, css, html, nothing } from "lit";
26+
import { LitElement, html, nothing } from "lit";
2727
import { customElement, property, query, state } from "lit/decorators";
2828
import { classMap } from "lit/directives/class-map";
2929
import memoizeOne from "memoize-one";
@@ -39,14 +39,14 @@ import { debounce } from "../../../../common/util/debounce";
3939
import "../../../../components/automation/ha-automation-row";
4040
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
4141
import "../../../../components/automation/ha-automation-row-event-chip";
42+
import "../../../../components/automation/ha-automation-row-live-test";
4243
import "../../../../components/ha-card";
4344
import "../../../../components/ha-condition-icon";
4445
import "../../../../components/ha-dropdown";
4546
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
4647
import "../../../../components/ha-dropdown-item";
4748
import "../../../../components/ha-expansion-panel";
4849
import "../../../../components/ha-icon-button";
49-
import "../../../../components/ha-tooltip";
5050
import type {
5151
AutomationClipboard,
5252
Condition,
@@ -498,23 +498,15 @@ export default class HaAutomationConditionRow extends LitElement {
498498
@click=${this._toggleSidebar}
499499
@toggle-collapsed=${this._toggleCollapse}
500500
>${this._renderRow()}
501-
<div
501+
<ha-automation-row-live-test
502502
slot="icons"
503-
id="live-test"
504-
class=${this._liveTestResult.state}
505-
role="status"
506-
tabindex="0"
507-
aria-label=${this.hass.localize(
503+
.state=${this._liveTestResult.state}
504+
.label=${this.hass.localize(
508505
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
509506
)}
510-
>
511-
${this._liveTestResult.message
512-
? html`<ha-tooltip for="live-test">
513-
${this._liveTestResult.message}
514-
</ha-tooltip>`
515-
: nothing}
516-
</div></ha-automation-row
517-
>`
507+
.message=${this._liveTestResult.message}
508+
></ha-automation-row-live-test
509+
></ha-automation-row>`
518510
: html`
519511
<ha-expansion-panel
520512
left-chevron
@@ -1064,52 +1056,7 @@ export default class HaAutomationConditionRow extends LitElement {
10641056
}
10651057

10661058
static get styles(): CSSResultGroup {
1067-
return [
1068-
rowStyles,
1069-
overflowStyles,
1070-
css`
1071-
#live-test {
1072-
position: absolute;
1073-
inset-inline-end: -6px;
1074-
width: 12px;
1075-
height: 12px;
1076-
border-radius: var(--ha-border-radius-circle);
1077-
border: 3px solid;
1078-
box-sizing: border-box;
1079-
background-color: var(--card-background-color);
1080-
transition: all var(--ha-animation-duration-normal) ease-in-out;
1081-
}
1082-
#live-test.pass {
1083-
background-color: var(--ha-color-fill-success-loud-resting);
1084-
border-color: var(--ha-color-fill-success-loud-resting);
1085-
}
1086-
#live-test.pass:hover {
1087-
background-color: var(--ha-color-fill-success-loud-hover);
1088-
border-color: var(--ha-color-fill-success-loud-hover);
1089-
}
1090-
#live-test.fail {
1091-
border-color: var(--ha-color-fill-warning-loud-resting);
1092-
}
1093-
#live-test.fail:hover {
1094-
background-color: var(--ha-color-fill-warning-loud-hover);
1095-
border-color: var(--ha-color-fill-warning-loud-hover);
1096-
}
1097-
#live-test.invalid {
1098-
border-color: var(--ha-color-fill-danger-loud-resting);
1099-
}
1100-
#live-test.invalid:hover {
1101-
background-color: var(--ha-color-fill-danger-loud-hover);
1102-
border-color: var(--ha-color-fill-danger-loud-hover);
1103-
}
1104-
#live-test.unknown {
1105-
border-color: var(--ha-color-fill-neutral-loud-resting);
1106-
}
1107-
#live-test.unknown:hover {
1108-
background-color: var(--ha-color-fill-neutral-loud-hover);
1109-
border-color: var(--ha-color-fill-neutral-loud-hover);
1110-
}
1111-
`,
1112-
];
1059+
return [rowStyles, overflowStyles];
11131060
}
11141061
}
11151062

src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
33
import { LitElement, css, html } from "lit";
44
import { customElement, property } from "lit/decorators";
55
import { fireEvent } from "../../../../common/dom/fire_event";
6-
import "../../../../components/ha-alert";
76
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
87
import type { HomeAssistant } from "../../../../types";
98
import type { Condition } from "../../common/validate-condition";
109
import { conditionsEntityContext } from "../conditions/context";
1110
import "../conditions/ha-card-conditions-editor";
11+
import "../conditions/ha-visibility-status";
1212

1313
@customElement("hui-badge-visibility-editor")
1414
export class HuiBadgeVisibilityEditor extends LitElement {
@@ -34,11 +34,10 @@ export class HuiBadgeVisibilityEditor extends LitElement {
3434
render() {
3535
const conditions = this.config.visibility ?? [];
3636
return html`
37-
<p class="intro">
38-
${this.hass.localize(
39-
`ui.panel.lovelace.editor.edit_badge.visibility.explanation`
40-
)}
41-
</p>
37+
<ha-visibility-status
38+
.hass=${this.hass}
39+
.conditions=${conditions}
40+
></ha-visibility-status>
4241
<ha-card-conditions-editor
4342
.hass=${this.hass}
4443
.conditions=${conditions}
@@ -62,10 +61,8 @@ export class HuiBadgeVisibilityEditor extends LitElement {
6261
}
6362

6463
static styles = css`
65-
.intro {
66-
margin: 0;
67-
color: var(--secondary-text-color);
68-
margin-bottom: 8px;
64+
ha-visibility-status {
65+
margin-bottom: var(--ha-space-3);
6966
}
7067
`;
7168
}

src/panels/lovelace/editor/card-editor/hui-card-visibility-editor.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
33
import { LitElement, css, html } from "lit";
44
import { customElement, property } from "lit/decorators";
55
import { fireEvent } from "../../../../common/dom/fire_event";
6-
import "../../../../components/ha-alert";
76
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
87
import type { HomeAssistant } from "../../../../types";
98
import type { Condition } from "../../common/validate-condition";
109
import { conditionsEntityContext } from "../conditions/context";
1110
import "../conditions/ha-card-conditions-editor";
11+
import "../conditions/ha-visibility-status";
1212

1313
@customElement("hui-card-visibility-editor")
1414
export class HuiCardVisibilityEditor extends LitElement {
@@ -34,11 +34,10 @@ export class HuiCardVisibilityEditor extends LitElement {
3434
render() {
3535
const conditions = this.config.visibility ?? [];
3636
return html`
37-
<p class="intro">
38-
${this.hass.localize(
39-
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
40-
)}
41-
</p>
37+
<ha-visibility-status
38+
.hass=${this.hass}
39+
.conditions=${conditions}
40+
></ha-visibility-status>
4241
<ha-card-conditions-editor
4342
.hass=${this.hass}
4443
.conditions=${conditions}
@@ -62,10 +61,8 @@ export class HuiCardVisibilityEditor extends LitElement {
6261
}
6362

6463
static styles = css`
65-
.intro {
66-
margin: 0;
67-
color: var(--secondary-text-color);
68-
margin-bottom: 8px;
64+
ha-visibility-status {
65+
margin-bottom: var(--ha-space-3);
6966
}
7067
`;
7168
}

0 commit comments

Comments
 (0)