Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for a new scene editor #20094

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 110 additions & 23 deletions src/panels/config/scene/ha-scene-editor.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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),
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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");
}
}
}

Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -1003,7 +1090,7 @@ export class HaSceneEditor extends SubscribeMixin(
}
div[slot="meta"] {
display: flex;
justify-content: center;
justify-content: right;
align-items: center;
}
`,
Expand Down
2 changes: 2 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading