Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add and and or condition to conditional card #18409

Merged
merged 3 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -4835,6 +4835,12 @@
},
"user": {
"label": "User"
},
"or": {
"label": "Or"
},
"and": {
"label": "And"
}
}
},
Expand Down
Loading