20220907.0 (#13646)

This commit is contained in:
Bram Kragten 2022-09-07 16:54:32 +02:00 committed by GitHub
commit c5428d8581
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 674 additions and 464 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220906.0"
version = "20220907.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -239,10 +239,13 @@ export const getStates = (
}
break;
case "light":
if (attribute === "effect") {
if (attribute === "effect" && state.attributes.effect_list) {
result.push(...state.attributes.effect_list);
} else if (attribute === "color_mode") {
result.push(...state.attributes.color_modes);
} else if (
attribute === "color_mode" &&
state.attributes.supported_color_modes
) {
result.push(...state.attributes.supported_color_modes);
}
break;
case "media_player":

View File

@ -7,6 +7,7 @@ import { getStates } from "../../common/entity/get_states";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import { formatAttributeValue } from "../../data/entity_attributes";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@ -55,7 +56,7 @@ class HaEntityStatePicker extends LitElement {
this.hass.locale,
key
)
: key,
: formatAttributeValue(this.hass, key),
}))
: [];
}
@ -69,16 +70,7 @@ class HaEntityStatePicker extends LitElement {
return html`
<ha-combo-box
.hass=${this.hass}
.value=${this.value
? this.entityId && this.hass.states[this.entityId]
? computeStateDisplay(
this.hass.localize,
this.hass.states[this.entityId],
this.hass.locale,
this.value
)
: this.value
: ""}
.value=${this.value}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize("ui.components.entity.entity-state-picker.state")}

View File

@ -17,8 +17,9 @@ export interface IconOverflowMenuItem {
narrowOnly?: boolean;
disabled?: boolean;
tooltip?: string;
onClick: CallableFunction;
action: () => any;
warning?: boolean;
divider?: boolean;
}
@customElement("ha-icon-overflow-menu")
@ -46,23 +47,23 @@ export class HaIconOverflowMenu extends LitElement {
slot="trigger"
></ha-icon-button>
${this.items.map(
(item) => html`
<mwc-list-item
graphic="icon"
.disabled=${item.disabled}
@click=${item.action}
class=${classMap({ warning: Boolean(item.warning) })}
>
<div slot="graphic">
<ha-svg-icon
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
</div>
${item.label}
</mwc-list-item>
`
${this.items.map((item) =>
item.divider
? html`<li divider role="separator"></li>`
: html`<mwc-list-item
graphic="icon"
?disabled=${item.disabled}
@click=${item.action}
class=${classMap({ warning: Boolean(item.warning) })}
>
<div slot="graphic">
<ha-svg-icon
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
</div>
${item.label}
</mwc-list-item> `
)}
</ha-button-menu>`
: html`
@ -70,6 +71,8 @@ export class HaIconOverflowMenu extends LitElement {
${this.items.map((item) =>
item.narrowOnly
? ""
: item.divider
? html`<div role="separator"></div>`
: html`<div>
${item.tooltip
? html`<paper-tooltip animation-delay="0" position="left">
@ -80,7 +83,7 @@ export class HaIconOverflowMenu extends LitElement {
@click=${item.action}
.label=${item.label}
.path=${item.path}
.disabled=${item.disabled}
?disabled=${item.disabled}
></ha-icon-button>
</div> `
)}
@ -114,6 +117,13 @@ export class HaIconOverflowMenu extends LitElement {
display: flex;
justify-content: flex-end;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
div[role="separator"] {
border-right: 1px solid var(--divider-color);
width: 1px;
}
`,
];
}

View File

@ -314,11 +314,25 @@ let inititialAutomationEditorData: Partial<AutomationConfig> | undefined;
export const getAutomationConfig = (hass: HomeAssistant, id: string) =>
hass.callApi<AutomationConfig>("GET", `config/automation/config/${id}`);
export const saveAutomationConfig = (
hass: HomeAssistant,
id: string,
config: AutomationConfig
) => hass.callApi<void>("POST", `config/automation/config/${id}`, config);
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
inititialAutomationEditorData = data;
navigate("/config/automation/edit/new");
};
export const duplicateAutomation = (config: AutomationConfig) => {
showAutomationEditor({
...config,
id: undefined,
alias: undefined,
});
};
export const getAutomationEditorInitData = () => {
const data = inititialAutomationEditorData;
inititialAutomationEditorData = undefined;

View File

@ -85,6 +85,13 @@ enum Protocols {
ZWaveLongRange = 1,
}
enum NodeType {
Controller,
/** @deprecated Use `NodeType["End Node"]` instead */
"Routing End Node",
"End Node" = 1,
}
export enum FirmwareUpdateStatus {
Error_Timeout = -1,
Error_Checksum = 0,
@ -142,12 +149,12 @@ export interface ZWaveJSController {
sdk_version: string;
type: number;
own_node_id: number;
is_secondary: boolean;
is_primary: boolean;
is_using_home_id_from_other_network: boolean;
is_sis_present: boolean;
was_real_primary: boolean;
is_static_update_controller: boolean;
is_slave: boolean;
is_suc: boolean;
node_type: NodeType;
firmware_version: string;
manufacturer_id: number;
product_id: number;

View File

@ -98,7 +98,9 @@ class MoreInfoClimate extends LitElement {
</div>
`
: ""}
${stateObj.attributes.temperature !== undefined &&
${supportTargetTemperature &&
!supportTargetTemperatureRange &&
stateObj.attributes.temperature !== undefined &&
stateObj.attributes.temperature !== null
? html`
<ha-climate-control
@ -112,10 +114,11 @@ class MoreInfoClimate extends LitElement {
></ha-climate-control>
`
: ""}
${(stateObj.attributes.target_temp_low !== undefined &&
${supportTargetTemperatureRange &&
((stateObj.attributes.target_temp_low !== undefined &&
stateObj.attributes.target_temp_low !== null) ||
(stateObj.attributes.target_temp_high !== undefined &&
stateObj.attributes.target_temp_high !== null)
(stateObj.attributes.target_temp_high !== undefined &&
stateObj.attributes.target_temp_high !== null))
? html`
<ha-climate-control
.hass=${this.hass}

View File

@ -31,6 +31,7 @@ import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
@ -44,6 +45,7 @@ import {
deleteAutomation,
getAutomationConfig,
getAutomationEditorInitData,
saveAutomationConfig,
showAutomationEditor,
triggerAutomationActions,
} from "../../../data/automation";
@ -153,7 +155,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon" @click=${this._showInfo}>
<mwc-list-item
graphic="icon"
.disabled=${!stateObj}
@click=${this._showInfo}
>
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
<ha-svg-icon
slot="graphic"
@ -198,7 +204,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
<mwc-list-item
graphic="icon"
@click=${this._promptAutomationMode}
.disabled=${!this.automationId || this._mode === "yaml"}
.disabled=${this._mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.change_mode"
@ -210,15 +216,18 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
</mwc-list-item>
`
: ""}
<mwc-list-item
graphic="icon"
@click=${this._toggleReOrderMode}
.disabled=${this._mode !== "gui"}
>
${this.hass.localize("ui.panel.config.automation.editor.re_order")}
<ha-svg-icon slot="graphic" .path=${mdiSort}></ha-svg-icon>
</mwc-list-item>
${this._config && !("use_blueprint" in this._config)
? html`<mwc-list-item
graphic="icon"
@click=${this._toggleReOrderMode}
.disabled=${this._mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order"
)}
<ha-svg-icon slot="graphic" .path=${mdiSort}></ha-svg-icon>
</mwc-list-item>`
: ""}
<mwc-list-item
.disabled=${!this.automationId}
@ -447,7 +456,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
this._dirty = false;
this._config = config;
} catch (err: any) {
showAlertDialog(this, {
await showAlertDialog(this, {
text:
err.status_code === 404
? this.hass.localize(
@ -458,7 +467,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
"err_no",
err.status_code
),
}).then(() => history.back());
});
history.back();
}
}
@ -534,11 +544,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
confirmText: this.hass!.localize("ui.common.leave"),
dismissText: this.hass!.localize("ui.common.stay"),
confirm: () => {
setTimeout(() => history.back());
afterNextRender(() => history.back());
},
});
} else {
history.back();
afterNextRender(() => history.back());
}
};
@ -577,8 +587,10 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
}
private async _delete() {
await deleteAutomation(this.hass, this.automationId as string);
history.back();
if (this.automationId) {
await deleteAutomation(this.hass, this.automationId);
history.back();
}
}
private _switchUiMode() {
@ -631,26 +643,21 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
await this._promptAutomationAlias();
}
this.hass!.callApi(
"POST",
"config/automation/config/" + id,
this._config
).then(
() => {
this._dirty = false;
try {
await saveAutomationConfig(this.hass, id, this._config!);
} catch (errors: any) {
this._errors = errors.body.message || errors.error || errors.body;
showToast(this, {
message: errors.body.message || errors.error || errors.body,
});
throw errors;
}
if (!this.automationId) {
navigate(`/config/automation/edit/${id}`, { replace: true });
}
},
(errors) => {
this._errors = errors.body.message || errors.error || errors.body;
showToast(this, {
message: errors.body.message || errors.error || errors.body,
});
throw errors;
}
);
this._dirty = false;
if (!this.automationId) {
navigate(`/config/automation/edit/${id}`, { replace: true });
}
}
private _subscribeAutomationConfig(ev) {

View File

@ -33,8 +33,8 @@ import "../../../components/ha-svg-icon";
import {
AutomationEntity,
deleteAutomation,
duplicateAutomation,
getAutomationConfig,
showAutomationEditor,
triggerAutomationActions,
} from "../../../data/automation";
import {
@ -212,6 +212,9 @@ class HaAutomationPicker extends LitElement {
),
action: () => this._showTrace(automation),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
@ -348,19 +351,45 @@ class HaAutomationPicker extends LitElement {
}
private async _delete(automation) {
await deleteAutomation(this.hass, automation.attributes.id);
try {
await deleteAutomation(this.hass, automation.attributes.id);
} catch (err: any) {
await showAlertDialog(this, {
text:
err.status_code === 400
? this.hass.localize(
"ui.panel.config.automation.editor.load_error_not_deletable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.load_error_unknown",
"err_no",
err.status_code
),
});
}
}
private async duplicate(automation) {
const config = await getAutomationConfig(
this.hass,
automation.attributes.id
);
showAutomationEditor({
...config,
id: undefined,
alias: undefined,
});
try {
const config = await getAutomationConfig(
this.hass,
automation.attributes.id
);
duplicateAutomation(config);
} catch (err: any) {
await showAlertDialog(this, {
text:
err.status_code === 404
? this.hass.localize(
"ui.panel.config.automation.editor.load_error_not_duplicable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.load_error_unknown",
"err_no",
err.status_code
),
});
}
}
private _showHelp() {
@ -389,7 +418,7 @@ class HaAutomationPicker extends LitElement {
);
if (automation?.attributes.id) {
navigate(`/config/automation/edit/${automation?.attributes.id}`);
navigate(`/config/automation/edit/${automation.attributes.id}`);
}
}

View File

@ -1,6 +1,5 @@
import {
mdiDownload,
mdiPencil,
mdiRayEndArrow,
mdiRayStartArrow,
mdiRefresh,
@ -34,7 +33,7 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../../../layouts/hass-subpage";
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@ -90,89 +89,76 @@ export class HaAutomationTrace extends LitElement {
</div>`;
}
const actionButtons = html`
<ha-icon-button
.label=${this.hass.localize("ui.panel.config.automation.trace.refresh")}
.path=${mdiRefresh}
@click=${this._refreshTraces}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.automation.trace.download_trace"
)}
.path=${mdiDownload}
.disabled=${!this._trace}
@click=${this._downloadTrace}
></ha-icon-button>
`;
return html`
${devButtons}
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automations}
>
${this.narrow
? html`<span slot="header">${title}</span>
<div slot="toolbar-icon">${actionButtons}</div>`
<hass-subpage .hass=${this.hass} .narrow=${this.narrow} .header=${title}>
${!this.narrow && stateObj?.attributes.id
? html`
<a
class="trace-link"
href="/config/automation/edit/${stateObj.attributes.id}"
slot="toolbar-icon"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.automation.trace.edit_automation"
)}
</mwc-button>
</a>
`
: ""}
<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize(
"ui.panel.config.automation.trace.refresh"
)}
.path=${mdiRefresh}
@click=${this._refreshTraces}
></ha-icon-button>
<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize(
"ui.panel.config.automation.trace.download_trace"
)}
.path=${mdiDownload}
.disabled=${!this._trace}
@click=${this._downloadTrace}
></ha-icon-button>
<div class="toolbar">
${!this.narrow
? html`<div>
${title}
<a
class="linkButton"
href="/config/automation/edit/${this.automationId}"
>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.edit_automation"
)}
.path=${mdiPencil}
tabindex="-1"
></ha-icon-button>
</a>
</div>`
: ""}
${this._traces && this._traces.length > 0
? html`
<div>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.older_trace"
)}
.path=${mdiRayEndArrow}
.disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId}
@click=${this._pickOlderTrace}
></ha-icon-button>
<select .value=${this._runId} @change=${this._pickTrace}>
${repeat(
this._traces,
(trace) => trace.run_id,
(trace) =>
html`<option value=${trace.run_id}>
${formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this.hass.locale
)}
</option>`
)}
</select>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.newer_trace"
)}
.path=${mdiRayStartArrow}
.disabled=${this._traces[0].run_id === this._runId}
@click=${this._pickNewerTrace}
></ha-icon-button>
</div>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.older_trace"
)}
.path=${mdiRayEndArrow}
.disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId}
@click=${this._pickOlderTrace}
></ha-icon-button>
<select .value=${this._runId} @change=${this._pickTrace}>
${repeat(
this._traces,
(trace) => trace.run_id,
(trace) =>
html`<option value=${trace.run_id}>
${formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this.hass.locale
)}
</option>`
)}
</select>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.newer_trace"
)}
.path=${mdiRayStartArrow}
.disabled=${this._traces[0].run_id === this._runId}
@click=${this._pickNewerTrace}
></ha-icon-button>
`
: ""}
${!this.narrow ? html`<div>${actionButtons}</div>` : ""}
</div>
${this._traces === undefined
@ -276,7 +262,7 @@ export class HaAutomationTrace extends LitElement {
</div>
</div>
`}
</hass-tabs-subpage>
</hass-subpage>
`;
}
@ -465,7 +451,7 @@ export class HaAutomationTrace extends LitElement {
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
@ -476,15 +462,6 @@ export class HaAutomationTrace extends LitElement {
box-sizing: border-box;
}
.toolbar > * {
display: flex;
align-items: center;
}
:host([narrow]) .toolbar > * {
display: contents;
}
.main {
height: calc(100% - 56px);
display: flex;
@ -520,6 +497,9 @@ export class HaAutomationTrace extends LitElement {
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}
`,
];
}

View File

@ -4,12 +4,9 @@ import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-alert";
import "../../../components/ha-textarea";
import "../../../components/ha-textfield";
import {
Condition,
ManualAutomationConfig,
@ -215,23 +212,12 @@ export class HaManualAutomationEditor extends LitElement {
ha-card {
overflow: hidden;
}
.link-button-row {
padding: 14px;
}
.description {
margin: 0;
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
}
ha-select,
.max {
margin-top: 16px;
width: 200px;
}
.header {
display: flex;
align-items: center;
@ -247,35 +233,6 @@ export class HaManualAutomationEditor extends LitElement {
.header a {
color: var(--secondary-text-color);
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.card-content {
padding: 16px;
}
.settings-icon {
display: none;
}
@media (min-width: 870px) {
.settings-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
margin-right: 8px;
}
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius);
}
ha-alert {
display: block;
margin-bottom: 16px;

View File

@ -126,3 +126,9 @@ export class HaConfigSection extends LitElement {
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section": HaConfigSection;
}
}

View File

@ -157,6 +157,9 @@ class HaSceneDashboard extends LitElement {
),
action: () => this._activateScene(scene),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(

View File

@ -29,6 +29,7 @@ import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
@ -279,7 +280,12 @@ export class HaSceneEditor extends SubscribeMixin(
>
${this._config
? html`
<div class="container">
<div
class=${classMap({
container: true,
narrow: !this.isWide,
})}
>
<ha-card outlined>
<div class="card-content">
<ha-textfield
@ -954,15 +960,13 @@ export class HaSceneEditor extends SubscribeMixin(
overflow: hidden;
}
.container {
display: flex;
justify-content: center;
margin-top: 24px;
}
.container > * {
padding: 28px 20px 0;
max-width: 1040px;
flex: 1 1 auto;
margin: 0 auto;
}
.narrow.container {
max-width: 640px;
}
.errors {
padding: 20px;
font-weight: bold;

View File

@ -45,13 +45,14 @@ export class HaBlueprintScriptEditor extends LitElement {
protected render() {
const blueprint = this._blueprint;
return html` <ha-config-section vertical .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
return html`
<ha-card
outlined
class="blueprint"
.header=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
)}</span
)}
>
<ha-card outlined>
<div class="blueprint-picker-container">
${this._blueprints
? Object.keys(this._blueprints).length
@ -118,7 +119,7 @@ export class HaBlueprintScriptEditor extends LitElement {
</p>`}`
: ""}
</ha-card>
</ha-config-section>`;
`;
}
private async _getBlueprints() {
@ -173,22 +174,50 @@ export class HaBlueprintScriptEditor extends LitElement {
return [
haStyle,
css`
:host {
display: block;
}
ha-card.blueprint {
margin: 0 auto;
}
.padding {
padding: 16px;
}
.link-button-row {
padding: 14px;
}
.blueprint-picker-container {
padding: 16px;
padding: 0 16px 16px;
}
ha-textfield,
ha-blueprint-picker {
display: block;
}
h3 {
margin: 16px;
}
.introduction {
margin-top: 0;
margin-bottom: 12px;
}
.introduction a {
color: var(--primary-color);
}
p {
margin-bottom: 0;
}
.description {
margin-bottom: 16px;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row ha-textfield,
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
ha-alert {
margin-bottom: 16px;
display: block;
}
`,
];

View File

@ -1,4 +1,3 @@
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import {
mdiCheck,
@ -6,7 +5,10 @@ import {
mdiContentSave,
mdiDelete,
mdiDotsVertical,
mdiHelpCircle,
mdiInformationOutline,
mdiPlay,
mdiSort,
mdiTransitConnection,
} from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -21,6 +23,7 @@ import {
import { property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
@ -38,7 +41,6 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import {
Action,
deleteScript,
getScriptConfig,
getScriptEditorInitData,
@ -59,6 +61,8 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id";
import "./blueprint-script-editor";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -83,7 +87,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@state() private _mode: "gui" | "yaml" = "gui";
@query("ha-yaml-editor", true) private _editor?: HaYamlEditor;
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
@query("manual-script-editor")
private _manualEditor?: HaManualScriptEditor;
private _schema = memoizeOne(
(
@ -175,23 +182,90 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
.backCallback=${this._backTapped}
.header=${!this._config?.alias ? "" : this._config.alias}
>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleMenuAction}
activatable
>
${this.scriptEntityId && !this.narrow
? html`
<a
class="trace-link"
href="/config/script/trace/${this.scriptEntityId}"
slot="toolbar-icon"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.script.editor.show_trace"
)}
</mwc-button>
</a>
`
: ""}
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item
graphic="icon"
.disabled=${!this.scriptEntityId}
@click=${this._showInfo}
>
${this.hass.localize("ui.panel.config.script.editor.show_info")}
<ha-svg-icon
slot="graphic"
.path=${mdiInformationOutline}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
graphic="icon"
.disabled=${!this.scriptEntityId}
@click=${this._runScript}
>
${this.hass.localize("ui.panel.config.script.picker.run_script")}
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
</mwc-list-item>
${this.scriptEntityId && this.narrow
? html`
<a href="/config/script/trace/${this.scriptEntityId}">
<mwc-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.script.editor.show_trace"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiTransitConnection}
></ha-svg-icon>
</mwc-list-item>
</a>
`
: ""}
${this._config && !("use_blueprint" in this._config)
? html`
<mwc-list-item
aria-label=${this.hass.localize(
"ui.panel.config.automation.editor.re_order"
)}
graphic="icon"
.disabled=${this._mode !== "gui"}
@click=${this._toggleReOrderMode}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order"
)}
<ha-svg-icon slot="graphic" .path=${mdiSort}></ha-svg-icon>
</mwc-list-item>
`
: ""}
<li divider role="separator"></li>
<mwc-list-item
aria-label=${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)}
graphic="icon"
@click=${this._switchUiMode}
>
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${this._mode === "gui"
@ -209,6 +283,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
"ui.panel.config.automation.editor.edit_yaml"
)}
graphic="icon"
@click=${this._switchYamlMode}
>
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml"
@ -230,6 +305,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
"ui.panel.config.script.picker.duplicate"
)}
graphic="icon"
@click=${this._duplicate}
>
${this.hass.localize("ui.panel.config.script.picker.duplicate")}
<ha-svg-icon
@ -245,6 +321,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
)}
class=${classMap({ warning: Boolean(this.scriptEntityId) })}
graphic="icon"
@click=${this._deleteConfirm}
>
${this.hass.localize("ui.panel.config.script.picker.delete")}
<ha-svg-icon
@ -270,47 +347,20 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
>
${this._config
? html`
<ha-card outlined>
<div class="card-content">
<ha-form
.schema=${schema}
.data=${data}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
${this.scriptEntityId
? html`
<div
class="card-actions layout horizontal justified center"
>
<a
href="/config/script/trace/${this
.scriptEntityId}"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.script.editor.show_trace"
)}
</mwc-button>
</a>
<mwc-button
@click=${this._runScript}
title=${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
?disabled=${this._dirty}
>
${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
</mwc-button>
</div>
`
: ``}
</ha-card>
<div class="config-container">
<ha-card outlined>
<div class="card-content">
<ha-form
.schema=${schema}
.data=${data}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
</ha-card>
</div>
${"use_blueprint" in this._config
? html`
@ -323,36 +373,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
></blueprint-script-editor>
`
: html`
<div class="header">
<h2 id="sequence-heading" class="name">
${this.hass.localize(
"ui.panel.config.script.editor.sequence"
)}
</h2>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
.label=${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
></ha-icon-button>
</a>
</div>
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged}
<manual-script-editor
.hass=${this.hass}
></ha-automation-action>
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
@value-changed=${this._configChanged}
></manual-script-editor>
`}
`
: ""}
@ -360,28 +387,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
`
: this._mode === "yaml"
? html`
${!this.narrow
? html`
<ha-card outlined>
<div class="card-header">${this._config?.alias}</div>
<div
class="card-actions layout horizontal justified center"
>
<mwc-button
@click=${this._runScript}
title=${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
?disabled=${this._dirty}
>
${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
</mwc-button>
</div>
</ha-card>
`
: ``}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
@ -518,6 +523,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
return undefined;
};
private async _showInfo() {
if (!this.scriptEntityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId });
}
private async _runScript(ev: CustomEvent) {
ev.stopPropagation();
await triggerScript(this.hass, this.scriptEntityId as string);
@ -625,22 +637,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
this._dirty = true;
}
private _sequenceChanged(ev: CustomEvent): void {
this._config = {
...this._config!,
sequence: ev.detail.value as Action[],
};
this._errors = undefined;
this._dirty = true;
}
private _preprocessYaml() {
return this._config;
}
private async _copyYaml(): Promise<void> {
if (this._editor?.yaml) {
await copyToClipboard(this._editor.yaml);
if (this._yamlEditor?.yaml) {
await copyToClipboard(this._yamlEditor.yaml);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@ -715,20 +718,17 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
history.back();
}
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._mode = "gui";
break;
case 1:
this._mode = "yaml";
break;
case 2:
this._duplicate();
break;
case 3:
this._deleteConfirm();
break;
private _switchUiMode() {
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private _toggleReOrderMode() {
if (this._manualEditor) {
this._manualEditor.reOrderMode = !this._manualEditor.reOrderMode;
}
}
@ -787,15 +787,19 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
font-weight: bold;
color: var(--error-color);
}
.content {
padding: 16px 16px 20px;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
.config-container,
manual-script-editor,
blueprint-script-editor {
margin: 0 auto;
max-width: 1040px;
padding: 28px 20px 0;
}
ha-yaml-editor {
flex-grow: 1;
--code-mirror-height: 100%;
@ -836,6 +840,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
.header a {
color: var(--secondary-text-color);
}
.trace-link {
text-decoration: none;
}
ha-button-menu a {
text-decoration: none;
color: var(--primary-color);
}
`,
];
}

View File

@ -155,6 +155,9 @@ class HaScriptPicker extends LitElement {
),
action: () => this._showTrace(script),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
@ -290,16 +293,31 @@ class HaScriptPicker extends LitElement {
}
private async _duplicate(script: any) {
const config = await getScriptConfig(
this.hass,
computeObjectId(script.entity_id)
);
showScriptEditor({
...config,
alias: `${config?.alias} (${this.hass.localize(
"ui.panel.config.script.picker.duplicate"
)})`,
});
try {
const config = await getScriptConfig(
this.hass,
computeObjectId(script.entity_id)
);
showScriptEditor({
...config,
alias: `${config?.alias} (${this.hass.localize(
"ui.panel.config.script.picker.duplicate"
)})`,
});
} catch (err: any) {
await showAlertDialog(this, {
text:
err.status_code === 404
? this.hass.localize(
"ui.panel.config.script.editor.load_error_not_duplicable"
)
: this.hass.localize(
"ui.panel.config.script.editor.load_error_unknown",
"err_no",
err.status_code
),
});
}
}
private async _deleteConfirm(script: any) {
@ -312,7 +330,22 @@ class HaScriptPicker extends LitElement {
}
private async _delete(script: any) {
await deleteScript(this.hass, computeObjectId(script.entity_id));
try {
await deleteScript(this.hass, computeObjectId(script.entity_id));
} catch (err: any) {
await showAlertDialog(this, {
text:
err.status_code === 400
? this.hass.localize(
"ui.panel.config.script.editor.load_error_not_deletable"
)
: this.hass.localize(
"ui.panel.config.script.editor.load_error_unknown",
"err_no",
err.status_code
),
});
}
}
static get styles(): CSSResultGroup {

View File

@ -1,6 +1,5 @@
import {
mdiDownload,
mdiPencil,
mdiRayEndArrow,
mdiRayStartArrow,
mdiRefresh,
@ -34,7 +33,7 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../../../layouts/hass-subpage";
@customElement("ha-script-trace")
export class HaScriptTrace extends LitElement {
@ -88,81 +87,72 @@ export class HaScriptTrace extends LitElement {
</div>`;
}
const actionButtons = html`
<ha-icon-button
label="Refresh"
@click=${this._refreshTraces}
.path=${mdiRefresh}
></ha-icon-button>
<ha-icon-button
.disabled=${!this._trace}
label="Download Trace"
@click=${this._downloadTrace}
.path=${mdiDownload}
></ha-icon-button>
`;
return html`
${devButtons}
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automations}
>
${this.narrow
? html`<span slot="header"> ${title} </span>
<div slot="toolbar-icon">${actionButtons}</div>`
<hass-subpage .hass=${this.hass} .narrow=${this.narrow} .header=${title}>
${!this.narrow && this.scriptEntityId
? html`
<a
class="trace-link"
href="/config/script/edit/${this.scriptEntityId}"
slot="toolbar-icon"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.script.trace.edit_script"
)}
</mwc-button>
</a>
`
: ""}
<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize(
"ui.panel.config.automation.trace.refresh"
)}
.path=${mdiRefresh}
@click=${this._refreshTraces}
></ha-icon-button>
<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize(
"ui.panel.config.automation.trace.download_trace"
)}
.path=${mdiDownload}
.disabled=${!this._trace}
@click=${this._downloadTrace}
></ha-icon-button>
<div class="toolbar">
${!this.narrow
? html`<div>
${title}
<a
class="linkButton"
href="/config/script/edit/${this.scriptEntityId}"
>
<ha-icon-button
label="Edit Script"
tabindex="-1"
.path=${mdiPencil}
></ha-icon-button>
</a>
</div>`
: ""}
${this._traces && this._traces.length > 0
? html`
<div>
<ha-icon-button
.disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId}
label="Older trace"
@click=${this._pickOlderTrace}
.path=${mdiRayEndArrow}
></ha-icon-button>
<select .value=${this._runId} @change=${this._pickTrace}>
${repeat(
this._traces,
(trace) => trace.run_id,
(trace) =>
html`<option value=${trace.run_id}>
${formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this.hass.locale
)}
</option>`
)}
</select>
<ha-icon-button
.disabled=${this._traces[0].run_id === this._runId}
label="Newer trace"
@click=${this._pickNewerTrace}
.path=${mdiRayStartArrow}
></ha-icon-button>
</div>
<ha-icon-button
.disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId}
label="Older trace"
@click=${this._pickOlderTrace}
.path=${mdiRayEndArrow}
></ha-icon-button>
<select .value=${this._runId} @change=${this._pickTrace}>
${repeat(
this._traces,
(trace) => trace.run_id,
(trace) =>
html`<option value=${trace.run_id}>
${formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this.hass.locale
)}
</option>`
)}
</select>
<ha-icon-button
.disabled=${this._traces[0].run_id === this._runId}
label="Newer trace"
@click=${this._pickNewerTrace}
.path=${mdiRayStartArrow}
></ha-icon-button>
`
: ""}
${!this.narrow ? html`<div>${actionButtons}</div>` : ""}
</div>
${this._traces === undefined
@ -266,7 +256,7 @@ export class HaScriptTrace extends LitElement {
</div>
</div>
`}
</hass-tabs-subpage>
</hass-subpage>
`;
}
@ -447,26 +437,14 @@ export class HaScriptTrace extends LitElement {
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 20px;
justify-content: center;
height: var(--header-height);
padding: 0 16px;
background-color: var(--primary-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
.toolbar > * {
display: flex;
align-items: center;
}
:host([narrow]) .toolbar > * {
display: contents;
}
.main {
height: calc(100% - 56px);
display: flex;
@ -499,6 +477,9 @@ export class HaScriptTrace extends LitElement {
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}
`,
];
}

View File

@ -0,0 +1,135 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import { Action, ScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../automation/action/ha-automation-action";
@customElement("manual-script-editor")
export class HaManualScriptEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public config!: ScriptConfig;
@property({ type: Boolean, reflect: true, attribute: "re-order-mode" })
public reOrderMode = false;
protected render() {
return html`
${this.reOrderMode
? html`
<ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description"
)}
<mwc-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</mwc-button>
</ha-alert>
`
: ""}
<div class="header">
<h2 id="sequence-heading" class="name">
${this.hass.localize("ui.panel.config.script.editor.sequence")}
</h2>
<a
href=${documentationUrl(this.hass, "/docs/scripts/")}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
.label=${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
></ha-icon-button>
</a>
</div>
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this.config.sequence}
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.reOrderMode=${this.reOrderMode}
></ha-automation-action>
`;
}
private _sequenceChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.config!, sequence: ev.detail.value as Action[] },
});
}
private _exitReOrderMode() {
this.reOrderMode = !this.reOrderMode;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
ha-card {
overflow: hidden;
}
.description {
margin: 0;
}
p {
margin-bottom: 0;
}
.header {
display: flex;
align-items: center;
}
.header:first-child {
margin-top: -16px;
}
.header .name {
font-size: 20px;
font-weight: 400;
flex: 1;
}
.header a {
color: var(--secondary-text-color);
}
ha-alert {
display: block;
margin-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"manual-script-editor": HaManualScriptEditor;
}
}

View File

@ -1828,6 +1828,8 @@
"default_name": "New Automation",
"missing_name": "Cannot save automation without a name",
"load_error_not_editable": "Only automations in automations.yaml are editable.",
"load_error_not_duplicable": "Only automations in automations.yaml can be duplicated.",
"load_error_not_deletable": "Only automations in automations.yaml can be deleted.",
"load_error_unknown": "Error loading automation ({err_no}).",
"save": "Save",
"unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?",
@ -2270,7 +2272,7 @@
"no_scripts": "We couldn't find any scripts",
"add_script": "Add script",
"run_script": "Run script",
"run": "[%key:ui::panel::config::automation::editor::run%]",
"run": "[%key:ui::panel::config::automation::editor::actions::run%]",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"headers": {
@ -2288,6 +2290,7 @@
"id_already_exists": "This ID already exists",
"introduction": "Use scripts to run a sequence of actions.",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"header": "Script: {name}",
"default_name": "New Script",
"modes": {
@ -2303,13 +2306,16 @@
"parallel": "Max number of parallel runs"
},
"load_error_not_editable": "Only scripts inside scripts.yaml are editable.",
"load_error_not_duplicable": "Only scripts in scripts.yaml can be duplicated.",
"load_error_not_deletable": "Only scripts in scripts.yaml can be deleted.",
"load_error_unknown": "Error loading script ({err_no}).",
"delete_confirm": "Are you sure you want to delete this script?",
"save_script": "Save script",
"sequence": "Sequence",
"sequence_sentence": "The sequence of actions of this script.",
"link_available_actions": "Learn more about available actions."
}
},
"trace": { "edit_script": "Edit script" }
},
"scene": {
"caption": "Scenes",