Add numeric state condition for conditional card (#18288)

* Add numeric state condition for conditional card

* Add validate ui

* Clean entity data

* Check for numeric state
This commit is contained in:
Paul Bottein 2023-10-23 16:17:23 +02:00 committed by GitHub
parent 463a3244cf
commit fdddc18291
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 175 additions and 10 deletions

View File

@ -1,7 +1,13 @@
import { mdiAccount, mdiResponsive, mdiStateMachine } from "@mdi/js";
import {
mdiAccount,
mdiNumeric,
mdiResponsive,
mdiStateMachine,
} from "@mdi/js";
import { Condition } from "./validate-condition";
export const ICON_CONDITION: Record<Condition["condition"], string> = {
numeric_state: mdiNumeric,
state: mdiStateMachine,
screen: mdiResponsive,
user: mdiAccount,

View File

@ -2,7 +2,11 @@ import { ensureArray } from "../../../common/array/ensure-array";
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
export type Condition = StateCondition | ScreenCondition | UserCondition;
export type Condition =
| NumericStateCondition
| ScreenCondition
| StateCondition
| UserCondition;
export type LegacyCondition = {
entity?: string;
@ -10,6 +14,13 @@ export type LegacyCondition = {
state_not?: string | string[];
};
export type NumericStateCondition = {
condition: "numeric_state";
entity?: string;
below?: number;
above?: number;
};
export type StateCondition = {
condition: "state";
entity?: string;
@ -41,6 +52,29 @@ function checkStateCondition(
: ensureArray(condition.state_not).includes(state);
}
function checkStateNumericCondition(
condition: NumericStateCondition,
hass: HomeAssistant
) {
const entity =
(condition.entity ? hass.states[condition.entity] : undefined) ?? undefined;
if (!entity) {
return false;
}
const numericState = Number(entity.state);
if (isNaN(numericState)) {
return false;
}
return (
(condition.above == null || condition.above < numericState) &&
(condition.below == null || condition.below >= numericState)
);
}
function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
return condition.media_query
? matchMedia(condition.media_query).matches
@ -64,6 +98,8 @@ export function checkConditionsMet(
return checkScreenCondition(c, hass);
case "user":
return checkUserCondition(c, hass);
case "numeric_state":
return checkStateNumericCondition(c, hass);
default:
return checkStateCondition(c, hass);
}
@ -87,6 +123,13 @@ function validateUserCondition(condition: UserCondition) {
return condition.users != null;
}
function validateNumericStateCondition(condition: NumericStateCondition) {
return (
condition.entity != null &&
(condition.above != null || condition.below != null)
);
}
export function validateConditionalConfig(
conditions: (Condition | LegacyCondition)[]
): boolean {
@ -97,6 +140,8 @@ export function validateConditionalConfig(
return validateScreenCondition(c);
case "user":
return validateUserCondition(c);
case "numeric_state":
return validateNumericStateCondition(c);
default:
return validateStateCondition(c);
}

View File

@ -12,11 +12,13 @@ import { ICON_CONDITION } from "../../common/icon-condition";
import { Condition, LegacyCondition } from "../../common/validate-condition";
import "./ha-card-condition-editor";
import { LovelaceConditionEditorConstructor } from "./types";
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";
const UI_CONDITION = [
"numeric_state",
"state",
"screen",
"user",

View File

@ -0,0 +1,113 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, literal, number, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import {
NumericStateCondition,
StateCondition,
} from "../../../common/validate-condition";
const numericStateConditionStruct = object({
condition: literal("numeric_state"),
entity: optional(string()),
above: optional(number()),
below: optional(number()),
});
@customElement("ha-card-condition-numeric_state")
export class HaCardConditionNumericState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: NumericStateCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): NumericStateCondition {
return { condition: "numeric_state", entity: "" };
}
protected static validateUIConfig(condition: StateCondition) {
return assert(condition, numericStateConditionStruct);
}
private _schema = memoizeOne(
(stateObj?: HassEntity) =>
[
{ name: "entity", selector: { entity: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "above",
selector: {
number: {
mode: "box",
unit_of_measurement: stateObj?.attributes.unit_of_measurement,
},
},
},
{
name: "below",
selector: {
number: {
mode: "box",
unit_of_measurement: stateObj?.attributes.unit_of_measurement,
},
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const stateObj = this.condition.entity
? this.hass.states[this.condition.entity]
: undefined;
return html`
<ha-form
.hass=${this.hass}
.data=${this.condition}
.schema=${this._schema(stateObj)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const condition = ev.detail.value as NumericStateCondition;
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "entity":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "below":
return "Below";
case "above":
return "Above";
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-numeric_state": HaCardConditionNumericState;
}
}

View File

@ -19,7 +19,7 @@ const stateConditionStruct = object({
type StateConditionData = {
condition: "state";
entity: string;
entity?: string;
invert: "true" | "false";
state?: string | string[];
};
@ -101,12 +101,9 @@ export class HaCardConditionState extends LitElement {
const data: StateConditionData = {
...content,
entity: this.condition.entity ?? "",
entity: this.condition.entity,
invert: this.condition.state_not ? "true" : "false",
state:
(this.condition.state_not as string | string[] | undefined) ??
(this.condition.state as string | string[] | undefined) ??
"",
state: this.condition.state_not ?? this.condition.state,
};
return html`
@ -125,12 +122,11 @@ export class HaCardConditionState extends LitElement {
ev.stopPropagation();
const data = ev.detail.value as StateConditionData;
const { invert, state, entity, condition: _, ...content } = data;
const { invert, state, condition: _, ...content } = data;
const condition: StateCondition = {
condition: "state",
...content,
entity: entity ?? "",
state: invert === "false" ? state ?? "" : undefined,
state_not: invert === "true" ? state ?? "" : undefined,
};

View File

@ -4758,6 +4758,9 @@
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
"add": "Add condition",
"condition": {
"numeric_state": {
"label": "Entity numeric state"
},
"screen": {
"label": "Screen",
"breakpoints": "Screen sizes",