Add and and or condition to conditional card (#18409)

This commit is contained in:
Paul Bottein 2023-11-01 15:28:37 +01:00 committed by GitHub
parent acb5a2b283
commit 463cfb869f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 221 additions and 19 deletions

View File

@ -1,5 +1,7 @@
import { import {
mdiAccount, mdiAccount,
mdiAmpersand,
mdiGateOr,
mdiNumeric, mdiNumeric,
mdiResponsive, mdiResponsive,
mdiStateMachine, mdiStateMachine,
@ -11,4 +13,6 @@ export const ICON_CONDITION: Record<Condition["condition"], string> = {
state: mdiStateMachine, state: mdiStateMachine,
screen: mdiResponsive, screen: mdiResponsive,
user: mdiAccount, user: mdiAccount,
and: mdiAmpersand,
or: mdiGateOr,
}; };

View File

@ -6,7 +6,9 @@ export type Condition =
| NumericStateCondition | NumericStateCondition
| ScreenCondition | ScreenCondition
| StateCondition | StateCondition
| UserCondition; | UserCondition
| OrCondition
| AndCondition;
export type LegacyCondition = { export type LegacyCondition = {
entity?: string; entity?: string;
@ -38,6 +40,16 @@ export type UserCondition = {
users?: string[]; users?: string[];
}; };
export type OrCondition = {
condition: "or";
conditions?: Condition[];
};
export type AndCondition = {
condition: "and";
conditions?: Condition[];
};
function checkStateCondition( function checkStateCondition(
condition: StateCondition | LegacyCondition, condition: StateCondition | LegacyCondition,
hass: HomeAssistant hass: HomeAssistant
@ -87,6 +99,16 @@ function checkUserCondition(condition: UserCondition, hass: HomeAssistant) {
: false; : false;
} }
function checkAndCondition(condition: AndCondition, hass: HomeAssistant) {
if (!condition.conditions) return true;
return checkConditionsMet(condition.conditions, hass);
}
function checkOrCondition(condition: OrCondition, hass: HomeAssistant) {
if (!condition.conditions) return true;
return condition.conditions.some((c) => checkConditionsMet([c], hass));
}
export function checkConditionsMet( export function checkConditionsMet(
conditions: (Condition | LegacyCondition)[], conditions: (Condition | LegacyCondition)[],
hass: HomeAssistant hass: HomeAssistant
@ -100,6 +122,10 @@ export function checkConditionsMet(
return checkUserCondition(c, hass); return checkUserCondition(c, hass);
case "numeric_state": case "numeric_state":
return checkStateNumericCondition(c, hass); return checkStateNumericCondition(c, hass);
case "and":
return checkAndCondition(c, hass);
case "or":
return checkOrCondition(c, hass);
default: default:
return checkStateCondition(c, hass); return checkStateCondition(c, hass);
} }
@ -123,6 +149,14 @@ function validateUserCondition(condition: UserCondition) {
return condition.users != null; return condition.users != null;
} }
function validateAndCondition(condition: AndCondition) {
return condition.conditions != null;
}
function validateOrCondition(condition: OrCondition) {
return condition.conditions != null;
}
function validateNumericStateCondition(condition: NumericStateCondition) { function validateNumericStateCondition(condition: NumericStateCondition) {
return ( return (
condition.entity != null && condition.entity != null &&
@ -142,6 +176,10 @@ export function validateConditionalConfig(
return validateUserCondition(c); return validateUserCondition(c);
case "numeric_state": case "numeric_state":
return validateNumericStateCondition(c); return validateNumericStateCondition(c);
case "and":
return validateAndCondition(c);
case "or":
return validateOrCondition(c);
default: default:
return validateStateCondition(c); return validateStateCondition(c);
} }

View File

@ -1,16 +1,31 @@
import { PropertyValues, ReactiveElement } from "lit"; import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { deepEqual } from "../../../common/util/deep-equal";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { ConditionalCardConfig } from "../cards/types"; import { ConditionalCardConfig } from "../cards/types";
import { import {
ScreenCondition, Condition,
LegacyCondition,
checkConditionsMet, checkConditionsMet,
validateConditionalConfig, validateConditionalConfig,
} from "../common/validate-condition"; } from "../common/validate-condition";
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
import { LovelaceCard } from "../types"; import { LovelaceCard } from "../types";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { deepEqual } from "../../../common/util/deep-equal"; function extractMediaQueries(
conditions: (Condition | LegacyCondition)[]
): string[] {
return conditions.reduce<string[]>((array, c) => {
if ("conditions" in c && c.conditions) {
array.push(...extractMediaQueries(c.conditions));
}
if ("condition" in c && c.condition === "screen" && c.media_query) {
array.push(c.media_query);
}
return array;
}, []);
}
@customElement("hui-conditional-base") @customElement("hui-conditional-base")
export class HuiConditionalBase extends ReactiveElement { export class HuiConditionalBase extends ReactiveElement {
@ -77,13 +92,7 @@ export class HuiConditionalBase extends ReactiveElement {
return; return;
} }
const conditions = this._config.conditions.filter( const mediaQueries = extractMediaQueries(this._config.conditions);
(c) => "condition" in c && c.condition === "screen"
) as ScreenCondition[];
const mediaQueries = conditions
.filter((c) => c.media_query)
.map((c) => c.media_query as string);
if (deepEqual(mediaQueries, this._mediaQueries)) return; if (deepEqual(mediaQueries, this._mediaQueries)) return;
@ -91,10 +100,15 @@ export class HuiConditionalBase extends ReactiveElement {
while (this._mediaQueriesListeners.length) { while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!(); this._mediaQueriesListeners.pop()!();
} }
mediaQueries.forEach((query) => { mediaQueries.forEach((query) => {
const listener = listenMediaQuery(query, (matches) => { const listener = listenMediaQuery(query, (matches) => {
// For performance, if there is only one condition, set the visibility directly // For performance, if there is only one condition and it's a screen condition, set the visibility directly
if (this._config!.conditions.length === 1) { if (
this._config!.conditions.length === 1 &&
"condition" in this._config!.conditions[0] &&
this._config!.conditions[0].condition === "screen"
) {
this._setVisibility(matches); this._setVisibility(matches);
return; return;
} }
@ -128,6 +142,7 @@ export class HuiConditionalBase extends ReactiveElement {
this._config!.conditions, this._config!.conditions,
this.hass! this.hass!
); );
this._setVisibility(conditionMet); this._setVisibility(conditionMet);
} }

View File

@ -1,5 +1,12 @@
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
@ -18,12 +25,16 @@ import "./types/ha-card-condition-numeric_state";
import "./types/ha-card-condition-screen"; import "./types/ha-card-condition-screen";
import "./types/ha-card-condition-state"; import "./types/ha-card-condition-state";
import "./types/ha-card-condition-user"; import "./types/ha-card-condition-user";
import "./types/ha-card-condition-or";
import "./types/ha-card-condition-and";
const UI_CONDITION = [ const UI_CONDITION = [
"numeric_state", "numeric_state",
"state", "state",
"screen", "screen",
"user", "user",
"and",
"or",
] as const satisfies readonly Condition["condition"][]; ] as const satisfies readonly Condition["condition"][];
@customElement("ha-card-conditions-editor") @customElement("ha-card-conditions-editor")
@ -35,6 +46,8 @@ export class HaCardConditionsEditor extends LitElement {
| LegacyCondition | LegacyCondition
)[]; )[];
@property({ attribute: true, type: Boolean }) public nested?: boolean;
private _focusLastConditionOnChange = false; private _focusLastConditionOnChange = false;
protected firstUpdated() { protected firstUpdated() {
@ -70,11 +83,15 @@ export class HaCardConditionsEditor extends LitElement {
protected render() { protected render() {
return html` return html`
<div class="conditions"> <div class="conditions">
<ha-alert alert-type="info"> ${!this.nested
${this.hass!.localize( ? html`
"ui.panel.lovelace.editor.condition-editor.explanation" <ha-alert alert-type="info">
)} ${this.hass!.localize(
</ha-alert> "ui.panel.lovelace.editor.condition-editor.explanation"
)}
</ha-alert>
`
: nothing}
${this.conditions.map( ${this.conditions.map(
(cond, idx) => html` (cond, idx) => html`
<ha-card-condition-editor <ha-card-condition-editor

View File

@ -0,0 +1,61 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { any, array, assert, literal, object, optional } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { HomeAssistant } from "../../../../../types";
import {
AndCondition,
Condition,
StateCondition,
} from "../../../common/validate-condition";
const andConditionStruct = object({
condition: literal("and"),
conditions: optional(array(any())),
});
@customElement("ha-card-condition-and")
export class HaCardConditionNumericAnd extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: AndCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): AndCondition {
return { condition: "and", conditions: [] };
}
protected static validateUIConfig(condition: StateCondition) {
return assert(condition, andConditionStruct);
}
protected render() {
return html`
<ha-card-conditions-editor
nested
.hass=${this.hass}
.conditions=${this.condition.conditions}
@value-changed=${this._valueChanged}
>
</ha-card-conditions-editor>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const conditions = ev.detail.value as Condition[];
const condition = {
...this.condition,
conditions,
};
fireEvent(this, "value-changed", { value: condition });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-and": HaCardConditionNumericAnd;
}
}

View File

@ -0,0 +1,61 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { any, array, assert, literal, object, optional } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { HomeAssistant } from "../../../../../types";
import {
Condition,
OrCondition,
StateCondition,
} from "../../../common/validate-condition";
const orConditionStruct = object({
condition: literal("or"),
conditions: optional(array(any())),
});
@customElement("ha-card-condition-or")
export class HaCardConditionOr extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: OrCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): OrCondition {
return { condition: "or", conditions: [] };
}
protected static validateUIConfig(condition: StateCondition) {
return assert(condition, orConditionStruct);
}
protected render() {
return html`
<ha-card-conditions-editor
nested
.hass=${this.hass}
.conditions=${this.condition.conditions}
@value-changed=${this._valueChanged}
>
</ha-card-conditions-editor>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const conditions = ev.detail.value as Condition[];
const condition = {
...this.condition,
conditions,
};
fireEvent(this, "value-changed", { value: condition });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-or": HaCardConditionOr;
}
}

View File

@ -4844,6 +4844,12 @@
}, },
"user": { "user": {
"label": "User" "label": "User"
},
"or": {
"label": "Or"
},
"and": {
"label": "And"
} }
} }
}, },