mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add scene editor (#4164)
* Add scene editor * Update ha-config-scene.ts * Update en.json * Update ha-scene-editor.ts * Partial comments * Types * 1 more * Comments * Lint * Update ha-device-picker.ts * Update ha-device-card.ts
This commit is contained in:
parent
2a617a9639
commit
da35c263d2
@ -21,6 +21,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
import {
|
import {
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
subscribeDeviceRegistry,
|
subscribeDeviceRegistry,
|
||||||
|
computeDeviceName,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { compare } from "../../common/string/compare";
|
import { compare } from "../../common/string/compare";
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
@ -33,7 +34,6 @@ import {
|
|||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
} from "../../data/entity_registry";
|
} from "../../data/entity_registry";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
name: string;
|
name: string;
|
||||||
@ -102,11 +102,11 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
const outputDevices = devices.map((device) => {
|
const outputDevices = devices.map((device) => {
|
||||||
return {
|
return {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name:
|
name: computeDeviceName(
|
||||||
device.name_by_user ||
|
device,
|
||||||
device.name ||
|
this.hass,
|
||||||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
|
deviceEntityLookup[device.id]
|
||||||
"No name",
|
),
|
||||||
area: device.area_id ? areaLookup[device.area_id].name : "No area",
|
area: device.area_id ? areaLookup[device.area_id].name : "No area",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -209,20 +209,6 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fallbackDeviceName(
|
|
||||||
deviceId: string,
|
|
||||||
deviceEntityLookup: DeviceEntityLookup
|
|
||||||
): string | undefined {
|
|
||||||
for (const entity of deviceEntityLookup[deviceId] || []) {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (stateObj) {
|
|
||||||
return computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
paper-input > paper-icon-button {
|
paper-input > paper-icon-button {
|
||||||
|
@ -22,7 +22,20 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
class HaEntitiesPickerLight extends LitElement {
|
class HaEntitiesPickerLight extends LitElement {
|
||||||
@property() public hass?: HomeAssistant;
|
@property() public hass?: HomeAssistant;
|
||||||
@property() public value?: string[];
|
@property() public value?: string[];
|
||||||
@property({ attribute: "domain-filter" }) public domainFilter?: string;
|
/**
|
||||||
|
* Show entities from specific domains.
|
||||||
|
* @type {string}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
/**
|
||||||
|
* Show no entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
@property({ attribute: "picked-entity-label" })
|
@property({ attribute: "picked-entity-label" })
|
||||||
public pickedEntityLabel?: string;
|
public pickedEntityLabel?: string;
|
||||||
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
||||||
@ -31,6 +44,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentEntities = this._currentEntities;
|
const currentEntities = this._currentEntities;
|
||||||
return html`
|
return html`
|
||||||
${currentEntities.map(
|
${currentEntities.map(
|
||||||
@ -40,7 +54,8 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
.curValue=${entityId}
|
.curValue=${entityId}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.domainFilter=${this.domainFilter}
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeDomains=${this.excludeDomains}
|
||||||
.entityFilter=${this._entityFilter}
|
.entityFilter=${this._entityFilter}
|
||||||
.value=${entityId}
|
.value=${entityId}
|
||||||
.label=${this.pickedEntityLabel}
|
.label=${this.pickedEntityLabel}
|
||||||
@ -52,7 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
<div>
|
<div>
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.domainFilter=${this.domainFilter}
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeDomains=${this.excludeDomains}
|
||||||
.entityFilter=${this._entityFilter}
|
.entityFilter=${this._entityFilter}
|
||||||
.label=${this.pickEntityLabel}
|
.label=${this.pickEntityLabel}
|
||||||
@value-changed=${this._addEntity}
|
@value-changed=${this._addEntity}
|
||||||
|
@ -60,7 +60,20 @@ class HaEntityPicker extends LitElement {
|
|||||||
@property() public hass?: HomeAssistant;
|
@property() public hass?: HomeAssistant;
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
@property({ attribute: "domain-filter" }) public domainFilter?: string;
|
/**
|
||||||
|
* Show entities from specific domains.
|
||||||
|
* @type {string}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
/**
|
||||||
|
* Show no entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
@property({ type: Boolean }) private _opened?: boolean;
|
@property({ type: Boolean }) private _opened?: boolean;
|
||||||
@property() private _hass?: HomeAssistant;
|
@property() private _hass?: HomeAssistant;
|
||||||
@ -68,7 +81,8 @@ class HaEntityPicker extends LitElement {
|
|||||||
private _getStates = memoizeOne(
|
private _getStates = memoizeOne(
|
||||||
(
|
(
|
||||||
hass: this["hass"],
|
hass: this["hass"],
|
||||||
domainFilter: this["domainFilter"],
|
includeDomains: this["includeDomains"],
|
||||||
|
excludeDomains: this["excludeDomains"],
|
||||||
entityFilter: this["entityFilter"]
|
entityFilter: this["entityFilter"]
|
||||||
) => {
|
) => {
|
||||||
let states: HassEntity[] = [];
|
let states: HassEntity[] = [];
|
||||||
@ -78,9 +92,15 @@ class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
let entityIds = Object.keys(hass.states);
|
let entityIds = Object.keys(hass.states);
|
||||||
|
|
||||||
if (domainFilter) {
|
if (includeDomains) {
|
||||||
|
entityIds = entityIds.filter((eid) =>
|
||||||
|
includeDomains.includes(eid.substr(0, eid.indexOf(".")))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDomains) {
|
||||||
entityIds = entityIds.filter(
|
entityIds = entityIds.filter(
|
||||||
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
|
(eid) => !excludeDomains.includes(eid.substr(0, eid.indexOf(".")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +128,8 @@ class HaEntityPicker extends LitElement {
|
|||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const states = this._getStates(
|
const states = this._getStates(
|
||||||
this._hass,
|
this._hass,
|
||||||
this.domainFilter,
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
this.entityFilter
|
this.entityFilter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { createCollection, Connection } from "home-assistant-js-websocket";
|
import { createCollection, Connection } from "home-assistant-js-websocket";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
|
import { EntityRegistryEntry } from "./entity_registry";
|
||||||
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
|
||||||
export interface DeviceRegistryEntry {
|
export interface DeviceRegistryEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@ -20,6 +22,33 @@ export interface DeviceRegistryEntryMutableParams {
|
|||||||
name_by_user?: string | null;
|
name_by_user?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computeDeviceName = (
|
||||||
|
device: DeviceRegistryEntry,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities?: EntityRegistryEntry[] | string[]
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
device.name_by_user ||
|
||||||
|
device.name ||
|
||||||
|
(entities && fallbackDeviceName(hass, entities)) ||
|
||||||
|
hass.localize("ui.panel.config.devices.unnamed_device")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fallbackDeviceName = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: EntityRegistryEntry[] | string[]
|
||||||
|
) => {
|
||||||
|
for (const entity of entities || []) {
|
||||||
|
const entityId = typeof entity === "string" ? entity : entity.entity_id;
|
||||||
|
const stateObj = hass.states[entityId];
|
||||||
|
if (stateObj) {
|
||||||
|
return computeStateName(stateObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateDeviceRegistryEntry = (
|
export const updateDeviceRegistryEntry = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
|
91
src/data/scene.ts
Normal file
91
src/data/scene.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
HassEntityBase,
|
||||||
|
HassEntityAttributeBase,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
|
|
||||||
|
import { HomeAssistant, ServiceCallResponse } from "../types";
|
||||||
|
|
||||||
|
export const SCENE_IGNORED_DOMAINS = [
|
||||||
|
"sensor",
|
||||||
|
"binary_sensor",
|
||||||
|
"device_tracker",
|
||||||
|
"person",
|
||||||
|
"persistent_notification",
|
||||||
|
"configuration",
|
||||||
|
"image_processing",
|
||||||
|
"sun",
|
||||||
|
"weather",
|
||||||
|
"zone",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SCENE_SAVED_ATTRIBUTES = {
|
||||||
|
light: [
|
||||||
|
"brightness",
|
||||||
|
"color_temp",
|
||||||
|
"effect",
|
||||||
|
"rgb_color",
|
||||||
|
"xy_color",
|
||||||
|
"hs_color",
|
||||||
|
],
|
||||||
|
media_player: [
|
||||||
|
"is_volume_muted",
|
||||||
|
"volume_level",
|
||||||
|
"sound_mode",
|
||||||
|
"source",
|
||||||
|
"media_content_id",
|
||||||
|
"media_content_type",
|
||||||
|
],
|
||||||
|
climate: [
|
||||||
|
"target_temperature",
|
||||||
|
"target_temperature_high",
|
||||||
|
"target_temperature_low",
|
||||||
|
"target_humidity",
|
||||||
|
"fan_mode",
|
||||||
|
"swing_mode",
|
||||||
|
"hvac_mode",
|
||||||
|
"preset_mode",
|
||||||
|
],
|
||||||
|
vacuum: ["cleaning_mode"],
|
||||||
|
fan: ["speed", "current_direction"],
|
||||||
|
water_heather: ["temperature", "operation_mode"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SceneEntity extends HassEntityBase {
|
||||||
|
attributes: HassEntityAttributeBase & { id?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SceneConfig {
|
||||||
|
name: string;
|
||||||
|
entities: SceneEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SceneEntities {
|
||||||
|
[entityId: string]: string | { state: string; [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const activateScene = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string
|
||||||
|
): Promise<ServiceCallResponse> =>
|
||||||
|
hass.callService("scene", "turn_on", { entity_id: entityId });
|
||||||
|
|
||||||
|
export const applyScene = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: SceneEntities
|
||||||
|
): Promise<ServiceCallResponse> =>
|
||||||
|
hass.callService("scene", "apply", { entities });
|
||||||
|
|
||||||
|
export const getSceneConfig = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
sceneId: string
|
||||||
|
): Promise<SceneConfig> =>
|
||||||
|
hass.callApi<SceneConfig>("GET", `config/scene/config/${sceneId}`);
|
||||||
|
|
||||||
|
export const saveScene = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
sceneId: string,
|
||||||
|
config: SceneConfig
|
||||||
|
) => hass.callApi("POST", `config/scene/config/${sceneId}`, config);
|
||||||
|
|
||||||
|
export const deleteScene = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callApi("DELETE", `config/scene/config/${id}`);
|
@ -36,6 +36,7 @@ class DialogConfirmation extends LitElement {
|
|||||||
<ha-paper-dialog
|
<ha-paper-dialog
|
||||||
with-backdrop
|
with-backdrop
|
||||||
opened
|
opened
|
||||||
|
modal
|
||||||
@opened-changed="${this._openedChanged}"
|
@opened-changed="${this._openedChanged}"
|
||||||
>
|
>
|
||||||
<h2>
|
<h2>
|
||||||
@ -48,10 +49,14 @@ class DialogConfirmation extends LitElement {
|
|||||||
</paper-dialog-scrollable>
|
</paper-dialog-scrollable>
|
||||||
<div class="paper-dialog-buttons">
|
<div class="paper-dialog-buttons">
|
||||||
<mwc-button @click="${this._dismiss}">
|
<mwc-button @click="${this._dismiss}">
|
||||||
${this.hass.localize("ui.dialogs.confirmation.cancel")}
|
${this._params.cancelBtnText
|
||||||
|
? this._params.cancelBtnText
|
||||||
|
: this.hass.localize("ui.dialogs.confirmation.cancel")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
<mwc-button @click="${this._confirm}">
|
<mwc-button @click="${this._confirm}">
|
||||||
${this.hass.localize("ui.dialogs.confirmation.ok")}
|
${this._params.confirmBtnText
|
||||||
|
? this._params.confirmBtnText
|
||||||
|
: this.hass.localize("ui.dialogs.confirmation.ok")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-paper-dialog>
|
</ha-paper-dialog>
|
||||||
|
@ -3,6 +3,8 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
export interface ConfirmationDialogParams {
|
export interface ConfirmationDialogParams {
|
||||||
title?: string;
|
title?: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
confirmBtnText?: string;
|
||||||
|
cancelBtnText?: string;
|
||||||
confirm: () => void;
|
confirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
subscribeAreaRegistry,
|
subscribeAreaRegistry,
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
} from "../../data/area_registry";
|
} from "../../data/area_registry";
|
||||||
|
import { computeDeviceName } from "../../data/device_registry";
|
||||||
|
|
||||||
@customElement("dialog-device-registry-detail")
|
@customElement("dialog-device-registry-detail")
|
||||||
class DialogDeviceRegistryDetail extends LitElement {
|
class DialogDeviceRegistryDetail extends LitElement {
|
||||||
@ -75,8 +76,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
@opened-changed="${this._openedChanged}"
|
@opened-changed="${this._openedChanged}"
|
||||||
>
|
>
|
||||||
<h2>
|
<h2>
|
||||||
${device.name ||
|
${computeDeviceName(device, this.hass)}
|
||||||
this.hass.localize("ui.panel.config.devices.unnamed_device")}
|
|
||||||
</h2>
|
</h2>
|
||||||
<paper-dialog-scrollable>
|
<paper-dialog-scrollable>
|
||||||
${this._error
|
${this._error
|
||||||
|
@ -180,6 +180,7 @@ export const provideHass = (
|
|||||||
dockedSidebar: "auto",
|
dockedSidebar: "auto",
|
||||||
vibrate: true,
|
vibrate: true,
|
||||||
moreInfoEntityId: null as any,
|
moreInfoEntityId: null as any,
|
||||||
|
// @ts-ignore
|
||||||
async callService(domain, service, data) {
|
async callService(domain, service, data) {
|
||||||
if (data && "entity_id" in data) {
|
if (data && "entity_id" in data) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LitElement, PropertyValues, property } from "lit-element";
|
import { PropertyValues, property, UpdatingElement } from "lit-element";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { HomeAssistant, Constructor } from "../types";
|
import { HomeAssistant, Constructor } from "../types";
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ export interface HassSubscribeElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:disable-next-line:variable-name */
|
/* tslint:disable-next-line:variable-name */
|
||||||
export const SubscribeMixin = <T extends Constructor<LitElement>>(
|
export const SubscribeMixin = <T extends Constructor<UpdatingElement>>(
|
||||||
superClass: T
|
superClass: T
|
||||||
) => {
|
) => {
|
||||||
class SubscribeClass extends superClass {
|
class SubscribeClass extends superClass {
|
||||||
|
@ -29,6 +29,7 @@ const PAGES: Array<{
|
|||||||
{ page: "area_registry", core: true },
|
{ page: "area_registry", core: true },
|
||||||
{ page: "automation" },
|
{ page: "automation" },
|
||||||
{ page: "script" },
|
{ page: "script" },
|
||||||
|
{ page: "scene" },
|
||||||
{ page: "zha" },
|
{ page: "zha" },
|
||||||
{ page: "zwave" },
|
{ page: "zwave" },
|
||||||
{ page: "customize", core: true, advanced: true },
|
{ page: "customize", core: true, advanced: true },
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
|
|
||||||
import { DeviceRegistryEntry } from "../../../../data/device_registry";
|
import {
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
computeDeviceName,
|
||||||
|
} from "../../../../data/device_registry";
|
||||||
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
||||||
import {
|
import {
|
||||||
LitElement,
|
LitElement,
|
||||||
@ -93,14 +96,10 @@ export class HaDeviceCard extends LitElement {
|
|||||||
return areas.find((area) => area.area_id === device.area_id).name;
|
return areas.find((area) => area.area_id === device.area_id).name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deviceName(device) {
|
|
||||||
return device.name_by_user || device.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeDeviceName(devices, deviceId) {
|
private _computeDeviceName(devices, deviceId) {
|
||||||
const device = devices.find((dev) => dev.id === deviceId);
|
const device = devices.find((dev) => dev.id === deviceId);
|
||||||
return device
|
return device
|
||||||
? this._deviceName(device)
|
? computeDeviceName(device, this.hass)
|
||||||
: `(${this.hass.localize(
|
: `(${this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_entry.device_unavailable"
|
"ui.panel.config.integrations.config_entry.device_unavailable"
|
||||||
)})`;
|
)})`;
|
||||||
|
@ -18,13 +18,15 @@ import {
|
|||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
import { DeviceRegistryEntry } from "../../../data/device_registry";
|
import {
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
computeDeviceName,
|
||||||
|
} from "../../../data/device_registry";
|
||||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
import { ConfigEntry } from "../../../data/config_entries";
|
import { ConfigEntry } from "../../../data/config_entries";
|
||||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
|
||||||
|
|
||||||
export interface DeviceRowData extends DeviceRegistryEntry {
|
export interface DeviceRowData extends DeviceRegistryEntry {
|
||||||
device?: DeviceRowData;
|
device?: DeviceRowData;
|
||||||
@ -99,11 +101,11 @@ export class HaDevicesDataTable extends LitElement {
|
|||||||
outputDevices = outputDevices.map((device) => {
|
outputDevices = outputDevices.map((device) => {
|
||||||
return {
|
return {
|
||||||
...device,
|
...device,
|
||||||
name:
|
name: computeDeviceName(
|
||||||
device.name_by_user ||
|
device,
|
||||||
device.name ||
|
this.hass,
|
||||||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
|
deviceEntityLookup[device.id]
|
||||||
"No name",
|
),
|
||||||
model: device.model || "<unknown>",
|
model: device.model || "<unknown>",
|
||||||
manufacturer: device.manufacturer || "<unknown>",
|
manufacturer: device.manufacturer || "<unknown>",
|
||||||
area: device.area_id ? areaLookup[device.area_id].name : "No area",
|
area: device.area_id ? areaLookup[device.area_id].name : "No area",
|
||||||
@ -250,20 +252,6 @@ export class HaDevicesDataTable extends LitElement {
|
|||||||
return batteryEntity ? batteryEntity.entity_id : undefined;
|
return batteryEntity ? batteryEntity.entity_id : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fallbackDeviceName(
|
|
||||||
deviceId: string,
|
|
||||||
deviceEntityLookup: DeviceEntityLookup
|
|
||||||
): string | undefined {
|
|
||||||
for (const entity of deviceEntityLookup[deviceId] || []) {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (stateObj) {
|
|
||||||
return computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleRowClicked(ev: CustomEvent) {
|
private _handleRowClicked(ev: CustomEvent) {
|
||||||
const deviceId = (ev.detail as RowClickedEvent).id;
|
const deviceId = (ev.detail as RowClickedEvent).id;
|
||||||
navigate(this, `/config/devices/device/${deviceId}`);
|
navigate(this, `/config/devices/device/${deviceId}`);
|
||||||
|
@ -88,6 +88,11 @@ class HaPanelConfig extends HassRouterPage {
|
|||||||
load: () =>
|
load: () =>
|
||||||
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script"),
|
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script"),
|
||||||
},
|
},
|
||||||
|
scene: {
|
||||||
|
tag: "ha-config-scene",
|
||||||
|
load: () =>
|
||||||
|
import(/* webpackChunkName: "panel-config-scene" */ "./scene/ha-config-scene"),
|
||||||
|
},
|
||||||
users: {
|
users: {
|
||||||
tag: "ha-config-users",
|
tag: "ha-config-users",
|
||||||
load: () =>
|
load: () =>
|
||||||
|
@ -52,7 +52,7 @@ export default class ZoneCondition extends Component<any> {
|
|||||||
onChange={this.zonePicked}
|
onChange={this.zonePicked}
|
||||||
hass={hass}
|
hass={hass}
|
||||||
allowCustomEntity
|
allowCustomEntity
|
||||||
domainFilter="zone"
|
includeDomains={["zone"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,7 @@ export default class SceneAction extends Component<any> {
|
|||||||
value={scene}
|
value={scene}
|
||||||
onChange={this.sceneChanged}
|
onChange={this.sceneChanged}
|
||||||
hass={hass}
|
hass={hass}
|
||||||
domainFilter="scene"
|
includeDomains={["scene"]}
|
||||||
allowCustomEntity
|
allowCustomEntity
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +51,7 @@ export default class GeolocationTrigger extends Component<any> {
|
|||||||
onChange={this.zonePicked}
|
onChange={this.zonePicked}
|
||||||
hass={hass}
|
hass={hass}
|
||||||
allowCustomEntity
|
allowCustomEntity
|
||||||
domainFilter="zone"
|
includeDomains={["zone"]}
|
||||||
/>
|
/>
|
||||||
<label id="eventlabel">
|
<label id="eventlabel">
|
||||||
{localize(
|
{localize(
|
||||||
|
@ -42,7 +42,7 @@ export default class ZoneTrigger extends Component<any> {
|
|||||||
onChange={this.zonePicked}
|
onChange={this.zonePicked}
|
||||||
hass={hass}
|
hass={hass}
|
||||||
allowCustomEntity
|
allowCustomEntity
|
||||||
domainFilter="zone"
|
includeDomains={["zone"]}
|
||||||
/>
|
/>
|
||||||
<label id="eventlabel">
|
<label id="eventlabel">
|
||||||
{localize(
|
{localize(
|
||||||
|
@ -104,7 +104,7 @@ class DialogPersonDetail extends LitElement {
|
|||||||
<ha-entities-picker
|
<ha-entities-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._deviceTrackers}
|
.value=${this._deviceTrackers}
|
||||||
domain-filter="device_tracker"
|
include-domains='["device_tracker"]'
|
||||||
.pickedEntityLabel=${this.hass.localize(
|
.pickedEntityLabel=${this.hass.localize(
|
||||||
"ui.panel.config.person.detail.device_tracker_picked"
|
"ui.panel.config.person.detail.device_tracker_picked"
|
||||||
)}
|
)}
|
||||||
|
81
src/panels/config/scene/ha-config-scene.ts
Normal file
81
src/panels/config/scene/ha-config-scene.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import "@polymer/app-route/app-route";
|
||||||
|
|
||||||
|
import "./ha-scene-editor";
|
||||||
|
import "./ha-scene-dashboard";
|
||||||
|
|
||||||
|
import {
|
||||||
|
HassRouterPage,
|
||||||
|
RouterOptions,
|
||||||
|
} from "../../../layouts/hass-router-page";
|
||||||
|
import { property, customElement, PropertyValues } from "lit-element";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
|
import { compare } from "../../../common/string/compare";
|
||||||
|
import { SceneEntity } from "../../../data/scene";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { HassEntities } from "home-assistant-js-websocket";
|
||||||
|
|
||||||
|
@customElement("ha-config-scene")
|
||||||
|
class HaConfigScene extends HassRouterPage {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public narrow!: boolean;
|
||||||
|
@property() public showAdvanced!: boolean;
|
||||||
|
@property() public scenes: SceneEntity[] = [];
|
||||||
|
|
||||||
|
protected routerOptions: RouterOptions = {
|
||||||
|
defaultPage: "dashboard",
|
||||||
|
routes: {
|
||||||
|
dashboard: {
|
||||||
|
tag: "ha-scene-dashboard",
|
||||||
|
cache: true,
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
tag: "ha-scene-editor",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private _computeScenes = memoizeOne((states: HassEntities) => {
|
||||||
|
const scenes: SceneEntity[] = [];
|
||||||
|
Object.keys(states).forEach((entityId) => {
|
||||||
|
if (computeDomain(entityId) === "scene") {
|
||||||
|
scenes.push(states[entityId] as SceneEntity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return scenes.sort((a, b) => {
|
||||||
|
return compare(computeStateName(a), computeStateName(b));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
protected updatePageEl(pageEl, changedProps: PropertyValues) {
|
||||||
|
pageEl.hass = this.hass;
|
||||||
|
pageEl.narrow = this.narrow;
|
||||||
|
pageEl.showAdvanced = this.showAdvanced;
|
||||||
|
|
||||||
|
if (this.hass) {
|
||||||
|
pageEl.scenes = this._computeScenes(this.hass.states);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!changedProps || changedProps.has("route")) &&
|
||||||
|
this._currentPage === "edit"
|
||||||
|
) {
|
||||||
|
const sceneId = this.routeTail.path.substr(1);
|
||||||
|
pageEl.creatingNew = sceneId === "new" ? true : false;
|
||||||
|
pageEl.scene =
|
||||||
|
sceneId === "new"
|
||||||
|
? undefined
|
||||||
|
: pageEl.scenes.find(
|
||||||
|
(entity: SceneEntity) => entity.attributes.id === sceneId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-scene": HaConfigScene;
|
||||||
|
}
|
||||||
|
}
|
213
src/panels/config/scene/ha-scene-dashboard.ts
Normal file
213
src/panels/config/scene/ha-scene-dashboard.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
CSSResultArray,
|
||||||
|
css,
|
||||||
|
property,
|
||||||
|
customElement,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
|
import "@polymer/paper-item/paper-item-body";
|
||||||
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
|
import "../../../layouts/hass-subpage";
|
||||||
|
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-fab";
|
||||||
|
|
||||||
|
import "../ha-config-section";
|
||||||
|
|
||||||
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { SceneEntity, activateScene } from "../../../data/scene";
|
||||||
|
import { showToast } from "../../../util/toast";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
import { forwardHaptic } from "../../../data/haptics";
|
||||||
|
|
||||||
|
@customElement("ha-scene-dashboard")
|
||||||
|
class HaSceneDashboard extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public narrow!: boolean;
|
||||||
|
@property() public scenes!: SceneEntity[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`
|
||||||
|
<hass-subpage
|
||||||
|
.header=${this.hass.localize("ui.panel.config.scene.caption")}
|
||||||
|
>
|
||||||
|
<ha-config-section .isWide=${!this.narrow}>
|
||||||
|
<div slot="header">
|
||||||
|
${this.hass.localize("ui.panel.config.scene.picker.header")}
|
||||||
|
</div>
|
||||||
|
<div slot="introduction">
|
||||||
|
${this.hass.localize("ui.panel.config.scene.picker.introduction")}
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://home-assistant.io/docs/scene/editor/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-card
|
||||||
|
.heading=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.pick_scene"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.scenes.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.no_scenes"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: this.scenes.map(
|
||||||
|
(scene) => html`
|
||||||
|
|
||||||
|
<div class='scene'>
|
||||||
|
<paper-icon-button
|
||||||
|
.scene=${scene}
|
||||||
|
icon="hass:play"
|
||||||
|
title="${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.activate_scene"
|
||||||
|
)}"
|
||||||
|
@click=${this._activateScene}
|
||||||
|
></paper-icon-button>
|
||||||
|
<paper-item-body two-line>
|
||||||
|
<div>${computeStateName(scene)}</div>
|
||||||
|
</paper-item-body>
|
||||||
|
<a
|
||||||
|
href=${ifDefined(
|
||||||
|
scene.attributes.id
|
||||||
|
? `/config/scene/edit/${scene.attributes.id}`
|
||||||
|
: undefined
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<paper-icon-button
|
||||||
|
title="${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.edit_scene"
|
||||||
|
)}"
|
||||||
|
icon="hass:pencil"
|
||||||
|
.disabled=${!scene.attributes.id}
|
||||||
|
></paper-icon-button>
|
||||||
|
${
|
||||||
|
!scene.attributes.id
|
||||||
|
? html`
|
||||||
|
<paper-tooltip position="left">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.only_editable"
|
||||||
|
)}
|
||||||
|
</paper-tooltip>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-card>
|
||||||
|
</ha-config-section>
|
||||||
|
|
||||||
|
<a href="/config/scene/edit/new">
|
||||||
|
<ha-fab
|
||||||
|
slot="fab"
|
||||||
|
?is-wide=${!this.narrow}
|
||||||
|
icon="hass:plus"
|
||||||
|
title=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.add_scene"
|
||||||
|
)}
|
||||||
|
?rtl=${computeRTL(this.hass)}
|
||||||
|
></ha-fab>
|
||||||
|
</a>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _activateScene(ev) {
|
||||||
|
const scene = ev.target.scene as SceneEntity;
|
||||||
|
await activateScene(this.hass, scene.entity_id);
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.activated",
|
||||||
|
"name",
|
||||||
|
computeStateName(scene)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
forwardHaptic("light");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultArray {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
hass-subpage {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-card {
|
||||||
|
margin-bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: horizontal;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene a[href] {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-entity-toggle {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[is-wide] {
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[rtl] {
|
||||||
|
right: auto;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[rtl][is-wide] {
|
||||||
|
bottom: 24px;
|
||||||
|
right: auto;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-scene-dashboard": HaSceneDashboard;
|
||||||
|
}
|
||||||
|
}
|
738
src/panels/config/scene/ha-scene-editor.ts
Normal file
738
src/panels/config/scene/ha-scene-editor.ts
Normal file
@ -0,0 +1,738 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
|
PropertyValues,
|
||||||
|
property,
|
||||||
|
customElement,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@polymer/app-layout/app-header/app-header";
|
||||||
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
|
import "@polymer/paper-item/paper-item-body";
|
||||||
|
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
|
||||||
|
import "../../../components/ha-fab";
|
||||||
|
import "../../../components/device/ha-device-picker";
|
||||||
|
import "../../../components/entity/ha-entities-picker";
|
||||||
|
import "../../../components/ha-paper-icon-button-arrow-prev";
|
||||||
|
import "../../../layouts/ha-app-layout";
|
||||||
|
|
||||||
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
|
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
|
import {
|
||||||
|
SceneEntity,
|
||||||
|
SceneConfig,
|
||||||
|
getSceneConfig,
|
||||||
|
deleteScene,
|
||||||
|
saveScene,
|
||||||
|
SCENE_IGNORED_DOMAINS,
|
||||||
|
SceneEntities,
|
||||||
|
SCENE_SAVED_ATTRIBUTES,
|
||||||
|
applyScene,
|
||||||
|
activateScene,
|
||||||
|
} from "../../../data/scene";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
subscribeDeviceRegistry,
|
||||||
|
computeDeviceName,
|
||||||
|
} from "../../../data/device_registry";
|
||||||
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
subscribeEntityRegistry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { HassEvent } from "home-assistant-js-websocket";
|
||||||
|
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
|
||||||
|
|
||||||
|
interface DeviceEntities {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceEntitiesLookup {
|
||||||
|
[deviceId: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-scene-editor")
|
||||||
|
export class HaSceneEditor extends SubscribeMixin(LitElement) {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public narrow?: boolean;
|
||||||
|
@property() public scene?: SceneEntity;
|
||||||
|
@property() public creatingNew?: boolean;
|
||||||
|
@property() public showAdvanced!: boolean;
|
||||||
|
@property() private _dirty?: boolean;
|
||||||
|
@property() private _errors?: string;
|
||||||
|
@property() private _config!: SceneConfig;
|
||||||
|
@property() private _entities: string[] = [];
|
||||||
|
@property() private _devices: string[] = [];
|
||||||
|
@property() private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
||||||
|
@property() private _entityRegistryEntries: EntityRegistryEntry[] = [];
|
||||||
|
private _storedStates: SceneEntities = {};
|
||||||
|
private _unsubscribeEvents?: () => void;
|
||||||
|
private _deviceEntityLookup: DeviceEntitiesLookup = {};
|
||||||
|
private _activateContextId?: string;
|
||||||
|
|
||||||
|
private _getEntitiesDevices = memoizeOne(
|
||||||
|
(
|
||||||
|
entities: string[],
|
||||||
|
devices: string[],
|
||||||
|
deviceEntityLookup: DeviceEntitiesLookup,
|
||||||
|
deviceRegs: DeviceRegistryEntry[]
|
||||||
|
) => {
|
||||||
|
const outputDevices: DeviceEntities[] = [];
|
||||||
|
|
||||||
|
if (devices.length) {
|
||||||
|
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
|
||||||
|
for (const device of deviceRegs) {
|
||||||
|
deviceLookup[device.id] = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.forEach((deviceId) => {
|
||||||
|
const device = deviceLookup[deviceId];
|
||||||
|
const deviceEntities: string[] = deviceEntityLookup[deviceId] || [];
|
||||||
|
outputDevices.push({
|
||||||
|
name: computeDeviceName(
|
||||||
|
device,
|
||||||
|
this.hass,
|
||||||
|
this._deviceEntityLookup[device.id]
|
||||||
|
),
|
||||||
|
id: device.id,
|
||||||
|
entities: deviceEntities,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputEntities: string[] = [];
|
||||||
|
|
||||||
|
entities.forEach((entity) => {
|
||||||
|
if (!outputDevices.find((device) => device.entities.includes(entity))) {
|
||||||
|
outputEntities.push(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { devices: outputDevices, entities: outputEntities };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this._unsubscribeEvents) {
|
||||||
|
this._unsubscribeEvents();
|
||||||
|
this._unsubscribeEvents = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hassSubscribe() {
|
||||||
|
return [
|
||||||
|
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._entityRegistryEntries = entries;
|
||||||
|
}),
|
||||||
|
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._deviceRegistryEntries = entries;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { devices, entities } = this._getEntitiesDevices(
|
||||||
|
this._entities,
|
||||||
|
this._devices,
|
||||||
|
this._deviceEntityLookup,
|
||||||
|
this._deviceRegistryEntries
|
||||||
|
);
|
||||||
|
return html`
|
||||||
|
<ha-app-layout has-scrolling-region>
|
||||||
|
<app-header slot="header" fixed>
|
||||||
|
<app-toolbar>
|
||||||
|
<ha-paper-icon-button-arrow-prev
|
||||||
|
@click=${this._backTapped}
|
||||||
|
></ha-paper-icon-button-arrow-prev>
|
||||||
|
<div main-title>
|
||||||
|
${this.scene
|
||||||
|
? computeStateName(this.scene)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.default_name"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${this.creatingNew
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<paper-icon-button
|
||||||
|
title="${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.picker.delete_scene"
|
||||||
|
)}"
|
||||||
|
icon="hass:delete"
|
||||||
|
@click=${this._deleteTapped}
|
||||||
|
></paper-icon-button>
|
||||||
|
`}
|
||||||
|
</app-toolbar>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
${this._errors
|
||||||
|
? html`
|
||||||
|
<div class="errors">${this._errors}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div
|
||||||
|
id="root"
|
||||||
|
class="${classMap({
|
||||||
|
rtl: computeRTL(this.hass),
|
||||||
|
})}"
|
||||||
|
>
|
||||||
|
<ha-config-section .isWide=${!this.narrow}>
|
||||||
|
<div slot="header">
|
||||||
|
${this.scene
|
||||||
|
? computeStateName(this.scene)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.default_name"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div slot="introduction">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.introduction"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<paper-input
|
||||||
|
.value=${this.scene ? computeStateName(this.scene) : ""}
|
||||||
|
@value-changed=${this._nameChanged}
|
||||||
|
label=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.name"
|
||||||
|
)}
|
||||||
|
></paper-input>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</ha-config-section>
|
||||||
|
|
||||||
|
<ha-config-section .isWide=${!this.narrow}>
|
||||||
|
<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>
|
||||||
|
<div class="card-header">
|
||||||
|
${device.name}
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hass:delete"
|
||||||
|
title="${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.devices.delete"
|
||||||
|
)}"
|
||||||
|
.device=${device.id}
|
||||||
|
@click=${this._deleteDevice}
|
||||||
|
></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
${device.entities.map((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity];
|
||||||
|
if (!stateObj) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<paper-icon-item
|
||||||
|
.entity=${stateObj.entity_id}
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
class="device-entity"
|
||||||
|
>
|
||||||
|
<state-badge
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
slot="item-icon"
|
||||||
|
></state-badge>
|
||||||
|
<paper-item-body>
|
||||||
|
${computeStateName(stateObj)}
|
||||||
|
</paper-item-body>
|
||||||
|
</paper-icon-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ha-card
|
||||||
|
.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"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</ha-config-section>
|
||||||
|
|
||||||
|
${this.showAdvanced
|
||||||
|
? html`
|
||||||
|
<ha-config-section .isWide=${!this.narrow}>
|
||||||
|
<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
|
||||||
|
class="entities"
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.without_device"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${entities.map((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity];
|
||||||
|
if (!stateObj) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<paper-icon-item
|
||||||
|
.entity=${stateObj.entity_id}
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
class="device-entity"
|
||||||
|
>
|
||||||
|
<state-badge
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
slot="item-icon"
|
||||||
|
></state-badge>
|
||||||
|
<paper-item-body>
|
||||||
|
${computeStateName(stateObj)}
|
||||||
|
</paper-item-body>
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hass:delete"
|
||||||
|
.entity=${entity}
|
||||||
|
.title="${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.delete"
|
||||||
|
)}"
|
||||||
|
@click=${this._deleteEntity}
|
||||||
|
></paper-icon-button>
|
||||||
|
</paper-icon-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<ha-card
|
||||||
|
header=${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.add"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.entities.device_entities"
|
||||||
|
)}
|
||||||
|
<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"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</ha-config-section>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ha-fab
|
||||||
|
slot="fab"
|
||||||
|
?is-wide="${!this.narrow}"
|
||||||
|
?dirty="${this._dirty}"
|
||||||
|
icon="hass:content-save"
|
||||||
|
.title="${this.hass.localize("ui.panel.config.scene.editor.save")}"
|
||||||
|
@click=${this._saveScene}
|
||||||
|
class="${classMap({
|
||||||
|
rtl: computeRTL(this.hass),
|
||||||
|
})}"
|
||||||
|
></ha-fab>
|
||||||
|
</ha-app-layout>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
const oldscene = changedProps.get("scene") as SceneEntity;
|
||||||
|
if (
|
||||||
|
changedProps.has("scene") &&
|
||||||
|
this.scene &&
|
||||||
|
this.hass &&
|
||||||
|
// Only refresh config if we picked a new scene. If same ID, don't fetch it.
|
||||||
|
(!oldscene || oldscene.attributes.id !== this.scene.attributes.id)
|
||||||
|
) {
|
||||||
|
this._loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
|
||||||
|
this._dirty = false;
|
||||||
|
this._config = {
|
||||||
|
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
|
||||||
|
entities: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("_entityRegistryEntries")) {
|
||||||
|
for (const entity of this._entityRegistryEntries) {
|
||||||
|
if (
|
||||||
|
!entity.device_id ||
|
||||||
|
SCENE_IGNORED_DOMAINS.includes(computeDomain(entity.entity_id))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!(entity.device_id in this._deviceEntityLookup)) {
|
||||||
|
this._deviceEntityLookup[entity.device_id] = [];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this._deviceEntityLookup[entity.device_id].includes(entity.entity_id)
|
||||||
|
) {
|
||||||
|
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showMoreInfo(ev: Event) {
|
||||||
|
const entityId = (ev.currentTarget as any).entity;
|
||||||
|
fireEvent(this, "hass-more-info", { entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadConfig() {
|
||||||
|
let config: SceneConfig;
|
||||||
|
try {
|
||||||
|
config = await getSceneConfig(this.hass, this.scene!.attributes.id!);
|
||||||
|
} catch (err) {
|
||||||
|
alert(
|
||||||
|
err.status_code === 404
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.load_error_not_editable"
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.scene.editor.load_error_unknown",
|
||||||
|
"err_no",
|
||||||
|
err.status_code
|
||||||
|
)
|
||||||
|
);
|
||||||
|
history.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.entities) {
|
||||||
|
config.entities = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._entities = Object.keys(config.entities);
|
||||||
|
|
||||||
|
this._entities.forEach((entity) => {
|
||||||
|
this._storeState(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) =>
|
||||||
|
this._entities.includes(entityReg.entity_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const entityReg of filteredEntityReg) {
|
||||||
|
if (!entityReg.device_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!this._devices.includes(entityReg.device_id)) {
|
||||||
|
this._devices = [...this._devices, entityReg.device_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { context } = await activateScene(this.hass, this.scene!.entity_id);
|
||||||
|
|
||||||
|
this._activateContextId = context.id;
|
||||||
|
|
||||||
|
this._unsubscribeEvents = await this.hass!.connection.subscribeEvents<
|
||||||
|
HassEvent
|
||||||
|
>((event) => this._stateChanged(event), "state_changed");
|
||||||
|
|
||||||
|
this._dirty = false;
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _entityPicked(ev: CustomEvent) {
|
||||||
|
const entityId = ev.detail.value;
|
||||||
|
(ev.target as any).value = "";
|
||||||
|
if (this._entities.includes(entityId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._entities = [...this._entities, entityId];
|
||||||
|
this._storeState(entityId);
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deleteEntity(ev: Event) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const deleteEntityId = (ev.target as any).entityId;
|
||||||
|
this._entities = this._entities.filter(
|
||||||
|
(entityId) => entityId !== deleteEntityId
|
||||||
|
);
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _devicePicked(ev: CustomEvent) {
|
||||||
|
const device = ev.detail.value;
|
||||||
|
(ev.target as any).value = "";
|
||||||
|
if (this._devices.includes(device)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._devices = [...this._devices, device];
|
||||||
|
const deviceEntities = this._deviceEntityLookup[device];
|
||||||
|
this._entities = [...this._entities, ...deviceEntities];
|
||||||
|
deviceEntities.forEach((entityId) => {
|
||||||
|
this._storeState(entityId);
|
||||||
|
});
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deleteDevice(ev: Event) {
|
||||||
|
const deviceId = (ev.target as any).device;
|
||||||
|
this._devices = this._devices.filter((device) => device !== deviceId);
|
||||||
|
const deviceEntities = this._deviceEntityLookup[deviceId];
|
||||||
|
this._entities = this._entities.filter(
|
||||||
|
(entityId) => !deviceEntities.includes(entityId)
|
||||||
|
);
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nameChanged(ev: CustomEvent) {
|
||||||
|
if (!this._config || this._config.name === ev.detail.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._config.name = ev.detail.value;
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stateChanged(event: HassEvent) {
|
||||||
|
if (
|
||||||
|
event.context.id !== this._activateContextId &&
|
||||||
|
this._entities.includes(event.data.entity_id)
|
||||||
|
) {
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _backTapped(): void {
|
||||||
|
if (this._dirty) {
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
text: this.hass!.localize(
|
||||||
|
"ui.panel.config.scene.editor.unsaved_confirm"
|
||||||
|
),
|
||||||
|
confirmBtnText: this.hass!.localize("ui.common.yes"),
|
||||||
|
cancelBtnText: this.hass!.localize("ui.common.no"),
|
||||||
|
confirm: () => this._goBack(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._goBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _goBack(): void {
|
||||||
|
applyScene(this.hass, this._storedStates);
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deleteTapped(): void {
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"),
|
||||||
|
confirmBtnText: this.hass!.localize("ui.common.yes"),
|
||||||
|
cancelBtnText: this.hass!.localize("ui.common.no"),
|
||||||
|
confirm: () => this._delete(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _delete(): Promise<void> {
|
||||||
|
await deleteScene(this.hass, this.scene!.attributes.id!);
|
||||||
|
applyScene(this.hass, this._storedStates);
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _calculateStates(): SceneEntities {
|
||||||
|
const output: SceneEntities = {};
|
||||||
|
this._entities.forEach((entityId) => {
|
||||||
|
const state = this._getCurrentState(entityId);
|
||||||
|
if (state) {
|
||||||
|
output[entityId] = state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _storeState(entityId: string): void {
|
||||||
|
if (entityId in this._storedStates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const state = this._getCurrentState(entityId);
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._storedStates[entityId] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCurrentState(entityId: string) {
|
||||||
|
const stateObj = this.hass.states[entityId];
|
||||||
|
if (!stateObj) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const domain = computeDomain(entityId);
|
||||||
|
const attributes = {};
|
||||||
|
for (const attribute in stateObj.attributes) {
|
||||||
|
if (
|
||||||
|
SCENE_SAVED_ATTRIBUTES[domain] &&
|
||||||
|
SCENE_SAVED_ATTRIBUTES[domain].includes(attribute)
|
||||||
|
) {
|
||||||
|
attributes[attribute] = stateObj.attributes[attribute];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...attributes, state: stateObj.state };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveScene(): Promise<void> {
|
||||||
|
const id = this.creatingNew ? "" + Date.now() : this.scene!.attributes.id!;
|
||||||
|
this._config = { ...this._config, entities: this._calculateStates() };
|
||||||
|
try {
|
||||||
|
await saveScene(this.hass, id, this._config);
|
||||||
|
this._dirty = false;
|
||||||
|
|
||||||
|
if (this.creatingNew) {
|
||||||
|
navigate(this, `/config/scene/edit/${id}`, true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this._errors = err.body.message || err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
ha-card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
padding: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--google-red-500);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
.triggers,
|
||||||
|
.script {
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
.triggers ha-card,
|
||||||
|
.script ha-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.add-card mwc-button {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.card-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.rtl .card-menu {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.card-menu paper-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
paper-icon-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
ha-card paper-icon-button {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.card-header > paper-icon-button {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
top: -8px;
|
||||||
|
}
|
||||||
|
.device-entity {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
span[slot="introduction"] a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-fab {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
margin-bottom: -80px;
|
||||||
|
transition: margin-bottom 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[is-wide] {
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[dirty] {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab.rtl {
|
||||||
|
right: auto;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[is-wide].rtl {
|
||||||
|
bottom: 24px;
|
||||||
|
right: auto;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-scene-editor": HaSceneEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -112,7 +112,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
|
|||||||
value="[[_computeEntityValue(parsedJSON)]]"
|
value="[[_computeEntityValue(parsedJSON)]]"
|
||||||
on-change="_entityPicked"
|
on-change="_entityPicked"
|
||||||
disabled="[[!validJSON]]"
|
disabled="[[!validJSON]]"
|
||||||
domain-filter="[[_computeEntityDomainFilter(_domain)]]"
|
include-domains="[[_computeEntityDomainFilter(_domain)]]"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
</template>
|
</template>
|
||||||
@ -285,7 +285,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_computeEntityDomainFilter(domain) {
|
_computeEntityDomainFilter(domain) {
|
||||||
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? domain : null;
|
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_callService() {
|
_callService() {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { STATES_OFF } from "../../../../common/const";
|
import { STATES_OFF } from "../../../../common/const";
|
||||||
import { turnOnOffEntity } from "./turn-on-off-entity";
|
import { turnOnOffEntity } from "./turn-on-off-entity";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant, ServiceCallResponse } from "../../../../types";
|
||||||
export const toggleEntity = (
|
export const toggleEntity = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string
|
entityId: string
|
||||||
): Promise<void> => {
|
): Promise<ServiceCallResponse> => {
|
||||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||||
return turnOnOffEntity(hass, entityId, turnOn);
|
return turnOnOffEntity(hass, entityId, turnOn);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant, ServiceCallResponse } from "../../../../types";
|
||||||
|
|
||||||
export const turnOnOffEntity = (
|
export const turnOnOffEntity = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
turnOn = true
|
turnOn = true
|
||||||
): Promise<void> => {
|
): Promise<ServiceCallResponse> => {
|
||||||
const stateDomain = computeDomain(entityId);
|
const stateDomain = computeDomain(entityId);
|
||||||
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="alarm_control_panel"
|
include-domains='["alarm_control_panel"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -92,7 +92,7 @@ export class HuiGaugeCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="sensor"
|
include-domains='["sensor"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -71,7 +71,7 @@ export class HuiLightCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="light"
|
include-domains='["light"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -52,7 +52,7 @@ export class HuiMediaControlCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="media_player"
|
include-domains='["media_player"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -152,7 +152,7 @@ export class HuiPictureEntityCardEditor extends LitElement
|
|||||||
.value="${this._camera_image}"
|
.value="${this._camera_image}"
|
||||||
.configValue=${"camera_image"}
|
.configValue=${"camera_image"}
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
domain-filter="camera"
|
include-domains='["camera"]'
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
<div class="side-by-side">
|
<div class="side-by-side">
|
||||||
|
@ -152,7 +152,7 @@ export class HuiPictureGlanceCardEditor extends LitElement
|
|||||||
.configValue=${"camera_image"}
|
.configValue=${"camera_image"}
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
domain-filter="camera"
|
include-domains='["camera"]'
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
<div class="side-by-side">
|
<div class="side-by-side">
|
||||||
<paper-dropdown-menu
|
<paper-dropdown-menu
|
||||||
|
@ -67,7 +67,7 @@ export class HuiPlantStatusCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="plant"
|
include-domains='["plant"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -96,7 +96,7 @@ export class HuiSensorCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="sensor"
|
include-domains='["sensor"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -66,7 +66,7 @@ export class HuiThermostatCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="climate"
|
include-domains='["climate"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -65,7 +65,7 @@ export class HuiWeatherForecastCardEditor extends LitElement
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.value="${this._entity}"
|
.value="${this._entity}"
|
||||||
.configValue=${"entity"}
|
.configValue=${"entity"}
|
||||||
domain-filter="weather"
|
include-domains='["weather"]'
|
||||||
@change="${this._valueChanged}"
|
@change="${this._valueChanged}"
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@ -16,10 +16,11 @@ import "../components/hui-warning";
|
|||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EntityRow, EntityConfig } from "./types";
|
import { EntityRow, EntityConfig } from "./types";
|
||||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||||
|
import { activateScene } from "../../../data/scene";
|
||||||
|
|
||||||
@customElement("hui-scene-entity-row")
|
@customElement("hui-scene-entity-row")
|
||||||
class HuiSceneEntityRow extends LitElement implements EntityRow {
|
class HuiSceneEntityRow extends LitElement implements EntityRow {
|
||||||
@property() public hass?: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() private _config?: EntityConfig;
|
@property() private _config?: EntityConfig;
|
||||||
|
|
||||||
@ -79,11 +80,9 @@ class HuiSceneEntityRow extends LitElement implements EntityRow {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _callService(ev): void {
|
private _callService(ev: Event): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.hass!.callService("scene", "turn_on", {
|
activateScene(this.hass, this._config!.entity);
|
||||||
entity_id: this._config!.entity,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|||||||
|
|
||||||
import "../components/entity/state-info";
|
import "../components/entity/state-info";
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
import LocalizeMixin from "../mixins/localize-mixin";
|
||||||
|
import { activateScene } from "../data/scene";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin LocalizeMixin
|
||||||
@ -23,7 +24,7 @@ class StateCardScene extends LocalizeMixin(PolymerElement) {
|
|||||||
|
|
||||||
<div class="horizontal justified layout">
|
<div class="horizontal justified layout">
|
||||||
${this.stateInfoTemplate}
|
${this.stateInfoTemplate}
|
||||||
<mwc-button on-click="activateScene"
|
<mwc-button on-click="_activateScene"
|
||||||
>[[localize('ui.card.scene.activate')]]</mwc-button
|
>[[localize('ui.card.scene.activate')]]</mwc-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -51,11 +52,9 @@ class StateCardScene extends LocalizeMixin(PolymerElement) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
activateScene(ev) {
|
_activateScene(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.hass.callService("scene", "turn_on", {
|
activateScene(this.hass, this.stateObj.entity_id);
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("state-card-scene", StateCardScene);
|
customElements.define("state-card-scene", StateCardScene);
|
||||||
|
@ -17,7 +17,7 @@ import hassCallApi from "../util/hass-call-api";
|
|||||||
import { subscribePanels } from "../data/ws-panels";
|
import { subscribePanels } from "../data/ws-panels";
|
||||||
import { forwardHaptic } from "../data/haptics";
|
import { forwardHaptic } from "../data/haptics";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { Constructor } from "../types";
|
import { Constructor, ServiceCallResponse } from "../types";
|
||||||
import { HassBaseEl } from "./hass-base-mixin";
|
import { HassBaseEl } from "./hass-base-mixin";
|
||||||
import { broadcastConnectionStatus } from "../data/connection-status";
|
import { broadcastConnectionStatus } from "../data/connection-status";
|
||||||
|
|
||||||
@ -54,7 +54,12 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
console.log("Calling service", domain, service, serviceData);
|
console.log("Calling service", domain, service, serviceData);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await callService(conn, domain, service, serviceData);
|
return (await callService(
|
||||||
|
conn,
|
||||||
|
domain,
|
||||||
|
service,
|
||||||
|
serviceData
|
||||||
|
)) as Promise<ServiceCallResponse>;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// tslint:disable-next-line: no-console
|
// tslint:disable-next-line: no-console
|
||||||
|
@ -517,6 +517,8 @@
|
|||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
"successfully_saved": "Successfully saved"
|
"successfully_saved": "Successfully saved"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@ -996,6 +998,47 @@
|
|||||||
"link_available_actions": "Learn more about available actions."
|
"link_available_actions": "Learn more about available actions."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scene": {
|
||||||
|
"caption": "Scenes",
|
||||||
|
"description": "Create and edit scenes",
|
||||||
|
"activated": "Activated scene {name}.",
|
||||||
|
"picker": {
|
||||||
|
"header": "Scene Editor",
|
||||||
|
"introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
|
||||||
|
"learn_more": "Learn more about scenes",
|
||||||
|
"pick_scene": "Pick scene to edit",
|
||||||
|
"no_scenes": "We couldn’t find any editable scenes",
|
||||||
|
"add_scene": "Add scene",
|
||||||
|
"only_editable": "Only scenes defined in scenes.yaml are editable.",
|
||||||
|
"edit_scene": "Edit scene",
|
||||||
|
"show_info_scene": "Show info about scene",
|
||||||
|
"delete_scene": "Delete scene",
|
||||||
|
"delete_confirm": "Are you sure you want to delete this scene?"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"introduction": "Use scenes to bring your home alive.",
|
||||||
|
"default_name": "New Scene",
|
||||||
|
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
|
||||||
|
"load_error_unknown": "Error loading scene ({err_no}).",
|
||||||
|
"save": "Save",
|
||||||
|
"unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?",
|
||||||
|
"name": "Name",
|
||||||
|
"devices": {
|
||||||
|
"header": "Devices",
|
||||||
|
"introduction": "Add the devices that you want to be included in your scene. Set all the devices to the state you want for this scene.",
|
||||||
|
"add": "Add a device",
|
||||||
|
"delete": "Delete device"
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"header": "Entities",
|
||||||
|
"introduction": "Entities that do not belong to a devices can be set here.",
|
||||||
|
"without_device": "Entities without device",
|
||||||
|
"device_entities": "If you add an entity that belongs to a device, the device will be added.",
|
||||||
|
"add": "Add an entity",
|
||||||
|
"delete": "Delete entity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"cloud": {
|
"cloud": {
|
||||||
"caption": "Home Assistant Cloud",
|
"caption": "Home Assistant Cloud",
|
||||||
"description_login": "Logged in as {email}",
|
"description_login": "Logged in as {email}",
|
||||||
|
12
src/types.ts
12
src/types.ts
@ -116,6 +116,16 @@ export interface Resources {
|
|||||||
[language: string]: { [key: string]: string };
|
[language: string]: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
id: string;
|
||||||
|
parrent_id?: string;
|
||||||
|
user_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceCallResponse {
|
||||||
|
context: Context;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HomeAssistant {
|
export interface HomeAssistant {
|
||||||
auth: Auth & { external?: ExternalMessaging };
|
auth: Auth & { external?: ExternalMessaging };
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
@ -150,7 +160,7 @@ export interface HomeAssistant {
|
|||||||
domain: string,
|
domain: string,
|
||||||
service: string,
|
service: string,
|
||||||
serviceData?: { [key: string]: any }
|
serviceData?: { [key: string]: any }
|
||||||
): Promise<void>;
|
): Promise<ServiceCallResponse>;
|
||||||
callApi<T>(
|
callApi<T>(
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
path: string,
|
path: string,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user