mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
Selectors and stuff
This commit is contained in:
parent
a6abc88007
commit
8837eede9a
@ -74,6 +74,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
has_entity_name: false,
|
||||
unique_id: "co2_intensity",
|
||||
options: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
@ -90,6 +91,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
has_entity_name: false,
|
||||
unique_id: "grid_fossil_fuel_percentage",
|
||||
options: null,
|
||||
labels: [],
|
||||
},
|
||||
]);
|
||||
|
||||
|
7
demo/src/stubs/label_registry.ts
Normal file
7
demo/src/stubs/label_registry.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockLabelRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: LabelRegistryEntry[] = []
|
||||
) => hass.mockWS("config/label_registry/list", () => data);
|
@ -7,6 +7,7 @@ import "../../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import "../../../../src/panels/config/automation/action/ha-automation-action";
|
||||
import { HaChooseAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-choose";
|
||||
@ -59,6 +60,7 @@ class DemoHaAutomationEditorAction extends LitElement {
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import "../../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import type { ConditionWithShorthand } from "../../../../src/data/automation";
|
||||
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
|
||||
@ -95,6 +96,7 @@ class DemoHaAutomationEditorCondition extends LitElement {
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import "../../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import type { Trigger } from "../../../../src/data/automation";
|
||||
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
||||
@ -141,6 +142,7 @@ class DemoHaAutomationEditorTrigger extends LitElement {
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
@ -58,6 +59,7 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -76,6 +78,7 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@ -94,6 +97,7 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
@ -118,6 +122,30 @@ const AREAS = [
|
||||
},
|
||||
];
|
||||
|
||||
const LABELS = [
|
||||
{
|
||||
label_id: "romantic",
|
||||
name: "Romantic",
|
||||
icon: "mdi:heart",
|
||||
color: "#ff0000",
|
||||
description: "Lights that can create a romantic atmosphere",
|
||||
},
|
||||
{
|
||||
label_id: "away",
|
||||
name: "Away",
|
||||
icon: "mdi:home-export-outline",
|
||||
color: "#cccccc",
|
||||
description: "All that can all be turned off when away from home",
|
||||
},
|
||||
{
|
||||
label_id: "cleaning",
|
||||
name: "Cleaning",
|
||||
icon: "mdi:home-export-outline",
|
||||
color: "#cccccc",
|
||||
description: "Everything to turn on while cleaning the house",
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
title: string;
|
||||
translations?: Record<string, string>;
|
||||
@ -132,6 +160,7 @@ const SCHEMAS: {
|
||||
entity: "Entity",
|
||||
device: "Device",
|
||||
area: "Area",
|
||||
label: "Label",
|
||||
target: "Target",
|
||||
number: "Number",
|
||||
boolean: "Boolean",
|
||||
@ -163,6 +192,7 @@ const SCHEMAS: {
|
||||
{ name: "Config entry", selector: { config_entry: {} } },
|
||||
{ name: "Duration", selector: { duration: {} } },
|
||||
{ name: "area", selector: { area: {} } },
|
||||
{ name: "label", selector: { label: {} } },
|
||||
{ name: "target", selector: { target: {} } },
|
||||
{ name: "number", selector: { number: { min: 0, max: 10 } } },
|
||||
{ name: "boolean", selector: { boolean: {} } },
|
||||
@ -444,6 +474,7 @@ class DemoHaForm extends LitElement {
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockLabelRegistry(hass, LABELS);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
@ -54,6 +55,7 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -72,6 +74,7 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@ -90,9 +93,7 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -114,6 +115,30 @@ const AREAS = [
|
||||
},
|
||||
];
|
||||
|
||||
const LABELS = [
|
||||
{
|
||||
label_id: "romantic",
|
||||
name: "Romantic",
|
||||
icon: "mdi:heart",
|
||||
color: "#ff0000",
|
||||
description: "Lights that can create a romantic atmosphere",
|
||||
},
|
||||
{
|
||||
label_id: "away",
|
||||
name: "Away",
|
||||
icon: "mdi:home-export-outline",
|
||||
color: "#cccccc",
|
||||
description: "All that can all be turned off when away from home",
|
||||
},
|
||||
{
|
||||
label_id: "cleaning",
|
||||
name: "Cleaning",
|
||||
icon: "mdi:home-export-outline",
|
||||
color: "#cccccc",
|
||||
description: "Everything to turn on while cleaning the house",
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
|
||||
@ -138,6 +163,7 @@ const SCHEMAS: {
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
area: { name: "Area", selector: { area: {} } },
|
||||
label: { name: "Label", selector: { label: {} } },
|
||||
target: { name: "Target", selector: { target: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
@ -161,8 +187,8 @@ const SCHEMAS: {
|
||||
},
|
||||
boolean: { name: "Boolean", selector: { boolean: {} } },
|
||||
time: { name: "Time", selector: { time: {} } },
|
||||
date: { name: "Date", selector: { date: {} } },
|
||||
datetime: { name: "Date Time", selector: { datetime: {} } },
|
||||
// date: { name: "Date", selector: { date: {} } },
|
||||
// datetime: { name: "Date Time", selector: { datetime: {} } },
|
||||
action: { name: "Action", selector: { action: {} } },
|
||||
text: {
|
||||
name: "Text",
|
||||
@ -279,6 +305,7 @@ const SCHEMAS: {
|
||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||
label: { name: "Label", selector: { label: { multiple: true } } },
|
||||
select: {
|
||||
name: "Select Multiple",
|
||||
selector: {
|
||||
@ -335,6 +362,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockLabelRegistry(hass, LABELS);
|
||||
mockHassioSupervisor(hass);
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||
|
@ -198,6 +198,7 @@ const createEntityRegistryEntries = (
|
||||
has_entity_name: false,
|
||||
unique_id: "updater",
|
||||
options: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
@ -221,6 +222,7 @@ const createDeviceRegistryEntries = (
|
||||
name_by_user: null,
|
||||
disabled_by: null,
|
||||
configuration_url: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
465
src/components/ha-label-picker.ts
Normal file
465
src/components/ha-label-picker.ts
Normal file
@ -0,0 +1,465 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
createLabelRegistryEntry,
|
||||
} from "../data/label_registry";
|
||||
import {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../dialogs/generic/show-dialog-box";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (
|
||||
item
|
||||
) => html`<mwc-list-item
|
||||
class=${classMap({ "add-new": item.label_id === "add_new" })}
|
||||
>
|
||||
${item.name}
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only labels with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no label with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only labels with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of labels to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-labels
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-labels" })
|
||||
public excludeLabels?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _suggestion?: string;
|
||||
|
||||
private _init = false;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
private _getLabels = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"],
|
||||
excludeLabels: this["excludeLabels"]
|
||||
): LabelRegistryEntry[] => {
|
||||
if (!labels.length) {
|
||||
return [
|
||||
{
|
||||
label_id: "no_labels",
|
||||
name: this.hass.localize("ui.components.label-picker.no_labels"),
|
||||
icon: null,
|
||||
color: "#CCCCCC",
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.labels);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputLabels = labels;
|
||||
|
||||
let labelIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
labelIds = inputDevices
|
||||
.filter((device) => device.labels)
|
||||
.map((device) => device.labels!)
|
||||
.flat(1);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
labelIds = (labelIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.labels)
|
||||
.map((entity) => entity.labels!)
|
||||
.flat(1)
|
||||
);
|
||||
}
|
||||
|
||||
if (labelIds) {
|
||||
outputLabels = labels.filter((label) =>
|
||||
labelIds!.includes(label.label_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeLabels) {
|
||||
outputLabels = outputLabels.filter(
|
||||
(label) => !excludeLabels!.includes(label.label_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (!outputLabels.length) {
|
||||
outputLabels = [
|
||||
{
|
||||
label_id: "no_labels",
|
||||
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||
icon: null,
|
||||
description: null,
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return noAdd
|
||||
? outputLabels
|
||||
: [
|
||||
...outputLabels,
|
||||
{
|
||||
label_id: "add_new",
|
||||
name: this.hass.localize("ui.components.label-picker.add_new"),
|
||||
icon: null,
|
||||
description: null,
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const labels = this._getLabels(
|
||||
Object.values(this.hass.labels),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeLabels
|
||||
);
|
||||
(this.comboBox as any).items = labels;
|
||||
(this.comboBox as any).filteredItems = labels;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
item-value-path="label_id"
|
||||
item-id-path="label_id"
|
||||
item-label-path="name"
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.label-picker.label")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this.hass.labels[this.placeholder]?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._labelChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filter = ev.detail.value;
|
||||
if (!filter) {
|
||||
this.comboBox.filteredItems = this.comboBox.items;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = this.comboBox.items?.filter((item) =>
|
||||
item.name.toLowerCase().includes(filter!.toLowerCase())
|
||||
);
|
||||
if (!this.noAdd && filteredItems?.length === 0) {
|
||||
this._suggestion = filter;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
label_id: "add_new_suggestion",
|
||||
name: this.hass.localize(
|
||||
"ui.components.label-picker.add_new_sugestion",
|
||||
{ name: this._suggestion }
|
||||
),
|
||||
icon: null,
|
||||
description: null,
|
||||
color: null,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _labelChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
|
||||
if (newValue === "no_labels") {
|
||||
newValue = "";
|
||||
}
|
||||
|
||||
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
(ev.target as any).value = this._value;
|
||||
showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.components.label-picker.add_dialog.title"),
|
||||
text: this.hass.localize("ui.components.label-picker.add_dialog.text"),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.components.label-picker.add_dialog.add"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.components.label-picker.add_dialog.name"
|
||||
),
|
||||
defaultValue:
|
||||
newValue === "add_new_suggestion" ? this._suggestion : undefined,
|
||||
confirm: async (name) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const label = await createLabelRegistryEntry(this.hass, {
|
||||
name,
|
||||
});
|
||||
const labels = [...Object.values(this.hass.labels), label];
|
||||
(this.comboBox as any).filteredItems = this._getLabels(
|
||||
labels,
|
||||
Object.values(this.hass.devices)!,
|
||||
Object.values(this.hass.entities)!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeLabels
|
||||
);
|
||||
await this.updateComplete;
|
||||
await this.comboBox.updateComplete;
|
||||
this._setValue(label.label_id);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.label-picker.add_dialog.failed_create_label"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
this._setValue(undefined);
|
||||
this._suggestion = undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _setValue(value?: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-label-picker": HaLabelPicker;
|
||||
}
|
||||
}
|
166
src/components/ha-labels-picker.ts
Normal file
166
src/components/ha-labels-picker.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-label-picker";
|
||||
|
||||
@customElement("ha-labels-picker")
|
||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only labels with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no label with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only label with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ attribute: "picked-label-label" })
|
||||
public pickedLabelLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-label-label" })
|
||||
public pickLabelLabel?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentLabels = this._currentLabels;
|
||||
return html`
|
||||
${currentLabels.map(
|
||||
(label) => html`
|
||||
<div>
|
||||
<ha-label-picker
|
||||
.curValue=${label}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${label}
|
||||
.label=${this.pickedLabelLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._labelChanged}
|
||||
></ha-label-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-label-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickLabelLabel}
|
||||
.helper=${this.helper}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.placeholder}
|
||||
.required=${this.required && !currentLabels.length}
|
||||
@value-changed=${this._addLabel}
|
||||
></ha-label-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentLabels(): string[] {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateLabels(labels) {
|
||||
this.value = labels;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: labels,
|
||||
});
|
||||
}
|
||||
|
||||
private _labelChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const curValue = (ev.currentTarget as any).curValue;
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue === curValue) {
|
||||
return;
|
||||
}
|
||||
const currentLabels = this._currentLabels;
|
||||
if (!newValue || currentLabels.includes(newValue)) {
|
||||
this._updateLabels(currentLabels.filter((ent) => ent !== curValue));
|
||||
return;
|
||||
}
|
||||
this._updateLabels(
|
||||
currentLabels.map((ent) => (ent === curValue ? newValue : ent))
|
||||
);
|
||||
}
|
||||
|
||||
private _addLabel(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const toAdd = ev.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(ev.currentTarget as any).value = "";
|
||||
const currentLabels = this._currentLabels;
|
||||
if (currentLabels.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateLabels([...currentLabels, toAdd]);
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-labels-picker": HaLabelsPicker;
|
||||
}
|
||||
}
|
132
src/components/ha-selector/ha-selector-label.ts
Normal file
132
src/components/ha-selector/ha-selector-label.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { LabelSelector } from "../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-label-picker";
|
||||
import "../ha-labels-picker";
|
||||
|
||||
@customElement("ha-selector-label")
|
||||
export class HaLabelSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: LabelSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
private _hasIntegration(selector: LabelSelector) {
|
||||
return (
|
||||
(selector.label?.entity &&
|
||||
ensureArray(selector.label.entity).some(
|
||||
(filter) => filter.integration
|
||||
)) ||
|
||||
(selector.label?.device &&
|
||||
ensureArray(selector.label.device).some((device) => device.integration))
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.selector.label?.multiple) {
|
||||
return html`
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-label-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-labels-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.pickLabelLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-labels-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector.label?.entity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ensureArray(this.selector.label.entity).some((filter) =>
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (!this.selector.label?.device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return ensureArray(this.selector.label.device).some((filter) =>
|
||||
filterSelectorDevices(filter, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-label": HaLabelSelector;
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ const LOAD_ELEMENTS = {
|
||||
entity: () => import("./ha-selector-entity"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
label: () => import("./ha-selector-label"),
|
||||
language: () => import("./ha-selector-language"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
number: () => import("./ha-selector-number"),
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
expandAreaTarget,
|
||||
expandDeviceTarget,
|
||||
expandLabelTarget,
|
||||
Selector,
|
||||
} from "../data/selector";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../types";
|
||||
@ -127,7 +128,8 @@ export class HaServiceControl extends LitElement {
|
||||
"target" in serviceData &&
|
||||
(this.value?.data?.entity_id ||
|
||||
this.value?.data?.area_id ||
|
||||
this.value?.data?.device_id)
|
||||
this.value?.data?.device_id ||
|
||||
this.value?.data?.label_id)
|
||||
) {
|
||||
const target = {
|
||||
...this.value.target,
|
||||
@ -142,6 +144,9 @@ export class HaServiceControl extends LitElement {
|
||||
if (this.value.data.device_id && !this.value.target?.device_id) {
|
||||
target.device_id = this.value.data.device_id;
|
||||
}
|
||||
if (this.value.data.label_id && !this.value.target?.label_id) {
|
||||
target.label_id = this.value.data.label_id;
|
||||
}
|
||||
|
||||
this._value = {
|
||||
...this.value,
|
||||
@ -152,6 +157,7 @@ export class HaServiceControl extends LitElement {
|
||||
delete this._value.data!.entity_id;
|
||||
delete this._value.data!.device_id;
|
||||
delete this._value.data!.area_id;
|
||||
delete this._value.data!.label_id;
|
||||
} else {
|
||||
this._value = this.value;
|
||||
}
|
||||
@ -263,6 +269,9 @@ export class HaServiceControl extends LitElement {
|
||||
const targetAreas = ensureArray(
|
||||
value?.target?.area_id || value?.data?.area_id
|
||||
)?.slice();
|
||||
const targetLabels = ensureArray(
|
||||
value?.target?.label_id || value?.data?.label_id
|
||||
)?.slice();
|
||||
if (targetAreas) {
|
||||
targetAreas.forEach((areaId) => {
|
||||
const expanded = expandAreaTarget(
|
||||
@ -288,6 +297,19 @@ export class HaServiceControl extends LitElement {
|
||||
);
|
||||
});
|
||||
}
|
||||
if (targetLabels) {
|
||||
targetLabels.forEach((labelId) => {
|
||||
const expanded = expandLabelTarget(
|
||||
this.hass,
|
||||
labelId,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
targetSelector
|
||||
);
|
||||
targetEntities.push(...expanded.entities);
|
||||
targetDevices.push(...expanded.devices);
|
||||
});
|
||||
}
|
||||
if (!targetEntities.length) {
|
||||
return false;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import "@material/mwc-menu/mwc-menu-surface";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiLabelMultiple,
|
||||
mdiPlus,
|
||||
mdiSofa,
|
||||
mdiUnfoldMoreVertical,
|
||||
@ -34,6 +35,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-label-picker";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
@ -70,7 +72,11 @@ export class HaTargetPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public addOnTop = false;
|
||||
|
||||
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
|
||||
@state() private _addMode?:
|
||||
| "area_id"
|
||||
| "entity_id"
|
||||
| "device_id"
|
||||
| "label_id";
|
||||
|
||||
@query("#input") private _inputElement?;
|
||||
|
||||
@ -123,6 +129,18 @@ export class HaTargetPicker extends LitElement {
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
${this.value?.label_id
|
||||
? ensureArray(this.value.label_id).map((labelId) => {
|
||||
const label = this.hass.labels![labelId];
|
||||
return this._renderChip(
|
||||
"label_id",
|
||||
labelId,
|
||||
label?.name || labelId,
|
||||
undefined,
|
||||
mdiLabelMultiple
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -190,6 +208,26 @@ export class HaTargetPicker extends LitElement {
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip label_id add"
|
||||
.type=${"label_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_label_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
</div>
|
||||
${this.helper
|
||||
@ -203,7 +241,7 @@ export class HaTargetPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _renderChip(
|
||||
type: "area_id" | "device_id" | "entity_id",
|
||||
type: "area_id" | "device_id" | "entity_id" | "label_id",
|
||||
id: string,
|
||||
name: string,
|
||||
entityState?: HassEntity,
|
||||
@ -320,7 +358,8 @@ export class HaTargetPicker extends LitElement {
|
||||
@click=${this._preventDefault}
|
||||
></ha-device-picker>
|
||||
`
|
||||
: html`
|
||||
: this._addMode === "entity_id"
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
@ -336,6 +375,24 @@ export class HaTargetPicker extends LitElement {
|
||||
@click=${this._preventDefault}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
`
|
||||
: html`
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"label_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_label_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeLabels=${ensureArray(this.value?.label_id)}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-label-picker>
|
||||
`}</mwc-menu-surface
|
||||
>`;
|
||||
}
|
||||
@ -405,6 +462,25 @@ export class HaTargetPicker extends LitElement {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else if (target.type === "label_id") {
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.labels.includes(target.id) &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.labels!.includes(target.id) &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@ -662,6 +738,14 @@ export class HaTargetPicker extends LitElement {
|
||||
.mdc-chip.entity_id.add {
|
||||
background: #d2e7b9;
|
||||
}
|
||||
.mdc-chip.label_id:not(.add) {
|
||||
border: 2px solid #eeefff;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.label_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.label_id.add {
|
||||
background: #eeefff;
|
||||
}
|
||||
.mdc-chip:hover {
|
||||
z-index: 5;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ const targetStruct = object({
|
||||
entity_id: optional(union([string(), array(string())])),
|
||||
device_id: optional(union([string(), array(string())])),
|
||||
area_id: optional(union([string(), array(string())])),
|
||||
label_id: optional(union([string(), array(string())])),
|
||||
});
|
||||
|
||||
export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
|
@ -85,6 +85,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
area_id: "areas",
|
||||
device_id: "devices",
|
||||
entity_id: "entities",
|
||||
label_id: "labels",
|
||||
})) {
|
||||
if (!(key in config.target)) {
|
||||
continue;
|
||||
@ -146,6 +147,13 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (key === "label_id") {
|
||||
const label_ = hass.labels[targetThing];
|
||||
if (label_?.name) {
|
||||
targets.push(label_.name);
|
||||
} else {
|
||||
targets.push("unknown label");
|
||||
}
|
||||
} else {
|
||||
targets.push(targetThing);
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ export type Selector =
|
||||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
| LabelSelector
|
||||
| LanguageSelector
|
||||
| LocationSelector
|
||||
| MediaSelector
|
||||
@ -157,6 +158,14 @@ export interface DeviceSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LabelSelector {
|
||||
label: {
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LegacyDeviceSelector {
|
||||
device: DeviceSelector["device"] & {
|
||||
/**
|
||||
@ -463,6 +472,45 @@ export const expandDeviceTarget = (
|
||||
return { entities: newEntities };
|
||||
};
|
||||
|
||||
export const expandLabelTarget = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string,
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"],
|
||||
targetSelector: TargetSelector,
|
||||
entitySources?: EntitySources
|
||||
) => {
|
||||
const newEntities: string[] = [];
|
||||
const newDevices: string[] = [];
|
||||
Object.values(devices).forEach((device) => {
|
||||
if (
|
||||
device.labels.includes(labelId) &&
|
||||
deviceMeetsTargetSelector(
|
||||
hass,
|
||||
Object.values(entities),
|
||||
device,
|
||||
targetSelector,
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
Object.values(entities).forEach((entity) => {
|
||||
if (
|
||||
entity.labels!.includes(labelId) &&
|
||||
entityMeetsTargetSelector(
|
||||
hass.states[entity.entity_id],
|
||||
targetSelector,
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
return { devices: newDevices, entities: newEntities };
|
||||
};
|
||||
|
||||
const deviceMeetsTargetSelector = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryDisplayEntry[],
|
||||
|
@ -347,6 +347,7 @@ export const provideHass = (
|
||||
areas: {},
|
||||
devices: {},
|
||||
entities: {},
|
||||
labels: {},
|
||||
formatEntityState: (stateObj, state) =>
|
||||
(state !== null ? state : stateObj.state) ?? "",
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
|
@ -404,13 +404,16 @@
|
||||
"expand": "Expand",
|
||||
"expand_area_id": "Split this area into separate devices and entities.",
|
||||
"expand_device_id": "Split this device into separate entities.",
|
||||
"expand_label_id": "Split this label into separate entities.",
|
||||
"remove": "Remove",
|
||||
"remove_area_id": "Remove area",
|
||||
"remove_device_id": "Remove device",
|
||||
"remove_entity_id": "Remove entity",
|
||||
"remove_label_id": "Remove label",
|
||||
"add_area_id": "Choose area",
|
||||
"add_device_id": "Choose device",
|
||||
"add_entity_id": "Choose entity"
|
||||
"add_entity_id": "Choose entity",
|
||||
"add_label_id": "Choose label"
|
||||
},
|
||||
"config-entry-picker": {
|
||||
"config_entry": "Integration"
|
||||
@ -476,6 +479,22 @@
|
||||
"failed_create_area": "Failed to create area."
|
||||
}
|
||||
},
|
||||
"label-picker": {
|
||||
"clear": "Clear",
|
||||
"show_labels": "Show labels",
|
||||
"label": "Label",
|
||||
"add_new_sugestion": "Add new label ''{name}''",
|
||||
"add_new": "Add new label…",
|
||||
"no_labels": "You don't have any labels",
|
||||
"no_match": "No matching labels found",
|
||||
"add_dialog": {
|
||||
"title": "Add new label",
|
||||
"text": "Enter the name of the new label.",
|
||||
"name": "Name",
|
||||
"add": "Add",
|
||||
"failed_create_label": "Failed to create label."
|
||||
}
|
||||
},
|
||||
"statistic-picker": {
|
||||
"statistic": "Statistic",
|
||||
"no_statistics": "You don't have any statistics",
|
||||
@ -574,7 +593,7 @@
|
||||
"service-control": {
|
||||
"required": "This field is required",
|
||||
"target": "Targets",
|
||||
"target_description": "What should this service use as targeted areas, devices or entities.",
|
||||
"target_description": "What should this service use as targeted areas, devices, entities or labels.",
|
||||
"data": "Service data",
|
||||
"integration_doc": "Integration documentation"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user