diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts
index 29fcc20115..c374c5a686 100644
--- a/src/components/device/ha-device-picker.ts
+++ b/src/components/device/ha-device-picker.ts
@@ -21,6 +21,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
+ computeDeviceName,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@@ -33,7 +34,6 @@ import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
-import { computeStateName } from "../../common/entity/compute_state_name";
interface Device {
name: string;
@@ -102,11 +102,11 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
const outputDevices = devices.map((device) => {
return {
id: device.id,
- name:
- device.name_by_user ||
- device.name ||
- this._fallbackDeviceName(device.id, deviceEntityLookup) ||
- "No name",
+ name: computeDeviceName(
+ device,
+ this.hass,
+ deviceEntityLookup[device.id]
+ ),
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 {
return css`
paper-input > paper-icon-button {
diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts
index 0ac70b9223..48cf365207 100644
--- a/src/components/entity/ha-entities-picker.ts
+++ b/src/components/entity/ha-entities-picker.ts
@@ -22,7 +22,20 @@ import { HassEntity } from "home-assistant-js-websocket";
class HaEntitiesPickerLight extends LitElement {
@property() public hass?: HomeAssistant;
@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" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@@ -31,6 +44,7 @@ class HaEntitiesPickerLight extends LitElement {
if (!this.hass) {
return;
}
+
const currentEntities = this._currentEntities;
return html`
${currentEntities.map(
@@ -40,7 +54,8 @@ class HaEntitiesPickerLight extends LitElement {
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
- .domainFilter=${this.domainFilter}
+ .includeDomains=${this.includeDomains}
+ .excludeDomains=${this.excludeDomains}
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
@@ -52,7 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
{
let states: HassEntity[] = [];
@@ -78,9 +92,15 @@ class HaEntityPicker extends LitElement {
}
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(
- (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 {
const states = this._getStates(
this._hass,
- this.domainFilter,
+ this.includeDomains,
+ this.excludeDomains,
this.entityFilter
);
diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts
index 873d6f9c9c..cdd8052b1a 100644
--- a/src/data/device_registry.ts
+++ b/src/data/device_registry.ts
@@ -1,6 +1,8 @@
import { HomeAssistant } from "../types";
import { createCollection, Connection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce";
+import { EntityRegistryEntry } from "./entity_registry";
+import { computeStateName } from "../common/entity/compute_state_name";
export interface DeviceRegistryEntry {
id: string;
@@ -20,6 +22,33 @@ export interface DeviceRegistryEntryMutableParams {
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 = (
hass: HomeAssistant,
deviceId: string,
diff --git a/src/data/scene.ts b/src/data/scene.ts
new file mode 100644
index 0000000000..26ac840144
--- /dev/null
+++ b/src/data/scene.ts
@@ -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 =>
+ hass.callService("scene", "turn_on", { entity_id: entityId });
+
+export const applyScene = (
+ hass: HomeAssistant,
+ entities: SceneEntities
+): Promise =>
+ hass.callService("scene", "apply", { entities });
+
+export const getSceneConfig = (
+ hass: HomeAssistant,
+ sceneId: string
+): Promise =>
+ hass.callApi("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}`);
diff --git a/src/dialogs/confirmation/dialog-confirmation.ts b/src/dialogs/confirmation/dialog-confirmation.ts
index 23bc7d312e..2ef6003b54 100644
--- a/src/dialogs/confirmation/dialog-confirmation.ts
+++ b/src/dialogs/confirmation/dialog-confirmation.ts
@@ -36,6 +36,7 @@ class DialogConfirmation extends LitElement {
@@ -48,10 +49,14 @@ class DialogConfirmation extends LitElement {
- ${this.hass.localize("ui.dialogs.confirmation.cancel")}
+ ${this._params.cancelBtnText
+ ? this._params.cancelBtnText
+ : this.hass.localize("ui.dialogs.confirmation.cancel")}
- ${this.hass.localize("ui.dialogs.confirmation.ok")}
+ ${this._params.confirmBtnText
+ ? this._params.confirmBtnText
+ : this.hass.localize("ui.dialogs.confirmation.ok")}
diff --git a/src/dialogs/confirmation/show-dialog-confirmation.ts b/src/dialogs/confirmation/show-dialog-confirmation.ts
index 47b3e556b8..df57cf5558 100644
--- a/src/dialogs/confirmation/show-dialog-confirmation.ts
+++ b/src/dialogs/confirmation/show-dialog-confirmation.ts
@@ -3,6 +3,8 @@ import { fireEvent } from "../../common/dom/fire_event";
export interface ConfirmationDialogParams {
title?: string;
text: string;
+ confirmBtnText?: string;
+ cancelBtnText?: string;
confirm: () => void;
}
diff --git a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts
index fc51180de8..5043adf429 100644
--- a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts
+++ b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts
@@ -24,6 +24,7 @@ import {
subscribeAreaRegistry,
AreaRegistryEntry,
} from "../../data/area_registry";
+import { computeDeviceName } from "../../data/device_registry";
@customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement {
@@ -75,8 +76,7 @@ class DialogDeviceRegistryDetail extends LitElement {
@opened-changed="${this._openedChanged}"
>
- ${device.name ||
- this.hass.localize("ui.panel.config.devices.unnamed_device")}
+ ${computeDeviceName(device, this.hass)}
${this._error
diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts
index f3101d5125..6870415ce1 100644
--- a/src/fake_data/provide_hass.ts
+++ b/src/fake_data/provide_hass.ts
@@ -180,6 +180,7 @@ export const provideHass = (
dockedSidebar: "auto",
vibrate: true,
moreInfoEntityId: null as any,
+ // @ts-ignore
async callService(domain, service, data) {
if (data && "entity_id" in data) {
await Promise.all(
diff --git a/src/mixins/subscribe-mixin.ts b/src/mixins/subscribe-mixin.ts
index 840be26d08..034235fa30 100644
--- a/src/mixins/subscribe-mixin.ts
+++ b/src/mixins/subscribe-mixin.ts
@@ -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 { HomeAssistant, Constructor } from "../types";
@@ -7,7 +7,7 @@ export interface HassSubscribeElement {
}
/* tslint:disable-next-line:variable-name */
-export const SubscribeMixin = >(
+export const SubscribeMixin = >(
superClass: T
) => {
class SubscribeClass extends superClass {
diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts
index ed64269965..72abd3ad17 100644
--- a/src/panels/config/dashboard/ha-config-navigation.ts
+++ b/src/panels/config/dashboard/ha-config-navigation.ts
@@ -29,6 +29,7 @@ const PAGES: Array<{
{ page: "area_registry", core: true },
{ page: "automation" },
{ page: "script" },
+ { page: "scene" },
{ page: "zha" },
{ page: "zwave" },
{ page: "customize", core: true, advanced: true },
diff --git a/src/panels/config/devices/device-detail/ha-device-card.ts b/src/panels/config/devices/device-detail/ha-device-card.ts
index 86e3109793..a9b5ad7b89 100644
--- a/src/panels/config/devices/device-detail/ha-device-card.ts
+++ b/src/panels/config/devices/device-detail/ha-device-card.ts
@@ -1,6 +1,9 @@
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 {
LitElement,
@@ -93,14 +96,10 @@ export class HaDeviceCard extends LitElement {
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) {
const device = devices.find((dev) => dev.id === deviceId);
return device
- ? this._deviceName(device)
+ ? computeDeviceName(device, this.hass)
: `(${this.hass.localize(
"ui.panel.config.integrations.config_entry.device_unavailable"
)})`;
diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts
index 20969fc859..66993e8392 100644
--- a/src/panels/config/devices/ha-devices-data-table.ts
+++ b/src/panels/config/devices/ha-devices-data-table.ts
@@ -18,13 +18,15 @@ import {
DataTableRowData,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
-import { DeviceRegistryEntry } from "../../../data/device_registry";
+import {
+ DeviceRegistryEntry,
+ computeDeviceName,
+} from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { ConfigEntry } from "../../../data/config_entries";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
-import { computeStateName } from "../../../common/entity/compute_state_name";
export interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@@ -99,11 +101,11 @@ export class HaDevicesDataTable extends LitElement {
outputDevices = outputDevices.map((device) => {
return {
...device,
- name:
- device.name_by_user ||
- device.name ||
- this._fallbackDeviceName(device.id, deviceEntityLookup) ||
- "No name",
+ name: computeDeviceName(
+ device,
+ this.hass,
+ deviceEntityLookup[device.id]
+ ),
model: device.model || "",
manufacturer: device.manufacturer || "",
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;
}
- 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) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);
diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts
index ea57036028..ed2bd9b1bb 100644
--- a/src/panels/config/ha-panel-config.ts
+++ b/src/panels/config/ha-panel-config.ts
@@ -88,6 +88,11 @@ class HaPanelConfig extends HassRouterPage {
load: () =>
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: {
tag: "ha-config-users",
load: () =>
diff --git a/src/panels/config/js/condition/zone.tsx b/src/panels/config/js/condition/zone.tsx
index 5d8ba95431..924284dd98 100644
--- a/src/panels/config/js/condition/zone.tsx
+++ b/src/panels/config/js/condition/zone.tsx
@@ -52,7 +52,7 @@ export default class ZoneCondition extends Component {
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
- domainFilter="zone"
+ includeDomains={["zone"]}
/>
);
diff --git a/src/panels/config/js/script/scene.tsx b/src/panels/config/js/script/scene.tsx
index a4fbb9a11f..001b360445 100644
--- a/src/panels/config/js/script/scene.tsx
+++ b/src/panels/config/js/script/scene.tsx
@@ -24,7 +24,7 @@ export default class SceneAction extends Component {
value={scene}
onChange={this.sceneChanged}
hass={hass}
- domainFilter="scene"
+ includeDomains={["scene"]}
allowCustomEntity
/>
diff --git a/src/panels/config/js/trigger/geo_location.tsx b/src/panels/config/js/trigger/geo_location.tsx
index bc20980f84..52deb4a6b2 100644
--- a/src/panels/config/js/trigger/geo_location.tsx
+++ b/src/panels/config/js/trigger/geo_location.tsx
@@ -51,7 +51,7 @@ export default class GeolocationTrigger extends Component {
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
- domainFilter="zone"
+ includeDomains={["zone"]}
/>