1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00

Button feature change variable handling script (#26786)

* Update types.ts

Added extra parameter button_action

* Update hui-button-card-feature-editor.ts

Added second field

* Update hui-button-card-feature.ts

* Update types.ts

* Update hui-button-card-feature-editor.ts

Fix issue with field naming

* Update hui-button-card-feature-editor.ts

* Update hui-button-card-feature-editor.ts

* Update hui-button-card-feature-editor.ts

* .

* Strategy update

* Update types.ts

* Update hui-button-card-feature-editor.ts

* Fix linting issues

* Add data field to editor

* localize error

* Update hui-button-card-feature.ts

Added suggestions

* Use UI to set script variables in button feature

* Update src/panels/lovelace/card-features/hui-button-card-feature.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Douwe
2025-09-24 14:53:42 +02:00
committed by GitHub
parent 8f781e53e3
commit 99d9c67492
10 changed files with 199 additions and 37 deletions

View File

@@ -5,17 +5,20 @@ import type {
} from "home-assistant-js-websocket";
import type { Describe } from "superstruct";
import {
object,
optional,
string,
union,
array,
assign,
boolean,
object,
optional,
refine,
string,
union,
} from "superstruct";
import { arrayLiteralIncludes } from "../common/array/literal-includes";
import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate";
import { hasTemplate } from "../common/string/has-template";
import { createSearchParam } from "../common/url/search-params";
import type { HomeAssistant } from "../types";
import type {
Condition,
@@ -26,9 +29,6 @@ import type {
} from "./automation";
import { migrateAutomationTrigger } from "./automation";
import type { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params";
import { hasTemplate } from "../common/string/has-template";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
@@ -395,6 +395,37 @@ export const hasScriptFields = (
return fields !== undefined && Object.keys(fields).length > 0;
};
export const hasRequiredScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
return (
fields !== undefined &&
Object.values(fields).some((field) => field.required)
);
};
export const requiredScriptFieldsFilled = (
hass: HomeAssistant,
entityId: string,
data?: Record<string, any>
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
if (fields === undefined || Object.keys(fields).length === 0) {
return true;
}
if (data === undefined) {
return false;
}
return Object.entries(fields).every(([key, field]) => {
if (field.required) {
return data[key] !== undefined;
}
return true;
});
};
export const migrateAutomationAction = (
action: Action | Action[]
): Action | Action[] => {

View File

@@ -3,20 +3,24 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import "../../../components/entity/state-info";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/entity/state-info";
import type { HomeAssistant } from "../../../types";
import type { ScriptEntity } from "../../../data/script";
import { canRun } from "../../../data/script";
import { isUnavailableState } from "../../../data/entity";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { listenMediaQuery } from "../../../common/dom/media_query";
import "../components/ha-more-info-state-header";
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import "../../../components/ha-markdown";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import { isUnavailableState } from "../../../data/entity";
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import type { ScriptEntity } from "../../../data/script";
import {
canRun,
hasRequiredScriptFields,
requiredScriptFieldsFilled,
} from "../../../data/script";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
@customElement("more-info-script")
class MoreInfoScript extends LitElement {
@@ -26,6 +30,8 @@ class MoreInfoScript extends LitElement {
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@property({ attribute: false }) public data?: Record<string, any>;
@state() private _scriptData: Record<string, any> = {};
@state() private narrow = false;
@@ -110,7 +116,10 @@ class MoreInfoScript extends LitElement {
hide-picker
hide-description
.hass=${this.hass}
.value=${this._scriptData}
.value=${{
...(this.data ? { data: this.data } : {}),
...this._scriptData,
}}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
@@ -198,7 +207,13 @@ class MoreInfoScript extends LitElement {
private _canRun() {
if (
canRun(this.stateObj!) ||
!hasRequiredScriptFields(this.hass, this.stateObj!.entity_id) ||
(requiredScriptFieldsFilled(
this.hass,
this.stateObj!.entity_id,
this._scriptData.data
) &&
canRun(this.stateObj!)) ||
// Restart can also always runs. Just cancels other run.
this.stateObj!.attributes.mode === "restart"
) {

View File

@@ -64,6 +64,7 @@ export interface MoreInfoDialogParams {
view?: View;
/** @deprecated Use `view` instead */
tab?: View;
data?: Record<string, any>;
}
type View = "info" | "history" | "settings" | "related";
@@ -96,6 +97,8 @@ export class MoreInfoDialog extends LitElement {
@state() private _entityId?: string | null;
@state() private _data?: Record<string, any>;
@state() private _currView: View = DEFAULT_VIEW;
@state() private _initialView: View = DEFAULT_VIEW;
@@ -116,6 +119,8 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog();
return;
}
this._data = params.data;
this._currView = params.view || DEFAULT_VIEW;
this._initialView = params.view || DEFAULT_VIEW;
this._childView = undefined;
@@ -570,6 +575,7 @@ export class MoreInfoDialog extends LitElement {
.entityId=${this._entityId}
.entry=${this._entry}
.editMode=${this._infoEditMode}
.data=${this._data}
></ha-more-info-info>
`
: this._currView === "history"

View File

@@ -27,6 +27,8 @@ export class MoreInfoInfo extends LitElement {
@property({ attribute: false }) public editMode?: boolean;
@property({ attribute: false }) public data?: Record<string, any>;
@state() private _sensorNumericDeviceClasses?: string[] = [];
private async _loadNumericDeviceClasses() {
@@ -105,6 +107,7 @@ export class MoreInfoInfo extends LitElement {
.hass=${this.hass}
.entry=${this.entry}
.editMode=${this.editMode}
.data=${this.data}
></more-info-content>
</div>
</div>

View File

@@ -6,14 +6,14 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import "../../components/ha-badge";
import type { ExtEntityRegistryEntry } from "../../data/entity_registry";
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
import type { TileCardConfig } from "../../panels/lovelace/cards/types";
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
import "../../panels/lovelace/sections/hui-section";
import type { HomeAssistant } from "../../types";
import { stateMoreInfoType } from "./state_more_info_control";
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
@customElement("more-info-content")
class MoreInfoContent extends LitElement {
@@ -25,6 +25,8 @@ class MoreInfoContent extends LitElement {
@property({ attribute: false }) public editMode?: boolean;
@property({ attribute: false }) public data?: Record<string, any>;
protected render() {
let moreInfoType: string | undefined;
@@ -48,6 +50,7 @@ class MoreInfoContent extends LitElement {
stateObj: this.stateObj,
entry: this.entry,
editMode: this.editMode,
data: this.data,
})}
${this._showEntityMembers(this.stateObj)
? html`

View File

@@ -4,7 +4,10 @@ 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 { hasScriptFields } from "../../../data/script";
import {
hasRequiredScriptFields,
requiredScriptFieldsFilled,
} from "../../../data/script";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
@@ -50,15 +53,28 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
if (domain === "script") {
const entityId = this._stateObj.entity_id;
if (hasScriptFields(this.hass!, entityId)) {
showMoreInfoDialog(this, { entityId: entityId });
if (
hasRequiredScriptFields(this.hass!, entityId) &&
!requiredScriptFieldsFilled(this.hass!, entityId, this._config?.data)
) {
showMoreInfoDialog(this, {
entityId: entityId,
data: this._config?.data,
});
return;
}
}
this.hass.callService(domain, service, {
const serviceData = {
entity_id: this._stateObj.entity_id,
});
...(this._config?.data
? {
variables: this._config.data,
}
: {}),
};
this.hass.callService(domain, service, serviceData);
}
static getStubConfig(): ButtonCardFeatureConfig {

View File

@@ -2,9 +2,12 @@ import type { AlarmMode } from "../../../data/alarm_control_panel";
import type { HvacMode } from "../../../data/climate";
import type { OperationMode } from "../../../data/water_heater";
export type ButtonCardData = Record<string, any>;
export interface ButtonCardFeatureConfig {
type: "button";
action_name?: string;
data?: ButtonCardData;
}
export interface CoverOpenCloseCardFeatureConfig {

View File

@@ -1,10 +1,21 @@
import { html, LitElement, nothing } from "lit";
import { mdiApplicationVariableOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-service-control";
import "../../../../components/ha-svg-icon";
import { hasScriptFields } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import type { ButtonCardFeatureConfig } from "../../card-features/types";
import type {
ButtonCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-button-card-feature-editor")
@@ -14,6 +25,8 @@ export class HuiButtonCardFeatureEditor
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ButtonCardFeatureConfig;
public setConfig(config: ButtonCardFeatureConfig): void {
@@ -35,6 +48,27 @@ export class HuiButtonCardFeatureEditor
return nothing;
}
let scriptData:
| {
action: string;
data?: Record<string, any>;
}
| undefined;
if (this.context?.entity_id) {
const domain = computeDomain(this.context.entity_id);
if (
domain === "script" &&
hasScriptFields(this.hass, this.context.entity_id)
) {
scriptData = {
action: this.context.entity_id,
data: this._config.data,
};
}
}
return html`
<ha-form
.hass=${this.hass}
@@ -43,19 +77,68 @@ export class HuiButtonCardFeatureEditor
.computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged}
></ha-form>
${scriptData
? html`<ha-expansion-panel
outlined
expanded
.header=${this.hass.localize(
"ui.components.service-control.script_variables"
)}
.secondary=${this.hass.localize("ui.common.optional")}
no-collapse
>
<ha-svg-icon
slot="leading-icon"
.path=${mdiApplicationVariableOutline}
></ha-svg-icon>
<ha-service-control
hide-picker
hide-description
.hass=${this.hass}
.value=${scriptData}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${false}
@value-changed=${this._scriptFieldVariablesChanged}
></ha-service-control
></ha-expansion-panel>`
: nothing}
`;
}
private _computeLabel = () => this.hass.localize("ui.common.name");
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "action_name":
return this.hass!.localize("ui.common.name");
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
private _scriptFieldVariablesChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", {
config: {
...(this._config || {}),
data: ev.detail.value.data,
},
});
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.dispatchEvent(
new CustomEvent("config-changed", {
detail: { config: ev.detail.value },
})
);
fireEvent(this, "config-changed", {
config: { ...(this._config || {}), ...ev.detail.value },
});
}
static styles = css`
ha-expansion-panel {
margin-top: 16px;
}
`;
}
declare global {

View File

@@ -30,6 +30,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
{
entityId: ev.detail.entityId,
view: ev.detail.view || ev.detail.tab,
data: ev.detail.data,
},
() => import("../dialogs/more-info/ha-more-info-dialog")
);

View File

@@ -955,7 +955,8 @@
"target": "Targets",
"target_secondary": "What should this action use as targeted areas, devices or entities.",
"action_data": "Action data",
"integration_doc": "Integration documentation"
"integration_doc": "Integration documentation",
"script_variables": "Script variables"
},
"related-items": {
"no_related_found": "No related items found.",