diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index 4d8ad42b73..54d8f709da 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -136,6 +136,11 @@ const SCHEMAS: { schema: [ { name: "addon", selector: { addon: {} } }, { name: "entity", selector: { entity: {} } }, + { + name: "State", + selector: { state: { entity_id: "" } }, + context: { filter_entity: "entity" }, + }, { name: "Attribute", selector: { attribute: { entity_id: "" } }, diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 6ce04e0995..6549c8e30e 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -115,6 +115,10 @@ const SCHEMAS: { name: "One of each", input: { entity: { name: "Entity", selector: { entity: {} } }, + state: { + name: "State", + selector: { state: { entity_id: "alarm_control_panel.alarm" } }, + }, attribute: { name: "Attribute", selector: { attribute: { entity_id: "" } }, diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts new file mode 100644 index 0000000000..c40ad804d0 --- /dev/null +++ b/src/common/entity/get_states.ts @@ -0,0 +1,89 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { computeStateDomain } from "./compute_state_domain"; +import { UNAVAILABLE_STATES } from "../../data/entity"; + +const FIXED_DOMAIN_STATES = { + alarm_control_panel: [ + "armed_away", + "armed_custom_bypass", + "armed_home", + "armed_night", + "armed_vacation", + "arming", + "disarmed", + "disarming", + "pending", + "triggered", + ], + automation: ["on", "off"], + binary_sensor: ["on", "off"], + button: [], + calendar: ["on", "off"], + camera: ["idle", "recording", "streaming"], + cover: ["closed", "closing", "open", "opening"], + device_tracker: ["home", "not_home"], + fan: ["on", "off"], + humidifier: ["on", "off"], + input_boolean: ["on", "off"], + input_button: [], + light: ["on", "off"], + lock: ["jammed", "locked", "locking", "unlocked", "unlocking"], + media_player: ["idle", "off", "paused", "playing", "standby"], + person: ["home", "not_home"], + remote: ["on", "off"], + scene: [], + schedule: ["on", "off"], + script: ["on", "off"], + siren: ["on", "off"], + sun: ["above_horizon", "below_horizon"], + switch: ["on", "off"], + update: ["on", "off"], + vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"], + weather: [ + "clear-night", + "cloudy", + "exceptional", + "fog", + "hail", + "lightning-rainy", + "lightning", + "partlycloudy", + "pouring", + "rainy", + "snowy-rainy", + "snowy", + "sunny", + "windy-variant", + "windy", + ], +}; + +export const getStates = (state: HassEntity): string[] => { + const domain = computeStateDomain(state); + const result: string[] = []; + + if (domain in FIXED_DOMAIN_STATES) { + result.push(...FIXED_DOMAIN_STATES[domain]); + } else { + // If not fixed, we at least know the current state + result.push(state.state); + } + + // Dynamic values based on the entities + switch (domain) { + case "climate": + result.push(...state.attributes.hvac_modes); + break; + case "input_select": + case "select": + result.push(...state.attributes.options); + break; + case "water_heater": + result.push(...state.attributes.operation_list); + break; + } + + // All entities can have unavailable states + result.push(...UNAVAILABLE_STATES); + return [...new Set(result)]; +}; diff --git a/src/components/entity/ha-entity-state-picker.ts b/src/components/entity/ha-entity-state-picker.ts new file mode 100644 index 0000000000..968bdedf46 --- /dev/null +++ b/src/components/entity/ha-entity-state-picker.ts @@ -0,0 +1,107 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import { computeStateDisplay } from "../../common/entity/compute_state_display"; +import { PolymerChangedEvent } from "../../polymer-types"; +import { getStates } from "../../common/entity/get_states"; +import { HomeAssistant } from "../../types"; +import "../ha-combo-box"; +import type { HaComboBox } from "../ha-combo-box"; + +export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; + +@customElement("ha-entity-state-picker") +class HaEntityStatePicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public entityId?: string; + + @property({ type: Boolean }) public autofocus = false; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @property({ type: Boolean, attribute: "allow-custom-value" }) + public allowCustomValue; + + @property() public label?: string; + + @property() public value?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) private _opened = false; + + @query("ha-combo-box", true) private _comboBox!: HaComboBox; + + protected shouldUpdate(changedProps: PropertyValues) { + return !(!changedProps.has("_opened") && this._opened); + } + + protected updated(changedProps: PropertyValues) { + if (changedProps.has("_opened") && this._opened) { + const state = this.entityId ? this.hass.states[this.entityId] : undefined; + (this._comboBox as any).items = + this.entityId && state + ? getStates(state).map((key) => ({ + value: key, + label: computeStateDisplay( + this.hass.localize, + state, + this.hass.locale, + key + ), + })) + : []; + } + } + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + + return html` + + + `; + } + + private _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + } + + private _valueChanged(ev: PolymerChangedEvent) { + this.value = ev.detail.value; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-entity-state-picker": HaEntityStatePicker; + } +} diff --git a/src/components/ha-selector/ha-selector-state.ts b/src/components/ha-selector/ha-selector-state.ts new file mode 100644 index 0000000000..4f2a39c540 --- /dev/null +++ b/src/components/ha-selector/ha-selector-state.ts @@ -0,0 +1,49 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { StateSelector } from "../../data/selector"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../types"; +import "../entity/ha-entity-state-picker"; + +@customElement("ha-selector-state") +export class HaSelectorState extends SubscribeMixin(LitElement) { + @property() public hass!: HomeAssistant; + + @property() public selector!: StateSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @property() public context?: { + filter_entity?: string; + }; + + protected render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-state": HaSelectorState; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 7d6400b289..cc13845e01 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -18,6 +18,7 @@ import "./ha-selector-file"; import "./ha-selector-number"; import "./ha-selector-object"; import "./ha-selector-select"; +import "./ha-selector-state"; import "./ha-selector-target"; import "./ha-selector-template"; import "./ha-selector-text"; diff --git a/src/data/selector.ts b/src/data/selector.ts index 86d6d0eda9..17474651b1 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -23,6 +23,7 @@ export type Selector = | NumberSelector | ObjectSelector | SelectSelector + | StateSelector | StringSelector | TargetSelector | TemplateSelector @@ -191,6 +192,12 @@ export interface SelectSelector { }; } +export interface StateSelector { + state: { + entity_id?: string; + }; +} + export interface StringSelector { text: { multiline?: boolean; diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts index ad0f86c146..310dc88d01 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts @@ -37,7 +37,7 @@ export class HaStateCondition extends LitElement implements ConditionElement { name: "attribute", selector: { attribute: { entity_id: entityId } }, }, - { name: "state", selector: { text: {} } }, + { name: "state", selector: { state: { entity_id: entityId } } }, { name: "for", selector: { duration: {} } }, ] as const ); diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts index 00ded53fac..6a24bf98d5 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts @@ -56,8 +56,18 @@ export class HaStateTrigger extends LitElement implements TriggerElement { name: "attribute", selector: { attribute: { entity_id: entityId } }, }, - { name: "from", selector: { text: {} } }, - { name: "to", selector: { text: {} } }, + { + name: "from", + selector: { + state: { entity_id: entityId ? entityId[0] : undefined }, + }, + }, + { + name: "to", + selector: { + state: { entity_id: entityId ? entityId[0] : undefined }, + }, + }, { name: "for", selector: { duration: {} } }, ] as const ); diff --git a/src/translations/en.json b/src/translations/en.json index 99d5bf27b0..0414bfa152 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -400,6 +400,9 @@ "entity-attribute-picker": { "attribute": "Attribute", "show_attributes": "Show attributes" + }, + "entity-state-picker": { + "state": "State" } }, "target-picker": {