From 4fd5eade0a821fd6613b40dfca31078a1f86aa9f Mon Sep 17 00:00:00 2001 From: karwosts Date: Fri, 15 Mar 2024 07:14:00 -0700 Subject: [PATCH] New scene editor --- src/panels/config/scene/ha-scene-editor.ts | 133 +++++++++++++++++---- src/translations/en.json | 2 + 2 files changed, 112 insertions(+), 23 deletions(-) diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 53ce39942a21..d3abebdf1046 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -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 = 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} > + + ${this.hass.localize("ui.panel.config.scene.picker.activate")} + + + + ${this.hass.localize("ui.panel.config.scene.editor.capture_all")} + + ${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} > + ${device.entities.some((entityId) => + this._dirtyEntities.has(entityId) + ) + ? html` ` + : nothing} ${device.entities.map((entityId) => { @@ -423,6 +446,17 @@ export class HaSceneEditor extends SubscribeMixin( > ${computeStateName(entityStateObj)}
+ ${this._dirtyEntities.has(entityId) + ? html` ` + : nothing} ) { 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( (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,7 +705,27 @@ 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) { @@ -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; } `, diff --git a/src/translations/en.json b/src/translations/en.json index fe9874df1155..1769ce5d4139 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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",