Skip to content

Commit

Permalink
Add and and or condition to conditional card (#18409)
Browse files Browse the repository at this point in the history
  • Loading branch information
piitaya authored Nov 1, 2023
1 parent acb5a2b commit 463cfb8
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 19 deletions.
4 changes: 4 additions & 0 deletions src/panels/lovelace/common/icon-condition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
mdiAccount,
mdiAmpersand,
mdiGateOr,
mdiNumeric,
mdiResponsive,
mdiStateMachine,
Expand All @@ -11,4 +13,6 @@ export const ICON_CONDITION: Record<Condition["condition"], string> = {
state: mdiStateMachine,
screen: mdiResponsive,
user: mdiAccount,
and: mdiAmpersand,
or: mdiGateOr,
};
40 changes: 39 additions & 1 deletion src/panels/lovelace/common/validate-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export type Condition =
| NumericStateCondition
| ScreenCondition
| StateCondition
| UserCondition;
| UserCondition
| OrCondition
| AndCondition;

export type LegacyCondition = {
entity?: string;
Expand Down Expand Up @@ -38,6 +40,16 @@ export type UserCondition = {
users?: string[];
};

export type OrCondition = {
condition: "or";
conditions?: Condition[];
};

export type AndCondition = {
condition: "and";
conditions?: Condition[];
};

function checkStateCondition(
condition: StateCondition | LegacyCondition,
hass: HomeAssistant
Expand Down Expand Up @@ -87,6 +99,16 @@ function checkUserCondition(condition: UserCondition, hass: HomeAssistant) {
: 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(
conditions: (Condition | LegacyCondition)[],
hass: HomeAssistant
Expand All @@ -100,6 +122,10 @@ export function checkConditionsMet(
return checkUserCondition(c, hass);
case "numeric_state":
return checkStateNumericCondition(c, hass);
case "and":
return checkAndCondition(c, hass);
case "or":
return checkOrCondition(c, hass);
default:
return checkStateCondition(c, hass);
}
Expand All @@ -123,6 +149,14 @@ function validateUserCondition(condition: UserCondition) {
return condition.users != null;
}

function validateAndCondition(condition: AndCondition) {
return condition.conditions != null;
}

function validateOrCondition(condition: OrCondition) {
return condition.conditions != null;
}

function validateNumericStateCondition(condition: NumericStateCondition) {
return (
condition.entity != null &&
Expand All @@ -142,6 +176,10 @@ export function validateConditionalConfig(
return validateUserCondition(c);
case "numeric_state":
return validateNumericStateCondition(c);
case "and":
return validateAndCondition(c);
case "or":
return validateOrCondition(c);
default:
return validateStateCondition(c);
}
Expand Down
39 changes: 27 additions & 12 deletions src/panels/lovelace/components/hui-conditional-base.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import { PropertyValues, ReactiveElement } from "lit";
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 { ConditionalCardConfig } from "../cards/types";
import {
ScreenCondition,
Condition,
LegacyCondition,
checkConditionsMet,
validateConditionalConfig,
} from "../common/validate-condition";
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/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")
export class HuiConditionalBase extends ReactiveElement {
Expand Down Expand Up @@ -77,24 +92,23 @@ export class HuiConditionalBase extends ReactiveElement {
return;
}

const conditions = this._config.conditions.filter(
(c) => "condition" in c && c.condition === "screen"
) as ScreenCondition[];

const mediaQueries = conditions
.filter((c) => c.media_query)
.map((c) => c.media_query as string);
const mediaQueries = extractMediaQueries(this._config.conditions);

if (deepEqual(mediaQueries, this._mediaQueries)) return;

this._mediaQueries = mediaQueries;
while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!();
}

mediaQueries.forEach((query) => {
const listener = listenMediaQuery(query, (matches) => {
// For performance, if there is only one condition, set the visibility directly
if (this._config!.conditions.length === 1) {
// For performance, if there is only one condition and it's a screen condition, set the visibility directly
if (
this._config!.conditions.length === 1 &&
"condition" in this._config!.conditions[0] &&
this._config!.conditions[0].condition === "screen"
) {
this._setVisibility(matches);
return;
}
Expand Down Expand Up @@ -128,6 +142,7 @@ export class HuiConditionalBase extends ReactiveElement {
this._config!.conditions,
this.hass!
);

this._setVisibility(conditionMet);
}

Expand Down
29 changes: 23 additions & 6 deletions src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
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 { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
Expand All @@ -18,12 +25,16 @@ import "./types/ha-card-condition-numeric_state";
import "./types/ha-card-condition-screen";
import "./types/ha-card-condition-state";
import "./types/ha-card-condition-user";
import "./types/ha-card-condition-or";
import "./types/ha-card-condition-and";

const UI_CONDITION = [
"numeric_state",
"state",
"screen",
"user",
"and",
"or",
] as const satisfies readonly Condition["condition"][];

@customElement("ha-card-conditions-editor")
Expand All @@ -35,6 +46,8 @@ export class HaCardConditionsEditor extends LitElement {
| LegacyCondition
)[];

@property({ attribute: true, type: Boolean }) public nested?: boolean;

private _focusLastConditionOnChange = false;

protected firstUpdated() {
Expand Down Expand Up @@ -70,11 +83,15 @@ export class HaCardConditionsEditor extends LitElement {
protected render() {
return html`
<div class="conditions">
<ha-alert alert-type="info">
${this.hass!.localize(
"ui.panel.lovelace.editor.condition-editor.explanation"
)}
</ha-alert>
${!this.nested
? html`
<ha-alert alert-type="info">
${this.hass!.localize(
"ui.panel.lovelace.editor.condition-editor.explanation"
)}
</ha-alert>
`
: nothing}
${this.conditions.map(
(cond, idx) => html`
<ha-card-condition-editor
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4844,6 +4844,12 @@
},
"user": {
"label": "User"
},
"or": {
"label": "Or"
},
"and": {
"label": "And"
}
}
},
Expand Down

0 comments on commit 463cfb8

Please sign in to comment.