diff --git a/src/common/const.ts b/src/common/const.ts index 3f6233c788..bd8f5df23b 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -27,6 +27,7 @@ import { mdiFormTextbox, mdiGasCylinder, mdiGauge, + mdiGestureTapButton, mdiGoogleAssistant, mdiGoogleCirclesCommunities, mdiHomeAssistant, @@ -83,6 +84,7 @@ export const FIXED_DOMAIN_ICONS = { homekit: mdiHomeAutomation, image_processing: mdiImageFilterFrames, input_boolean: mdiToggleSwitchOutline, + input_button: mdiGestureTapButton, input_datetime: mdiCalendarClock, input_number: mdiRayVertex, input_select: mdiFormatListBulleted, @@ -150,6 +152,7 @@ export const DOMAINS_WITH_CARD = [ "climate", "cover", "configurator", + "input_button", "input_select", "input_number", "input_text", @@ -216,6 +219,7 @@ export const DOMAINS_INPUT_ROW = [ "group", "humidifier", "input_boolean", + "input_button", "input_datetime", "input_number", "input_select", diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index c07be7594d..adf62f2104 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -119,6 +119,7 @@ export const computeStateDisplay = ( // state of button is a timestamp if ( domain === "button" || + domain === "input_button" || (domain === "sensor" && stateObj.attributes.device_class === "timestamp") ) { return formatDateTime(new Date(compareState), locale); diff --git a/src/data/input_button.ts b/src/data/input_button.ts new file mode 100644 index 0000000000..785a381a48 --- /dev/null +++ b/src/data/input_button.ts @@ -0,0 +1,41 @@ +import { HomeAssistant } from "../types"; + +export interface InputButton { + id: string; + name: string; + icon?: string; +} + +export interface InputButtonMutableParams { + name: string; + icon: string; +} + +export const fetchInputButton = (hass: HomeAssistant) => + hass.callWS({ type: "input_button/list" }); + +export const createInputButton = ( + hass: HomeAssistant, + values: InputButtonMutableParams +) => + hass.callWS({ + type: "input_button/create", + ...values, + }); + +export const updateInputButton = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "input_button/update", + input_button_id: id, + ...updates, + }); + +export const deleteInputButton = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "input_button/delete", + input_button_id: id, + }); diff --git a/src/panels/config/entities/const.ts b/src/panels/config/entities/const.ts index 1c5e4b4f59..11d2b2622b 100644 --- a/src/panels/config/entities/const.ts +++ b/src/panels/config/entities/const.ts @@ -7,4 +7,5 @@ export const PLATFORMS_WITH_SETTINGS_TAB = { input_datetime: "entity-settings-helper-tab", counter: "entity-settings-helper-tab", timer: "entity-settings-helper-tab", + input_button: "entity-settings-helper-tab", }; diff --git a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts index 085aa19eb9..913bcd8353 100644 --- a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts +++ b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts @@ -24,6 +24,11 @@ import { fetchInputBoolean, updateInputBoolean, } from "../../../../../data/input_boolean"; +import { + deleteInputButton, + fetchInputButton, + updateInputButton, +} from "../../../../../data/input_button"; import { deleteInputDateTime, fetchInputDateTime, @@ -55,6 +60,7 @@ import type { HomeAssistant } from "../../../../../types"; import type { Helper } from "../../../helpers/const"; import "../../../helpers/forms/ha-counter-form"; import "../../../helpers/forms/ha-input_boolean-form"; +import "../../../helpers/forms/ha-input_button-form"; import "../../../helpers/forms/ha-input_datetime-form"; import "../../../helpers/forms/ha-input_number-form"; import "../../../helpers/forms/ha-input_select-form"; @@ -69,6 +75,11 @@ const HELPERS = { update: updateInputBoolean, delete: deleteInputBoolean, }, + input_button: { + fetch: fetchInputButton, + update: updateInputButton, + delete: deleteInputButton, + }, input_text: { fetch: fetchInputText, update: updateInputText, diff --git a/src/panels/config/helpers/const.ts b/src/panels/config/helpers/const.ts index 7115338351..2e927f66ad 100644 --- a/src/panels/config/helpers/const.ts +++ b/src/panels/config/helpers/const.ts @@ -1,5 +1,6 @@ import { Counter } from "../../../data/counter"; import { InputBoolean } from "../../../data/input_boolean"; +import { InputButton } from "../../../data/input_button"; import { InputDateTime } from "../../../data/input_datetime"; import { InputNumber } from "../../../data/input_number"; import { InputSelect } from "../../../data/input_select"; @@ -8,6 +9,7 @@ import { Timer } from "../../../data/timer"; export const HELPER_DOMAINS = [ "input_boolean", + "input_button", "input_text", "input_number", "input_datetime", @@ -18,6 +20,7 @@ export const HELPER_DOMAINS = [ export type Helper = | InputBoolean + | InputButton | InputText | InputNumber | InputSelect diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index 9ae73825f4..b94e865f02 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -10,6 +10,7 @@ import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-dialog"; import { createCounter } from "../../../data/counter"; import { createInputBoolean } from "../../../data/input_boolean"; +import { createInputButton } from "../../../data/input_button"; import { createInputDateTime } from "../../../data/input_datetime"; import { createInputNumber } from "../../../data/input_number"; import { createInputSelect } from "../../../data/input_select"; @@ -20,6 +21,7 @@ import { HomeAssistant } from "../../../types"; import { Helper } from "./const"; import "./forms/ha-counter-form"; import "./forms/ha-input_boolean-form"; +import "./forms/ha-input_button-form"; import "./forms/ha-input_datetime-form"; import "./forms/ha-input_number-form"; import "./forms/ha-input_select-form"; @@ -28,6 +30,7 @@ import "./forms/ha-timer-form"; const HELPERS = { input_boolean: createInputBoolean, + input_button: createInputButton, input_text: createInputText, input_number: createInputNumber, input_datetime: createInputDateTime, diff --git a/src/panels/config/helpers/forms/ha-input_button-form.ts b/src/panels/config/helpers/forms/ha-input_button-form.ts new file mode 100644 index 0000000000..e61faa498a --- /dev/null +++ b/src/panels/config/helpers/forms/ha-input_button-form.ts @@ -0,0 +1,114 @@ +import "@polymer/paper-input/paper-input"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-icon-picker"; +import { InputButton } from "../../../../data/input_button"; +import { haStyle } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; + +@customElement("ha-input_button-form") +class HaInputButtonForm extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public new?: boolean; + + @state() private _name!: string; + + @state() private _icon!: string; + + private _item?: InputButton; + + set item(item: InputButton) { + this._item = item; + if (item) { + this._name = item.name || ""; + this._icon = item.icon || ""; + } else { + this._name = ""; + this._icon = ""; + } + } + + public focus() { + this.updateComplete.then(() => + ( + this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement + )?.focus() + ); + } + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + const nameInvalid = !this._name || this._name.trim() === ""; + + return html` +
+ + +
+ `; + } + + private _valueChanged(ev: CustomEvent) { + if (!this.new && !this._item) { + return; + } + ev.stopPropagation(); + const configValue = (ev.target as any).configValue; + const value = ev.detail.value; + if (this[`_${configValue}`] === value) { + return; + } + const newValue = { ...this._item }; + if (!value) { + delete newValue[configValue]; + } else { + newValue[configValue] = ev.detail.value; + } + fireEvent(this, "value-changed", { + value: newValue, + }); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .form { + color: var(--primary-text-color); + } + .row { + padding: 16px 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-input_button-form": HaInputButtonForm; + } +} diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index b4c9afc556..6794fdfb58 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -28,6 +28,8 @@ const LAZY_LOAD_TYPES = { "climate-entity": () => import("../entity-rows/hui-climate-entity-row"), "cover-entity": () => import("../entity-rows/hui-cover-entity-row"), "group-entity": () => import("../entity-rows/hui-group-entity-row"), + "input-button-entity": () => + import("../entity-rows/hui-input-button-entity-row"), "humidifier-entity": () => import("../entity-rows/hui-humidifier-entity-row"), "input-datetime-entity": () => import("../entity-rows/hui-input-datetime-entity-row"), @@ -61,6 +63,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { group: "group", humidifier: "humidifier", input_boolean: "toggle", + input_button: "input-button", input_number: "input-number", input_select: "input-select", input_text: "input-text", diff --git a/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts new file mode 100644 index 0000000000..387a3b57a9 --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts @@ -0,0 +1,82 @@ +import "@material/mwc-button/mwc-button"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { UNAVAILABLE } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import "../components/hui-generic-entity-row"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; +import { ActionRowConfig, LovelaceRow } from "./types"; + +@customElement("hui-input-button-entity-row") +class HuiInputButtonEntityRow extends LitElement implements LovelaceRow { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _config?: ActionRowConfig; + + public setConfig(config: ActionRowConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + return html` + + + ${this.hass.localize("ui.card.button.press")} + + + `; + } + + static get styles(): CSSResultGroup { + return css` + mwc-button:last-child { + margin-right: -0.57em; + } + `; + } + + private _pressButton(ev): void { + ev.stopPropagation(); + this.hass.callService("input_button", "press", { + entity_id: this._config!.entity, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-input-button-entity-row": HuiInputButtonEntityRow; + } +} diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js index f5c1a34a0b..5f2d5ea484 100644 --- a/src/state-summary/state-card-content.js +++ b/src/state-summary/state-card-content.js @@ -7,6 +7,7 @@ import "./state-card-climate"; import "./state-card-configurator"; import "./state-card-cover"; import "./state-card-display"; +import "./state-card-input_button"; import "./state-card-input_number"; import "./state-card-input_select"; import "./state-card-input_text"; diff --git a/src/state-summary/state-card-input_button.ts b/src/state-summary/state-card-input_button.ts new file mode 100644 index 0000000000..9e6437bf4d --- /dev/null +++ b/src/state-summary/state-card-input_button.ts @@ -0,0 +1,54 @@ +import "@material/mwc-button"; +import { HassEntity } from "home-assistant-js-websocket"; +import { CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../components/entity/ha-entity-toggle"; +import "../components/entity/state-info"; +import { UNAVAILABLE } from "../data/entity"; +import { haStyle } from "../resources/styles"; +import { HomeAssistant } from "../types"; + +@customElement("state-card-input_button") +export class StateCardInputButton extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public stateObj!: HassEntity; + + @property({ type: Boolean }) public inDialog = false; + + protected render() { + const stateObj = this.stateObj; + return html` +
+ + + ${this.hass.localize("ui.card.button.press")} + +
+ `; + } + + private _pressButton(ev: Event) { + ev.stopPropagation(); + this.hass.callService("input_button", "press", { + entity_id: this.stateObj.entity_id, + }); + } + + static get styles(): CSSResultGroup { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-input_button": StateCardInputButton; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 96bb97facd..c635f8529c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1242,6 +1242,7 @@ "input_number": "Number", "input_select": "Dropdown", "input_boolean": "Toggle", + "input_button": "Button", "input_datetime": "Date and/or time", "counter": "Counter", "timer": "Timer" @@ -1441,6 +1442,7 @@ "person": "People", "zone": "Zones", "input_boolean": "Input booleans", + "input_button": "Input buttons", "input_text": "Input texts", "input_number": "Input numbers", "input_datetime": "Input date times",