Skip to content

Commit

Permalink
Add numeric state condition for conditional card
Browse files Browse the repository at this point in the history
  • Loading branch information
piitaya committed Oct 19, 2023
1 parent 49f88a9 commit ffc14df
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 13 deletions.
3 changes: 2 additions & 1 deletion src/panels/lovelace/common/icon-condition.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { mdiResponsive, mdiStateMachine } from "@mdi/js";
import { mdiNumeric, mdiResponsive, mdiStateMachine } from "@mdi/js";
import { Condition } from "./validate-condition";

export const ICON_CONDITION: Record<Condition["condition"], string> = {
state: mdiStateMachine,
screen: mdiResponsive,
numeric_state: mdiNumeric,
};
60 changes: 48 additions & 12 deletions src/panels/lovelace/common/validate-condition.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";

export type Condition = StateCondition | ScreenCondition;
export type Condition =
| StateCondition
| ScreenCondition
| NumericStateCondition;

export type LegacyCondition = {
entity?: string;
state?: string;
state_not?: string;
};

export type NumericStateCondition = {
condition: "numeric_state";
entity?: string;
below?: number;
above?: number;
};

export type StateCondition = {
condition: "state";
entity?: string;
Expand All @@ -32,10 +42,25 @@ function checkStateCondition(condition: StateCondition, hass: HomeAssistant) {
: state !== condition.state_not;
}

function checkScreenCondition(
condition: ScreenCondition,
_hass: HomeAssistant
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);
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
: false;
Expand All @@ -46,15 +71,18 @@ export function checkConditionsMet(
hass: HomeAssistant
): boolean {
return conditions.every((c) => {
if (c.condition === "screen") {
return checkScreenCondition(c, hass);
switch (c.condition) {
case "screen":
return checkScreenCondition(c, hass);
case "numeric_state":
return checkStateNumericCondition(c, hass);
default:
return checkStateCondition(c, hass);
}

return checkStateCondition(c, hass);
});
}

function valideStateCondition(condition: StateCondition) {
function validateStateCondition(condition: StateCondition) {
return (
condition.entity != null &&
(condition.state != null || condition.state_not != null)
Expand All @@ -65,11 +93,19 @@ function validateScreenCondition(condition: ScreenCondition) {
return condition.media_query != null;
}

function validateNumericStateCondition(condition: NumericStateCondition) {
return condition.entity != null;
}

export function validateConditionalConfig(conditions: Condition[]): boolean {
return conditions.every((c) => {
if (c.condition === "screen") {
return validateScreenCondition(c);
switch (c.condition) {
case "screen":
return validateScreenCondition(c);
case "numeric_state":
return validateNumericStateCondition(c);
default:
return validateStateCondition(c);
}
return valideStateCondition(c);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } 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 } from "../../../common/validate-condition";

const numericStateConditionStruct = object({
condition: literal("state"),
entity: 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 willUpdate(changedProperties: PropertyValues): void {
if (!changedProperties.has("condition")) {
return;
}
try {
assert(this.condition, numericStateConditionStruct);
} catch (err: any) {
fireEvent(this, "ui-mode-not-available", err);
}
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import "../conditions/ha-card-condition-editor";
import { LovelaceConditionEditorConstructor } from "../conditions/types";
import "../conditions/types/ha-card-condition-screen";
import "../conditions/types/ha-card-condition-state";
import "../conditions/types/ha-card-condition-numeric_state";
import "../hui-element-editor";
import type { ConfigChangedEvent } from "../hui-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
Expand All @@ -43,6 +44,7 @@ import { configElementStyle } from "./config-elements-style";

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

Expand Down
3 changes: 3 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4801,6 +4801,9 @@
},
"state": {
"label": "Entity state"
},
"numeric_state": {
"label": "Entity numeric state"
}
}
},
Expand Down

0 comments on commit ffc14df

Please sign in to comment.