mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-16 13:56:35 +00:00
Update Scene Editor (#22847)
* New scene editor design * bugfixes * reorder overflow menu, change label * category support
This commit is contained in:
parent
b8e2298cdd
commit
37d2c6844a
@ -338,6 +338,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
narrow
|
narrow
|
||||||
.items=${[
|
.items=${[
|
||||||
|
{
|
||||||
|
path: mdiPlay,
|
||||||
|
label: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.apply"
|
||||||
|
),
|
||||||
|
action: () => this._activateScene(scene),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: mdiInformationOutline,
|
path: mdiInformationOutline,
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
@ -352,13 +359,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
),
|
),
|
||||||
action: () => this._openSettings(scene),
|
action: () => this._openSettings(scene),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: mdiPlay,
|
|
||||||
label: this.hass.localize(
|
|
||||||
"ui.panel.config.scene.picker.activate"
|
|
||||||
),
|
|
||||||
action: () => this._activateScene(scene),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: mdiTag,
|
path: mdiTag,
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import {
|
import {
|
||||||
|
mdiCheck,
|
||||||
|
mdiCog,
|
||||||
mdiContentDuplicate,
|
mdiContentDuplicate,
|
||||||
mdiContentSave,
|
mdiContentSave,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
|
mdiInformationOutline,
|
||||||
|
mdiPlay,
|
||||||
|
mdiTag,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { HassEvent } from "home-assistant-js-websocket";
|
import type { HassEvent } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
@ -18,10 +23,14 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
|
|||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
import { afterNextRender } from "../../../common/util/render-status";
|
import { afterNextRender } from "../../../common/util/render-status";
|
||||||
|
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||||
|
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||||
import "../../../components/device/ha-device-picker";
|
import "../../../components/device/ha-device-picker";
|
||||||
import "../../../components/entity/ha-entities-picker";
|
import "../../../components/entity/ha-entities-picker";
|
||||||
import "../../../components/ha-area-picker";
|
import "../../../components/ha-area-picker";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
@ -99,6 +108,8 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
@state() private _errors?: string;
|
@state() private _errors?: string;
|
||||||
|
|
||||||
|
@state() private _yamlErrors?: string;
|
||||||
|
|
||||||
@state() private _config?: SceneConfig;
|
@state() private _config?: SceneConfig;
|
||||||
|
|
||||||
@state() private _entities: string[] = [];
|
@state() private _entities: string[] = [];
|
||||||
@ -115,6 +126,8 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
@state() private _scene?: SceneEntity;
|
@state() private _scene?: SceneEntity;
|
||||||
|
|
||||||
|
@state() private _mode: "review" | "live" | "yaml" = "review";
|
||||||
|
|
||||||
private _storedStates: SceneEntities = {};
|
private _storedStates: SceneEntities = {};
|
||||||
|
|
||||||
private _unsubscribeEvents?: () => void;
|
private _unsubscribeEvents?: () => void;
|
||||||
@ -139,6 +152,16 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _getCategory = memoizeOne(
|
||||||
|
(entries: EntityRegistryEntry[], entity_id: string | undefined) => {
|
||||||
|
if (!entity_id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const entry = entries.find((ent) => ent.entity_id === entity_id);
|
||||||
|
return entry?.categories?.scene;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _getEntitiesDevices = memoizeOne(
|
private _getEntitiesDevices = memoizeOne(
|
||||||
(
|
(
|
||||||
entities: string[],
|
entities: string[],
|
||||||
@ -204,13 +227,6 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const { devices, entities } = this._getEntitiesDevices(
|
|
||||||
this._entities,
|
|
||||||
this._devices,
|
|
||||||
this._deviceEntityLookup,
|
|
||||||
this._deviceRegistryEntries
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -232,6 +248,72 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
.path=${mdiDotsVertical}
|
.path=${mdiDotsVertical}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
|
||||||
|
<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${!this.sceneId || this._mode === "live"}
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.panel.config.scene.picker.apply")}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiPlay}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||||
|
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiInformationOutline}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.picker.show_settings"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCog}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
|
||||||
|
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}`
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiTag}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
|
||||||
|
<li divider role="separator"></li>
|
||||||
|
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
|
||||||
|
${this._mode !== "yaml"
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
|
||||||
|
${this._mode === "yaml"
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-list-item>
|
||||||
|
|
||||||
|
<li divider role="separator"></li>
|
||||||
|
|
||||||
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
|
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.scene.picker.duplicate_scene"
|
"ui.panel.config.scene.picker.duplicate_scene"
|
||||||
@ -257,208 +339,7 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
|
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
|
||||||
<div
|
${this._mode === "yaml" ? this.renderYamlMode() : this.renderUiMode()}
|
||||||
id="root"
|
|
||||||
class=${classMap({
|
|
||||||
rtl: computeRTL(this.hass),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${this._config
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
class=${classMap({
|
|
||||||
container: true,
|
|
||||||
narrow: !this.isWide,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ha-card outlined>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-textfield
|
|
||||||
.value=${this._config.name}
|
|
||||||
.name=${"name"}
|
|
||||||
@change=${this._valueChanged}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.name"
|
|
||||||
)}
|
|
||||||
></ha-textfield>
|
|
||||||
<ha-icon-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.icon"
|
|
||||||
)}
|
|
||||||
.name=${"icon"}
|
|
||||||
.value=${this._config.icon}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
>
|
|
||||||
</ha-icon-picker>
|
|
||||||
<ha-area-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.area"
|
|
||||||
)}
|
|
||||||
.name=${"area"}
|
|
||||||
.value=${this._sceneAreaIdWithUpdates || ""}
|
|
||||||
@value-changed=${this._areaChanged}
|
|
||||||
>
|
|
||||||
</ha-area-picker>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ha-config-section vertical .isWide=${this.isWide}>
|
|
||||||
<div slot="header">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.devices.header"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div slot="introduction">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.devices.introduction"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${devices.map(
|
|
||||||
(device) => html`
|
|
||||||
<ha-card outlined>
|
|
||||||
<h1 class="card-header">
|
|
||||||
${device.name}
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiDelete}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.devices.delete"
|
|
||||||
)}
|
|
||||||
.device=${device.id}
|
|
||||||
@click=${this._deleteDevice}
|
|
||||||
></ha-icon-button>
|
|
||||||
</h1>
|
|
||||||
<mwc-list>
|
|
||||||
${device.entities.map((entityId) => {
|
|
||||||
const entityStateObj = this.hass.states[entityId];
|
|
||||||
if (!entityStateObj) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<ha-list-item
|
|
||||||
hasMeta
|
|
||||||
graphic="icon"
|
|
||||||
.entityId=${entityId}
|
|
||||||
@click=${this._showMoreInfo}
|
|
||||||
>
|
|
||||||
<state-badge
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${entityStateObj}
|
|
||||||
slot="graphic"
|
|
||||||
></state-badge>
|
|
||||||
${computeStateName(entityStateObj)}
|
|
||||||
</ha-list-item>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</mwc-list>
|
|
||||||
</ha-card>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ha-card
|
|
||||||
outlined
|
|
||||||
.header=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.devices.add"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-device-picker
|
|
||||||
@value-changed=${this._devicePicked}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.devices.add"
|
|
||||||
)}
|
|
||||||
></ha-device-picker>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</ha-config-section>
|
|
||||||
|
|
||||||
${this.showAdvanced
|
|
||||||
? html`
|
|
||||||
<ha-config-section vertical .isWide=${this.isWide}>
|
|
||||||
<div slot="header">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.entities.header"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div slot="introduction">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.entities.introduction"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
${entities.length
|
|
||||||
? html`
|
|
||||||
<ha-card
|
|
||||||
outlined
|
|
||||||
class="entities"
|
|
||||||
.header=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.entities.without_device"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<mwc-list>
|
|
||||||
${entities.map((entityId) => {
|
|
||||||
const entityStateObj =
|
|
||||||
this.hass.states[entityId];
|
|
||||||
if (!entityStateObj) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<ha-list-item
|
|
||||||
hasMeta
|
|
||||||
graphic="icon"
|
|
||||||
.entityId=${entityId}
|
|
||||||
@click=${this._showMoreInfo}
|
|
||||||
>
|
|
||||||
<state-badge
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${entityStateObj}
|
|
||||||
slot="graphic"
|
|
||||||
></state-badge>
|
|
||||||
${computeStateName(entityStateObj)}
|
|
||||||
<div slot="meta">
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiDelete}
|
|
||||||
.entityId=${entityId}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.entities.delete"
|
|
||||||
)}
|
|
||||||
@click=${this._deleteEntity}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
</ha-list-item>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</mwc-list>
|
|
||||||
</ha-card>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
|
|
||||||
<ha-card
|
|
||||||
outlined
|
|
||||||
header=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.entities.add"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-entity-picker
|
|
||||||
@value-changed=${this._entityPicked}
|
|
||||||
.excludeDomains=${SCENE_IGNORED_DOMAINS}
|
|
||||||
.hass=${this.hass}
|
|
||||||
label=${this.hass.localize(
|
|
||||||
"ui.panel.config.scene.editor.entities.add"
|
|
||||||
)}
|
|
||||||
></ha-entity-picker>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</ha-config-section>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<ha-fab
|
<ha-fab
|
||||||
slot="fab"
|
slot="fab"
|
||||||
.label=${this.hass.localize("ui.panel.config.scene.editor.save")}
|
.label=${this.hass.localize("ui.panel.config.scene.editor.save")}
|
||||||
@ -473,6 +354,270 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderYamlMode() {
|
||||||
|
return html` <ha-yaml-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.defaultValue=${this._config}
|
||||||
|
@value-changed=${this._yamlChanged}
|
||||||
|
></ha-yaml-editor>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderUiMode() {
|
||||||
|
const { devices, entities } = this._getEntitiesDevices(
|
||||||
|
this._entities,
|
||||||
|
this._devices,
|
||||||
|
this._deviceEntityLookup,
|
||||||
|
this._deviceRegistryEntries
|
||||||
|
);
|
||||||
|
return html` <div
|
||||||
|
id="root"
|
||||||
|
class=${classMap({
|
||||||
|
rtl: computeRTL(this.hass),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._config
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
container: true,
|
||||||
|
narrow: !this.isWide,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._mode === "live"
|
||||||
|
? html` <ha-alert
|
||||||
|
alert-type="info"
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.live_preview"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.live_preview_detail"
|
||||||
|
)}
|
||||||
|
<ha-button slot="action" @click=${this._exitLiveMode}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.back_to_review_mode"
|
||||||
|
)}</ha-button
|
||||||
|
>
|
||||||
|
</ha-alert>`
|
||||||
|
: html` <ha-alert
|
||||||
|
alert-type="info"
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.review_mode"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.review_mode_detail"
|
||||||
|
)}
|
||||||
|
<ha-button slot="action" @click=${this._enterLiveMode}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.live_preview"
|
||||||
|
)}</ha-button
|
||||||
|
>
|
||||||
|
</ha-alert>`}
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-textfield
|
||||||
|
.value=${this._config.name}
|
||||||
|
.name=${"name"}
|
||||||
|
@change=${this._valueChanged}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.name"
|
||||||
|
)}
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-icon-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.icon"
|
||||||
|
)}
|
||||||
|
.name=${"icon"}
|
||||||
|
.value=${this._config.icon}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-icon-picker>
|
||||||
|
<ha-area-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.area"
|
||||||
|
)}
|
||||||
|
.name=${"area"}
|
||||||
|
.value=${this._sceneAreaIdWithUpdates || ""}
|
||||||
|
@value-changed=${this._areaChanged}
|
||||||
|
>
|
||||||
|
</ha-area-picker>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
|
<div slot="header">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.devices.header"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${this._mode === "live"
|
||||||
|
? html` <div slot="introduction">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.devices.introduction"
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${devices.map(
|
||||||
|
(device) => html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<h1 class="card-header">
|
||||||
|
${device.name}
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDelete}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.devices.delete"
|
||||||
|
)}
|
||||||
|
.device=${device.id}
|
||||||
|
@click=${this._deleteDevice}
|
||||||
|
></ha-icon-button>
|
||||||
|
</h1>
|
||||||
|
<mwc-list>
|
||||||
|
${device.entities.map((entityId) => {
|
||||||
|
const entityStateObj = this.hass.states[entityId];
|
||||||
|
if (!entityStateObj) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-list-item
|
||||||
|
hasMeta
|
||||||
|
.graphic=${this._mode === "live"
|
||||||
|
? "icon"
|
||||||
|
: undefined}
|
||||||
|
.entityId=${entityId}
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
>
|
||||||
|
${this._mode === "live"
|
||||||
|
? html`
|
||||||
|
<state-badge
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${entityStateObj}
|
||||||
|
slot="graphic"
|
||||||
|
></state-badge>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${computeStateName(entityStateObj)}
|
||||||
|
</ha-list-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</mwc-list>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${this._mode === "live"
|
||||||
|
? html`
|
||||||
|
<ha-card
|
||||||
|
outlined
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.devices.add"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-device-picker
|
||||||
|
@value-changed=${this._devicePicked}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.devices.add"
|
||||||
|
)}
|
||||||
|
></ha-device-picker>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-config-section>
|
||||||
|
|
||||||
|
${this.showAdvanced
|
||||||
|
? html` <ha-config-section vertical .isWide=${this.isWide}>
|
||||||
|
<div slot="header">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.header"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${this._mode === "live"
|
||||||
|
? html` <div slot="introduction">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.introduction"
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${entities.length
|
||||||
|
? html`
|
||||||
|
<ha-card
|
||||||
|
outlined
|
||||||
|
class="entities"
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.without_device"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<mwc-list>
|
||||||
|
${entities.map((entityId) => {
|
||||||
|
const entityStateObj = this.hass.states[entityId];
|
||||||
|
if (!entityStateObj) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-list-item
|
||||||
|
hasMeta
|
||||||
|
.graphic=${this._mode === "live"
|
||||||
|
? "icon"
|
||||||
|
: undefined}
|
||||||
|
.entityId=${entityId}
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
>
|
||||||
|
${this._mode === "live"
|
||||||
|
? html` <state-badge
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${entityStateObj}
|
||||||
|
slot="graphic"
|
||||||
|
></state-badge>`
|
||||||
|
: nothing}
|
||||||
|
${computeStateName(entityStateObj)}
|
||||||
|
<div slot="meta">
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDelete}
|
||||||
|
.entityId=${entityId}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.delete"
|
||||||
|
)}
|
||||||
|
@click=${this._deleteEntity}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
</ha-list-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</mwc-list>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._mode === "live"
|
||||||
|
? html` <ha-card
|
||||||
|
outlined
|
||||||
|
header=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.add"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-entity-picker
|
||||||
|
@value-changed=${this._entityPicked}
|
||||||
|
.excludeDomains=${SCENE_IGNORED_DOMAINS}
|
||||||
|
.hass=${this.hass}
|
||||||
|
label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.add"
|
||||||
|
)}
|
||||||
|
></ha-entity-picker>
|
||||||
|
</div>
|
||||||
|
</ha-card>`
|
||||||
|
: nothing}
|
||||||
|
</ha-config-section>`
|
||||||
|
: nothing}
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
@ -522,47 +667,151 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
|
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
|
||||||
if (
|
if (
|
||||||
this._entities.includes(entity.entity_id) &&
|
this._entities.includes(entity.entity_id) &&
|
||||||
!this._single_entities.includes(entity.device_id) &&
|
!this._single_entities.includes(entity.entity_id) &&
|
||||||
!this._devices.includes(entity.device_id)
|
!this._devices.includes(entity.device_id)
|
||||||
) {
|
) {
|
||||||
this._devices = [...this._devices, entity.device_id];
|
this._devices = [...this._devices, entity.device_id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
changedProps.has("scenes") &&
|
|
||||||
this.sceneId &&
|
|
||||||
this._config &&
|
|
||||||
!this._scene
|
|
||||||
) {
|
|
||||||
this._setScene();
|
|
||||||
}
|
|
||||||
if (this._scenesSet && changedProps.has("scenes")) {
|
if (this._scenesSet && changedProps.has("scenes")) {
|
||||||
this._scenesSet();
|
this._scenesSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
if (this._scene) {
|
||||||
|
if (this.hass.states[this._scene.entity_id] !== this._scene) {
|
||||||
|
this._scene = this.hass.states[this._scene.entity_id];
|
||||||
|
}
|
||||||
|
} else if (this.sceneId) {
|
||||||
|
this._scene = Object.values(this.hass.states).find(
|
||||||
|
(stateObj) =>
|
||||||
|
stateObj.entity_id.startsWith("scene") &&
|
||||||
|
stateObj.attributes?.id === this.sceneId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||||
switch (ev.detail.index) {
|
switch (ev.detail.index) {
|
||||||
case 0:
|
case 0:
|
||||||
this._duplicate();
|
activateScene(this.hass, this._scene!.entity_id);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
|
fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id });
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
showMoreInfoDialog(this, {
|
||||||
|
entityId: this._scene!.entity_id,
|
||||||
|
view: "settings",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this._editCategory(this._scene!);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
if (this._mode === "yaml") {
|
||||||
|
this._initEntities(this._config!);
|
||||||
|
this._exitYamlMode();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
if (this._mode !== "yaml") {
|
||||||
|
this._enterYamlMode();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
this._duplicate();
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
this._deleteTapped();
|
this._deleteTapped();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setScene() {
|
private async _exitYamlMode() {
|
||||||
const scene = this.scenes.find(
|
if (this._yamlErrors) {
|
||||||
(entity: SceneEntity) => entity.attributes.id === this.sceneId
|
const result = await showConfirmationDialog(this, {
|
||||||
);
|
text: html`${this.hass.localize(
|
||||||
if (!scene) {
|
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||||
|
)}<br /><br />${this._yamlErrors}`,
|
||||||
|
confirmText: this.hass!.localize("ui.common.continue"),
|
||||||
|
destructive: true,
|
||||||
|
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._yamlErrors = undefined;
|
||||||
|
this._mode = "review";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _enterYamlMode() {
|
||||||
|
if (this._mode === "live") {
|
||||||
|
this._generateConfigFromLive();
|
||||||
|
if (this._unsubscribeEvents) {
|
||||||
|
this._unsubscribeEvents();
|
||||||
|
this._unsubscribeEvents = undefined;
|
||||||
|
}
|
||||||
|
applyScene(this.hass, this._storedStates);
|
||||||
|
}
|
||||||
|
this._mode = "yaml";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _enterLiveMode() {
|
||||||
|
if (this._dirty) {
|
||||||
|
const result = await showConfirmationDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.enter_live_mode_unsaved"
|
||||||
|
),
|
||||||
|
confirmText: this.hass!.localize("ui.common.continue"),
|
||||||
|
destructive: true,
|
||||||
|
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._entities.forEach((entity) => this._storeState(entity));
|
||||||
|
this._mode = "live";
|
||||||
|
await this._setScene();
|
||||||
|
this._subscribeEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _exitLiveMode() {
|
||||||
|
this._generateConfigFromLive();
|
||||||
|
if (this._unsubscribeEvents) {
|
||||||
|
this._unsubscribeEvents();
|
||||||
|
this._unsubscribeEvents = undefined;
|
||||||
|
}
|
||||||
|
applyScene(this.hass, this._storedStates);
|
||||||
|
this._mode = "review";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _yamlChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._dirty = true;
|
||||||
|
if (!ev.detail.isValid) {
|
||||||
|
this._yamlErrors = ev.detail.errorMsg;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._yamlErrors = undefined;
|
||||||
|
this._config = ev.detail.value;
|
||||||
|
this._errors = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _setScene() {
|
||||||
|
if (!this._scene) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._scene = scene;
|
|
||||||
const { context } = await activateScene(this.hass, this._scene.entity_id);
|
const { context } = await activateScene(this.hass, this._scene.entity_id);
|
||||||
this._activateContextId = context.id;
|
this._activateContextId = context.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _subscribeEvents() {
|
||||||
this._unsubscribeEvents =
|
this._unsubscribeEvents =
|
||||||
await this.hass!.connection.subscribeEvents<HassEvent>(
|
await this.hass!.connection.subscribeEvents<HassEvent>(
|
||||||
(event) => this._stateChanged(event),
|
(event) => this._stateChanged(event),
|
||||||
@ -601,7 +850,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
this._initEntities(config);
|
this._initEntities(config);
|
||||||
|
|
||||||
this._setScene();
|
this._scene = this.scenes.find(
|
||||||
|
(entity: SceneEntity) => entity.attributes.id === this.sceneId
|
||||||
|
);
|
||||||
|
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
this._config = config;
|
this._config = config;
|
||||||
@ -609,7 +860,6 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
private _initEntities(config: SceneConfig) {
|
private _initEntities(config: SceneConfig) {
|
||||||
this._entities = Object.keys(config.entities);
|
this._entities = Object.keys(config.entities);
|
||||||
this._entities.forEach((entity) => this._storeState(entity));
|
|
||||||
this._single_entities = [];
|
this._single_entities = [];
|
||||||
|
|
||||||
const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) =>
|
const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) =>
|
||||||
@ -665,6 +915,12 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
this._single_entities = this._single_entities.filter(
|
this._single_entities = this._single_entities.filter(
|
||||||
(entityId) => entityId !== deleteEntityId
|
(entityId) => entityId !== deleteEntityId
|
||||||
);
|
);
|
||||||
|
if (this._config!.entities) {
|
||||||
|
delete this._config!.entities[deleteEntityId];
|
||||||
|
}
|
||||||
|
if (this._config!.metadata) {
|
||||||
|
delete this._config!.metadata[deleteEntityId];
|
||||||
|
}
|
||||||
this._dirty = true;
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -700,6 +956,11 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
this._entities = this._entities.filter(
|
this._entities = this._entities.filter(
|
||||||
(entityId) => !deviceEntities.includes(entityId)
|
(entityId) => !deviceEntities.includes(entityId)
|
||||||
);
|
);
|
||||||
|
if (this._config!.entities) {
|
||||||
|
deviceEntities.forEach((entityId) => {
|
||||||
|
delete this._config!.entities[entityId];
|
||||||
|
});
|
||||||
|
}
|
||||||
this._dirty = true;
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -758,7 +1019,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _goBack(): void {
|
private _goBack(): void {
|
||||||
applyScene(this.hass, this._storedStates);
|
if (this._mode === "live") {
|
||||||
|
applyScene(this.hass, this._storedStates);
|
||||||
|
}
|
||||||
afterNextRender(() => history.back());
|
afterNextRender(() => history.back());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -780,7 +1043,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
private async _delete(): Promise<void> {
|
private async _delete(): Promise<void> {
|
||||||
await deleteScene(this.hass, this.sceneId!);
|
await deleteScene(this.hass, this.sceneId!);
|
||||||
applyScene(this.hass, this._storedStates);
|
if (this._mode === "live") {
|
||||||
|
applyScene(this.hass, this._storedStates);
|
||||||
|
}
|
||||||
history.back();
|
history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,16 +1130,29 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
return { ...stateObj.attributes, state: stateObj.state };
|
return { ...stateObj.attributes, state: stateObj.state };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _saveScene(): Promise<void> {
|
private _generateConfigFromLive() {
|
||||||
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
|
|
||||||
this._config = {
|
this._config = {
|
||||||
...this._config!,
|
...this._config!,
|
||||||
entities: this._calculateStates(),
|
entities: this._calculateStates(),
|
||||||
metadata: this._calculateMetaData(),
|
metadata: this._calculateMetaData(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveScene(): Promise<void> {
|
||||||
|
if (this._yamlErrors) {
|
||||||
|
showToast(this, {
|
||||||
|
message: this._yamlErrors,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
|
||||||
|
if (this._mode === "live") {
|
||||||
|
this._generateConfigFromLive();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
this._saving = true;
|
this._saving = true;
|
||||||
await saveScene(this.hass, id, this._config);
|
await saveScene(this.hass, id, this._config!);
|
||||||
|
|
||||||
if (this._updatedAreaId !== undefined) {
|
if (this._updatedAreaId !== undefined) {
|
||||||
let scene =
|
let scene =
|
||||||
@ -943,6 +1221,27 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _editCategory(scene: any) {
|
||||||
|
const entityReg = this._entityRegistryEntries.find(
|
||||||
|
(reg) => reg.entity_id === scene.entity_id
|
||||||
|
);
|
||||||
|
if (!entityReg) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.no_category_support"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.no_category_entity_reg"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showAssignCategoryDialog(this, {
|
||||||
|
scope: "scene",
|
||||||
|
entityReg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@ -982,6 +1281,14 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
bottom: calc(-80px - env(safe-area-inset-bottom));
|
bottom: calc(-80px - env(safe-area-inset-bottom));
|
||||||
transition: bottom 0.3s;
|
transition: bottom 0.3s;
|
||||||
}
|
}
|
||||||
|
ha-alert {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
ha-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
--mdc-theme-primary: var(--primary-color);
|
||||||
|
}
|
||||||
ha-fab.dirty {
|
ha-fab.dirty {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
@ -1002,6 +1309,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
li[role="separator"] {
|
||||||
|
border-bottom-color: var(--divider-color);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -3857,6 +3857,7 @@
|
|||||||
"edit_scene": "Edit scene",
|
"edit_scene": "Edit scene",
|
||||||
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
|
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
|
||||||
"activate": "Activate",
|
"activate": "Activate",
|
||||||
|
"apply": "Apply",
|
||||||
"delete_scene": "Delete scene",
|
"delete_scene": "Delete scene",
|
||||||
"delete": "[%key:ui::common::delete%]",
|
"delete": "[%key:ui::common::delete%]",
|
||||||
"delete_confirm_title": "Delete scene?",
|
"delete_confirm_title": "Delete scene?",
|
||||||
@ -3881,6 +3882,12 @@
|
|||||||
"search": "Search {number} scenes"
|
"search": "Search {number} scenes"
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
|
"review_mode": "Review Mode",
|
||||||
|
"review_mode_detail": "You can adjust the scene's details and remove devices or entities. To fully edit, switch to Live Preview, which will apply the scene.",
|
||||||
|
"live_preview": "Live Preview",
|
||||||
|
"live_preview_detail": "In Live Preview, all changes to this scene are applied in real-time to your devices and entities.",
|
||||||
|
"enter_live_mode_unsaved": "You have unsaved changes to this scene. Continuing to live preview will apply the saved scene, which may overwrite your unsaved changes. Consider if you would like to save the scene first before activating it.",
|
||||||
|
"back_to_review_mode": "Back to review mode",
|
||||||
"default_name": "New scene",
|
"default_name": "New scene",
|
||||||
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
|
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
|
||||||
"load_error_unknown": "Error loading scene ({err_no}).",
|
"load_error_unknown": "Error loading scene ({err_no}).",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user