From 6dbfc2f4eda02f1505fa46205a70152d45651a09 Mon Sep 17 00:00:00 2001
From: Douwe <61123717+dhoeben@users.noreply.github.com>
Date: Wed, 30 Jul 2025 16:13:05 +0200
Subject: [PATCH] Add new card feature: button (#26165)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
---
.../card-features/hui-button-card-feature.ts | 98 +++++++++++++++++++
src/panels/lovelace/card-features/types.ts | 6 ++
.../create-card-feature-element.ts | 2 +
.../hui-button-card-feature-editor.ts | 60 ++++++++++++
.../hui-card-features-editor.ts | 4 +
5 files changed, 170 insertions(+)
create mode 100644 src/panels/lovelace/card-features/hui-button-card-feature.ts
create mode 100644 src/panels/lovelace/editor/config-elements/hui-button-card-feature-editor.ts
diff --git a/src/panels/lovelace/card-features/hui-button-card-feature.ts b/src/panels/lovelace/card-features/hui-button-card-feature.ts
new file mode 100644
index 0000000000..5d1f08ff2a
--- /dev/null
+++ b/src/panels/lovelace/card-features/hui-button-card-feature.ts
@@ -0,0 +1,98 @@
+import { html, LitElement, nothing } from "lit";
+import type { HassEntity } from "home-assistant-js-websocket";
+import { customElement, property, state } from "lit/decorators";
+import { computeDomain } from "../../../common/entity/compute_domain";
+import "../../../components/ha-control-button";
+import "../../../components/ha-control-button-group";
+import type { HomeAssistant } from "../../../types";
+import type { LovelaceCardFeature } from "../types";
+import { cardFeatureStyles } from "./common/card-feature-styles";
+import type {
+ LovelaceCardFeatureContext,
+ ButtonCardFeatureConfig,
+} from "./types";
+
+export const supportsButtonCardFeature = (
+ hass: HomeAssistant,
+ context: LovelaceCardFeatureContext
+) => {
+ const stateObj = context.entity_id
+ ? hass.states[context.entity_id]
+ : undefined;
+ if (!stateObj) return false;
+ const domain = computeDomain(stateObj.entity_id);
+ return ["button", "script"].includes(domain);
+};
+
+@customElement("hui-button-card-feature")
+class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
+
+ @state() private _config?: ButtonCardFeatureConfig;
+
+ private get _stateObj() {
+ if (!this.hass || !this.context || !this.context.entity_id) {
+ return undefined;
+ }
+ return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
+ }
+
+ private _pressButton() {
+ if (!this.hass || !this._stateObj) return;
+
+ const domain = computeDomain(this._stateObj.entity_id);
+ const service = domain === "button" ? "press" : "turn_on";
+
+ this.hass.callService(domain, service, {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ static getStubConfig(): ButtonCardFeatureConfig {
+ return {
+ type: "button",
+ };
+ }
+
+ public setConfig(config: ButtonCardFeatureConfig): void {
+ if (!config) {
+ throw new Error("Invalid configuration");
+ }
+ this._config = config;
+ }
+
+ protected render() {
+ if (
+ !this._config ||
+ !this.hass ||
+ !this.context ||
+ !this._stateObj ||
+ !supportsButtonCardFeature(this.hass, this.context)
+ ) {
+ return nothing;
+ }
+
+ return html`
+
+
+ ${this._config.action_name ??
+ this.hass.localize("ui.card.button.press")}
+
+
+ `;
+ }
+
+ static styles = cardFeatureStyles;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-button-card-feature": HuiButtonCardFeature;
+ }
+}
diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts
index 9ded542fe3..c937170aff 100644
--- a/src/panels/lovelace/card-features/types.ts
+++ b/src/panels/lovelace/card-features/types.ts
@@ -2,6 +2,11 @@ import type { AlarmMode } from "../../../data/alarm_control_panel";
import type { HvacMode } from "../../../data/climate";
import type { OperationMode } from "../../../data/water_heater";
+export interface ButtonCardFeatureConfig {
+ type: "button";
+ action_name?: string;
+}
+
export interface CoverOpenCloseCardFeatureConfig {
type: "cover-open-close";
}
@@ -185,6 +190,7 @@ export type LovelaceCardFeaturePosition = "bottom" | "inline";
export type LovelaceCardFeatureConfig =
| AlarmModesCardFeatureConfig
+ | ButtonCardFeatureConfig
| ClimateFanModesCardFeatureConfig
| ClimateSwingModesCardFeatureConfig
| ClimateSwingHorizontalModesCardFeatureConfig
diff --git a/src/panels/lovelace/create-element/create-card-feature-element.ts b/src/panels/lovelace/create-element/create-card-feature-element.ts
index fc003da1f9..fa5a006b44 100644
--- a/src/panels/lovelace/create-element/create-card-feature-element.ts
+++ b/src/panels/lovelace/create-element/create-card-feature-element.ts
@@ -1,4 +1,5 @@
import "../card-features/hui-alarm-modes-card-feature";
+import "../card-features/hui-button-card-feature";
import "../card-features/hui-climate-fan-modes-card-feature";
import "../card-features/hui-climate-hvac-modes-card-feature";
import "../card-features/hui-climate-preset-modes-card-feature";
@@ -37,6 +38,7 @@ import {
const TYPES = new Set([
"alarm-modes",
+ "button",
"area-controls",
"climate-fan-modes",
"climate-swing-modes",
diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-feature-editor.ts
new file mode 100644
index 0000000000..e5fa06a69b
--- /dev/null
+++ b/src/panels/lovelace/editor/config-elements/hui-button-card-feature-editor.ts
@@ -0,0 +1,60 @@
+import { html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import type { HomeAssistant } from "../../../../types";
+import type { ButtonCardFeatureConfig } from "../../card-features/types";
+import type { LovelaceCardFeatureEditor } from "../../types";
+import "../../../../components/ha-form/ha-form";
+import type { HaFormSchema } from "../../../../components/ha-form/types";
+
+@customElement("hui-button-card-feature-editor")
+export class HuiButtonCardFeatureEditor
+ extends LitElement
+ implements LovelaceCardFeatureEditor
+{
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _config?: ButtonCardFeatureConfig;
+
+ public setConfig(config: ButtonCardFeatureConfig): void {
+ this._config = config;
+ }
+
+ private _schema: HaFormSchema[] = [
+ {
+ name: "action_name",
+ selector: {
+ text: {},
+ },
+ },
+ ];
+
+ protected render() {
+ if (!this.hass || !this._config) {
+ return nothing;
+ }
+
+ return html`
+
+ `;
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ ev.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent("config-changed", {
+ detail: { config: ev.detail.value },
+ })
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-button-card-feature-editor": HuiButtonCardFeatureEditor;
+ }
+}
diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts
index befbb73c7b..0b0c17f27b 100644
--- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts
@@ -19,6 +19,7 @@ import {
import type { HomeAssistant } from "../../../../types";
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
import { supportsAreaControlsCardFeature } from "../../card-features/hui-area-controls-card-feature";
+import { supportsButtonCardFeature } from "../../card-features/hui-button-card-feature";
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature";
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
@@ -63,6 +64,7 @@ type SupportsFeature = (
const UI_FEATURE_TYPES = [
"alarm-modes",
"area-controls",
+ "button",
"climate-fan-modes",
"climate-hvac-modes",
"climate-preset-modes",
@@ -98,6 +100,7 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
const EDITABLES_FEATURE_TYPES = new Set([
"alarm-modes",
"area-controls",
+ "button",
"climate-fan-modes",
"climate-hvac-modes",
"climate-preset-modes",
@@ -120,6 +123,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
> = {
"alarm-modes": supportsAlarmModesCardFeature,
"area-controls": supportsAreaControlsCardFeature,
+ button: supportsButtonCardFeature,
"climate-fan-modes": supportsClimateFanModesCardFeature,
"climate-swing-modes": supportsClimateSwingModesCardFeature,
"climate-swing-horizontal-modes":