New scene editor

This commit is contained in:
karwosts 2024-03-15 07:14:00 -07:00
parent 56a23c5c3d
commit 4fd5eade0a
2 changed files with 112 additions and 23 deletions

View File

@ -1,10 +1,12 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list";
import {
mdiCamera,
mdiContentDuplicate,
mdiContentSave,
mdiDelete,
mdiDotsVertical,
mdiPalette,
} from "@mdi/js";
import { HassEvent } from "home-assistant-js-websocket";
import {
@ -18,6 +20,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { deepEqual } from "../../../common/util/deep-equal";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@ -125,10 +128,10 @@ export class HaSceneEditor extends SubscribeMixin(
private _deviceEntityLookup: DeviceEntitiesLookup = {};
private _activateContextId?: string;
@state() private _saving = false;
@state() private _dirtyEntities: Set<string> = new Set();
// undefined means not set in this session
// null means picked nothing.
@state() private _updatedAreaId?: string | null;
@ -236,6 +239,14 @@ export class HaSceneEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
${this.hass.localize("ui.panel.config.scene.picker.activate")}
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
<ha-list-item .disabled=${!this._dirtyEntities.size} graphic="icon">
${this.hass.localize("ui.panel.config.scene.editor.capture_all")}
<ha-svg-icon slot="graphic" .path=${mdiCamera}></ha-svg-icon>
</ha-list-item>
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
${this.hass.localize(
"ui.panel.config.scene.picker.duplicate_scene"
@ -334,6 +345,18 @@ export class HaSceneEditor extends SubscribeMixin(
.device=${device.id}
@click=${this._deleteDevice}
></ha-icon-button>
${device.entities.some((entityId) =>
this._dirtyEntities.has(entityId)
)
? html` <ha-icon-button
.path=${mdiCamera}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.capture"
)}
.device=${device.id}
@click=${this._handleCaptureDevice}
></ha-icon-button>`
: nothing}
</h1>
<mwc-list>
${device.entities.map((entityId) => {
@ -423,6 +446,17 @@ export class HaSceneEditor extends SubscribeMixin(
></state-badge>
${computeStateName(entityStateObj)}
<div slot="meta">
${this._dirtyEntities.has(entityId)
? html` <ha-icon-button
.path=${mdiCamera}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.capture"
)}
.entityId=${entityId}
@click=${this
._handleCaptureEntity}
></ha-icon-button>`
: nothing}
<ha-icon-button
.path=${mdiDelete}
.entityId=${entityId}
@ -549,9 +583,15 @@ export class HaSceneEditor extends SubscribeMixin(
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._duplicate();
activateScene(this.hass, this._scene!.entity_id);
break;
case 1:
this._entities.forEach((entity) => this._captureEntity(entity));
break;
case 2:
this._duplicate();
break;
case 3:
this._deleteTapped();
break;
}
@ -565,8 +605,9 @@ export class HaSceneEditor extends SubscribeMixin(
return;
}
this._scene = scene;
const { context } = await activateScene(this.hass, this._scene.entity_id);
this._activateContextId = context.id;
if (this._unsubscribeEvents) {
this._unsubscribeEvents();
}
this._unsubscribeEvents =
await this.hass!.connection.subscribeEvents<HassEvent>(
(event) => this._stateChanged(event),
@ -614,6 +655,13 @@ export class HaSceneEditor extends SubscribeMixin(
private _initEntities(config: SceneConfig) {
this._entities = Object.keys(config.entities);
this._entities.forEach((entity) => this._storeState(entity));
this._dirtyEntities = new Set(
this._entities.filter((entity) => {
const currState = this._getCurrentState(entity);
const configState = config.entities[entity];
return !deepEqual(currState, configState);
})
);
this._single_entities = [];
const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) =>
@ -657,9 +705,29 @@ export class HaSceneEditor extends SubscribeMixin(
this._entities = [...this._entities, entityId];
this._single_entities.push(entityId);
this._storeState(entityId);
const currentState = this._getCurrentState(entityId);
if (currentState) {
this._config!.entities[entityId] = currentState;
}
this._dirty = true;
}
private _handleCaptureEntity(ev: Event) {
ev.stopPropagation();
const entityId = (ev.target as any).entityId;
this._captureEntity(entityId);
}
private _captureEntity(entityId: string) {
this._dirtyEntities.delete(entityId);
this.requestUpdate("_dirtyEntities");
this._dirty = true;
const currentState = this._getCurrentState(entityId);
if (currentState) {
this._config!.entities[entityId] = currentState;
}
}
private _deleteEntity(ev: Event) {
ev.stopPropagation();
const deleteEntityId = (ev.target as any).entityId;
@ -669,6 +737,10 @@ export class HaSceneEditor extends SubscribeMixin(
this._single_entities = this._single_entities.filter(
(entityId) => entityId !== deleteEntityId
);
delete this._config!.entities[deleteEntityId];
if (this._config!.metadata) {
delete this._config!.metadata[deleteEntityId];
}
this._dirty = true;
}
@ -684,6 +756,10 @@ export class HaSceneEditor extends SubscribeMixin(
this._entities = [...this._entities, ...deviceEntities];
deviceEntities.forEach((entityId) => {
this._storeState(entityId);
const currentState = this._getCurrentState(entityId);
if (currentState) {
this._config!.entities[entityId] = currentState;
}
});
this._dirty = true;
}
@ -694,6 +770,14 @@ export class HaSceneEditor extends SubscribeMixin(
this._pickDevice(device);
}
private _handleCaptureDevice(ev: Event) {
ev.stopPropagation();
const deviceId = (ev.target as any).device;
this._deviceEntityLookup[deviceId].forEach((entityId) =>
this._captureEntity(entityId)
);
}
private _deleteDevice(ev: Event) {
const deviceId = (ev.target as any).device;
this._devices = this._devices.filter((device) => device !== deviceId);
@ -704,6 +788,12 @@ export class HaSceneEditor extends SubscribeMixin(
this._entities = this._entities.filter(
(entityId) => !deviceEntities.includes(entityId)
);
deviceEntities.forEach((entity) => {
delete this._config!.entities[entity];
if (this._config!.metadata) {
delete this._config!.metadata[entity];
}
});
this._dirty = true;
}
@ -746,11 +836,20 @@ export class HaSceneEditor extends SubscribeMixin(
}
private _stateChanged(event: HassEvent) {
if (
event.context.id !== this._activateContextId &&
this._entities.includes(event.data.entity_id)
) {
this._dirty = true;
const data = event.data;
if (this._entities.includes(data.entity_id)) {
const newState = {
...data.new_state.attributes,
state: data.new_state.state,
};
const configState = this._config!.entities[data.entity_id];
if (deepEqual(newState, configState)) {
this._dirtyEntities.delete(data.entity_id);
this.requestUpdate("_dirtyEntities");
} else if (!this._dirtyEntities.has(data.entity_id)) {
this._dirtyEntities.add(data.entity_id);
this.requestUpdate("_dirtyEntities");
}
}
}
@ -839,17 +938,6 @@ export class HaSceneEditor extends SubscribeMixin(
return output;
}
private _calculateStates(): SceneEntities {
const output: SceneEntities = {};
this._entities.forEach((entityId) => {
const entityState = this._getCurrentState(entityId);
if (entityState) {
output[entityId] = entityState;
}
});
return output;
}
private _storeState(entityId: string): void {
if (entityId in this._storedStates) {
return;
@ -873,7 +961,6 @@ export class HaSceneEditor extends SubscribeMixin(
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
this._config = {
...this._config!,
entities: this._calculateStates(),
metadata: this._calculateMetaData(),
};
try {
@ -1003,7 +1090,7 @@ export class HaSceneEditor extends SubscribeMixin(
}
div[slot="meta"] {
display: flex;
justify-content: center;
justify-content: right;
align-items: center;
}
`,

View File

@ -3413,6 +3413,8 @@
},
"editor": {
"default_name": "New scene",
"capture": "Capture",
"capture_all": "Capture all",
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
"load_error_unknown": "Error loading scene ({err_no}).",
"save": "Save",