mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Add support for labels (#20189)
* Add support for labels * Update ha-label-picker.ts * Remove aliases from label * Use opacity for chips in labels picker * Fix label filtering in target picker * Update ha-labels-picker.ts * Update dialog-area-registry-detail.ts --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
b239ec2b71
commit
eb4ae926b7
@ -72,6 +72,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
id: "sensor.co2_intensity",
|
id: "sensor.co2_intensity",
|
||||||
name: null,
|
name: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
|
labels: [],
|
||||||
platform: "co2signal",
|
platform: "co2signal",
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
entity_category: null,
|
entity_category: null,
|
||||||
@ -88,6 +89,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
id: "sensor.co2_intensity",
|
id: "sensor.co2_intensity",
|
||||||
name: null,
|
name: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
|
labels: [],
|
||||||
platform: "co2signal",
|
platform: "co2signal",
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
entity_category: null,
|
entity_category: null,
|
||||||
|
@ -59,6 +59,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
@ -77,6 +78,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@ -95,6 +97,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -106,6 +109,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||||||
icon: null,
|
icon: null,
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "bedroom",
|
area_id: "bedroom",
|
||||||
@ -114,6 +118,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||||||
icon: "mdi:bed",
|
icon: "mdi:bed",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "livingroom",
|
area_id: "livingroom",
|
||||||
@ -122,6 +127,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||||||
icon: "mdi:sofa",
|
icon: "mdi:sofa",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
@ -73,6 +74,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@ -91,6 +93,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -102,6 +105,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||||||
icon: null,
|
icon: null,
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "bedroom",
|
area_id: "bedroom",
|
||||||
@ -110,6 +114,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||||||
icon: "mdi:bed",
|
icon: "mdi:bed",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "livingroom",
|
area_id: "livingroom",
|
||||||
@ -118,6 +123,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||||||
icon: "mdi:sofa",
|
icon: "mdi:sofa",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -406,6 +406,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
entity_id: "select.speed",
|
entity_id: "select.speed",
|
||||||
translation_key: "speed",
|
translation_key: "speed",
|
||||||
platform: "demo",
|
platform: "demo",
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -199,6 +199,7 @@ const createEntityRegistryEntries = (
|
|||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
unique_id: "updater",
|
unique_id: "updater",
|
||||||
options: null,
|
options: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -222,6 +223,7 @@ const createDeviceRegistryEntries = (
|
|||||||
name_by_user: null,
|
name_by_user: null,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -19,12 +19,16 @@ export class HaInputChip extends MdInputChip {
|
|||||||
var(--rgb-primary-text-color),
|
var(--rgb-primary-text-color),
|
||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
|
--ha-input-chip-selected-container-opacity: 1;
|
||||||
}
|
}
|
||||||
/** Set the size of mdc icons **/
|
/** Set the size of mdc icons **/
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
}
|
}
|
||||||
|
.selected::before {
|
||||||
|
opacity: var(--ha-input-chip-selected-container-opacity);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
picture: null,
|
picture: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -288,6 +289,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
picture: null,
|
picture: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -303,6 +305,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
picture: null,
|
picture: null,
|
||||||
icon: "mdi:plus",
|
icon: "mdi:plus",
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,15 @@ import "@material/mwc-list/mwc-list-item";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import {
|
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||||
computeCssColor,
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
THEME_COLORS,
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
} from "../../../common/color/compute-color";
|
import "./ha-select";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { HomeAssistant } from "../types";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { LocalizeKeys } from "../common/translations/localize";
|
||||||
import "../../../components/ha-select";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
|
||||||
|
|
||||||
@customElement("hui-color-picker")
|
@customElement("ha-color-picker")
|
||||||
export class HuiColorPicker extends LitElement {
|
export class HaColorPicker extends LitElement {
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
@ -21,6 +19,8 @@ export class HuiColorPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public defaultColor = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
_valueSelected(ev) {
|
_valueSelected(ev) {
|
||||||
@ -52,16 +52,16 @@ export class HuiColorPicker extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<mwc-list-item value="default">
|
${this.defaultColor
|
||||||
${this.hass.localize(
|
? html` <mwc-list-item value="default">
|
||||||
`ui.panel.lovelace.editor.color-picker.default_color`
|
${this.hass.localize(`ui.components.color-picker.default_color`)}
|
||||||
)}
|
</mwc-list-item>`
|
||||||
</mwc-list-item>
|
: nothing}
|
||||||
${Array.from(THEME_COLORS).map(
|
${Array.from(THEME_COLORS).map(
|
||||||
(color) => html`
|
(color) => html`
|
||||||
<mwc-list-item .value=${color} graphic="icon">
|
<mwc-list-item .value=${color} graphic="icon">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`ui.panel.lovelace.editor.color-picker.colors.${color}`
|
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||||
) || color}
|
) || color}
|
||||||
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
@ -100,6 +100,6 @@ export class HuiColorPicker extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hui-color-picker": HuiColorPicker;
|
"ha-color-picker": HaColorPicker;
|
||||||
}
|
}
|
||||||
}
|
}
|
484
src/components/ha-label-picker.ts
Normal file
484
src/components/ha-label-picker.ts
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, PropertyValues, TemplateResult, html, nothing } 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 {
|
||||||
|
ScorableTextItem,
|
||||||
|
fuzzyFilterSort,
|
||||||
|
} from "../common/string/filter/sequence-matching";
|
||||||
|
import {
|
||||||
|
DeviceEntityDisplayLookup,
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
getDeviceEntityDisplayLookup,
|
||||||
|
} from "../data/device_registry";
|
||||||
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } 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-list-item";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
type ScorableLabelRegistryEntry = ScorableTextItem & LabelRegistryEntry;
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
class=${classMap({ "add-new": item.label_id === "add_new" })}
|
||||||
|
>
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${item.name}
|
||||||
|
</ha-list-item>`;
|
||||||
|
|
||||||
|
@customElement("ha-label-picker")
|
||||||
|
export class HaLabelPicker 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 = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only labels with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no labels 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-label" })
|
||||||
|
public excludeLabels?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@state() private _labels?: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLabels = memoizeOne(
|
||||||
|
(
|
||||||
|
labels: LabelRegistryEntry[],
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
|
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: 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.length > 0);
|
||||||
|
|
||||||
|
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;
|
||||||
|
const usedLabels = new Set<string>();
|
||||||
|
|
||||||
|
let areaIds: string[] | undefined;
|
||||||
|
|
||||||
|
if (inputDevices) {
|
||||||
|
areaIds = inputDevices
|
||||||
|
.filter((device) => device.area_id)
|
||||||
|
.map((device) => device.area_id!);
|
||||||
|
|
||||||
|
inputDevices.forEach((device) => {
|
||||||
|
device.labels.forEach((label) => usedLabels.add(label));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputEntities) {
|
||||||
|
areaIds = (areaIds ?? []).concat(
|
||||||
|
inputEntities
|
||||||
|
.filter((entity) => entity.area_id)
|
||||||
|
.map((entity) => entity.area_id!)
|
||||||
|
);
|
||||||
|
inputEntities.forEach((entity) => {
|
||||||
|
entity.labels.forEach((label) => usedLabels.add(label));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds) {
|
||||||
|
areaIds.forEach((areaId) => {
|
||||||
|
const area = areas[areaId];
|
||||||
|
area.labels.forEach((label) => usedLabels.add(label));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeLabels) {
|
||||||
|
outputLabels = outputLabels.filter(
|
||||||
|
(label) => !excludeLabels!.includes(label.label_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputDevices || inputEntities) {
|
||||||
|
outputLabels = outputLabels.filter((label) =>
|
||||||
|
usedLabels.has(label.label_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputLabels.length) {
|
||||||
|
outputLabels = [
|
||||||
|
{
|
||||||
|
label_id: "no_labels",
|
||||||
|
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
color: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return noAdd
|
||||||
|
? outputLabels
|
||||||
|
: [
|
||||||
|
...outputLabels,
|
||||||
|
{
|
||||||
|
label_id: "add_new",
|
||||||
|
name: this.hass.localize("ui.components.label-picker.add_new"),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
color: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this._init && this.hass && this._labels) ||
|
||||||
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
|
) {
|
||||||
|
this._init = true;
|
||||||
|
const labels = this._getLabels(
|
||||||
|
this._labels!,
|
||||||
|
this.hass.areas,
|
||||||
|
Object.values(this.hass.devices),
|
||||||
|
Object.values(this.hass.entities),
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeLabels
|
||||||
|
).map((label) => ({
|
||||||
|
...label,
|
||||||
|
strings: [label.label_id, label.name],
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.comboBox.items = labels;
|
||||||
|
this.comboBox.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._labels?.find((label) => label.label_id === 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 target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = fuzzyFilterSort<ScorableLabelRegistryEntry>(
|
||||||
|
filterString,
|
||||||
|
target.items || []
|
||||||
|
);
|
||||||
|
if (!this.noAdd && filteredItems?.length === 0) {
|
||||||
|
this._suggestion = filterString;
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
label_id: "add_new_suggestion",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.label-picker.add_new_sugestion",
|
||||||
|
{ name: this._suggestion }
|
||||||
|
),
|
||||||
|
picture: 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 = "";
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
showLabelDetailDialog(this, {
|
||||||
|
entry: undefined,
|
||||||
|
suggestedName: newValue === "add_new_suggestion" ? this._suggestion : "",
|
||||||
|
createEntry: async (values) => {
|
||||||
|
const label = await createLabelRegistryEntry(this.hass, values);
|
||||||
|
const labels = [...this._labels!, label];
|
||||||
|
this.comboBox.filteredItems = this._getLabels(
|
||||||
|
labels,
|
||||||
|
this.hass.areas!,
|
||||||
|
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);
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._suggestion = undefined;
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
213
src/components/ha-labels-picker.ts
Normal file
213
src/components/ha-labels-picker.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
updateLabelRegistryEntry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import "./chips/ha-chip-set";
|
||||||
|
import "./chips/ha-input-chip";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-label-picker";
|
||||||
|
import type { HaLabelPicker } from "./ha-label-picker";
|
||||||
|
|
||||||
|
@customElement("ha-labels-picker")
|
||||||
|
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-add" })
|
||||||
|
public noAdd = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only labels with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no labels 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-label" })
|
||||||
|
public excludeLabels?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _labels?: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@query("ha-label-picker", true) public labelPicker!: HaLabelPicker;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.labelPicker?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.labelPicker?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${this.value?.length
|
||||||
|
? html`<ha-chip-set>
|
||||||
|
${repeat(
|
||||||
|
this.value,
|
||||||
|
(item) => item,
|
||||||
|
(item, idx) => {
|
||||||
|
const label = this._labels?.find(
|
||||||
|
(lbl) => lbl.label_id === item
|
||||||
|
);
|
||||||
|
const color = label?.color
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
return html`
|
||||||
|
<ha-input-chip
|
||||||
|
.idx=${idx}
|
||||||
|
.item=${label}
|
||||||
|
@remove=${this._removeItem}
|
||||||
|
@click=${this._openDetail}
|
||||||
|
.label=${label?.name}
|
||||||
|
selected
|
||||||
|
style=${color ? `--color: ${color}` : ""}
|
||||||
|
>
|
||||||
|
${label?.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="icon"
|
||||||
|
.icon=${label.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-input-chip>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</ha-chip-set>`
|
||||||
|
: nothing}
|
||||||
|
<ha-label-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.label-picker.add_label")
|
||||||
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.excludeLabels=${this.value}
|
||||||
|
@value-changed=${this._labelChanged}
|
||||||
|
>
|
||||||
|
</ha-label-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeItem(ev) {
|
||||||
|
this._value.splice(ev.target.idx, 1);
|
||||||
|
this._setValue([...this._value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openDetail(ev) {
|
||||||
|
const label = ev.target.item;
|
||||||
|
showLabelDetailDialog(this, {
|
||||||
|
entry: label,
|
||||||
|
updateEntry: async (values) => {
|
||||||
|
const updated = await updateLabelRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
label.label_id,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
this._labels = this._labels!.map((lbl) =>
|
||||||
|
lbl.label_id === updated.label_id ? updated : lbl
|
||||||
|
);
|
||||||
|
return updated;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (!newValue || this._value.includes(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setValue([...this._value, newValue]);
|
||||||
|
this.labelPicker.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value?: string[]) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-chip-set {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
ha-input-chip {
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
--md-input-chip-selected-container-color: var(--color);
|
||||||
|
--ha-input-chip-selected-container-opacity: 0.3;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-labels-picker": HaLabelsPicker;
|
||||||
|
}
|
||||||
|
}
|
83
src/components/ha-selector/ha-selector-label.ts
Normal file
83
src/components/ha-selector/ha-selector-label.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { LabelSelector } from "../../data/selector";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-labels-picker";
|
||||||
|
|
||||||
|
@customElement("ha-selector-label")
|
||||||
|
export class HaLabelSelector extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public value?: string | string[];
|
||||||
|
|
||||||
|
@property() public name?: string;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public selector!: LabelSelector;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (this.selector.label.multiple) {
|
||||||
|
return html`
|
||||||
|
<ha-labels-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${ensureArray(this.value ?? [])}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.label=${this.label}
|
||||||
|
@value-changed=${this._handleChange}
|
||||||
|
>
|
||||||
|
</ha-labels-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-label-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.label=${this.label}
|
||||||
|
@value-changed=${this._handleChange}
|
||||||
|
>
|
||||||
|
</ha-label-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleChange(ev) {
|
||||||
|
let value = ev.detail.value;
|
||||||
|
if (this.value === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(value === "" || (Array.isArray(value) && value.length === 0)) &&
|
||||||
|
!this.required
|
||||||
|
) {
|
||||||
|
value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-labels-picker {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-selector-label": HaLabelSelector;
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { UiColorSelector } from "../../data/selector";
|
import { UiColorSelector } from "../../data/selector";
|
||||||
import "../../panels/lovelace/components/hui-color-picker";
|
import "../ha-color-picker";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
@customElement("ha-selector-ui_color")
|
@customElement("ha-selector-ui_color")
|
||||||
@ -19,13 +19,14 @@ export class HaSelectorUiColor extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<hui-color-picker
|
<ha-color-picker
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.defaultColor=${this.selector.ui_color?.default_color}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></hui-color-picker>
|
></ha-color-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ const LOAD_ELEMENTS = {
|
|||||||
entity: () => import("./ha-selector-entity"),
|
entity: () => import("./ha-selector-entity"),
|
||||||
statistic: () => import("./ha-selector-statistic"),
|
statistic: () => import("./ha-selector-statistic"),
|
||||||
file: () => import("./ha-selector-file"),
|
file: () => import("./ha-selector-file"),
|
||||||
|
label: () => import("./ha-selector-label"),
|
||||||
language: () => import("./ha-selector-language"),
|
language: () => import("./ha-selector-language"),
|
||||||
navigation: () => import("./ha-selector-navigation"),
|
navigation: () => import("./ha-selector-navigation"),
|
||||||
number: () => import("./ha-selector-number"),
|
number: () => import("./ha-selector-number"),
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
expandAreaTarget,
|
expandAreaTarget,
|
||||||
expandDeviceTarget,
|
expandDeviceTarget,
|
||||||
expandFloorTarget,
|
expandFloorTarget,
|
||||||
|
expandLabelTarget,
|
||||||
Selector,
|
Selector,
|
||||||
} from "../data/selector";
|
} from "../data/selector";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
@ -274,6 +275,24 @@ export class HaServiceControl extends LitElement {
|
|||||||
const targetFloors = ensureArray(
|
const targetFloors = ensureArray(
|
||||||
value?.target?.floor_id || value?.data?.floor_id
|
value?.target?.floor_id || value?.data?.floor_id
|
||||||
)?.slice();
|
)?.slice();
|
||||||
|
const targetLabels = ensureArray(
|
||||||
|
value?.target?.label_id || value?.data?.label_id
|
||||||
|
)?.slice();
|
||||||
|
if (targetLabels) {
|
||||||
|
targetLabels.forEach((labelId) => {
|
||||||
|
const expanded = expandLabelTarget(
|
||||||
|
this.hass,
|
||||||
|
labelId,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
|
targetSelector
|
||||||
|
);
|
||||||
|
targetDevices.push(...expanded.devices);
|
||||||
|
targetEntities.push(...expanded.entities);
|
||||||
|
targetAreas.push(...expanded.areas);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (targetFloors) {
|
if (targetFloors) {
|
||||||
targetFloors.forEach((floorId) => {
|
targetFloors.forEach((floorId) => {
|
||||||
const expanded = expandFloorTarget(
|
const expanded = expandFloorTarget(
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
mdiClose,
|
mdiClose,
|
||||||
mdiDevices,
|
mdiDevices,
|
||||||
mdiFloorPlan,
|
mdiFloorPlan,
|
||||||
|
mdiLabel,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiSofa,
|
mdiSofa,
|
||||||
mdiUnfoldMoreVertical,
|
mdiUnfoldMoreVertical,
|
||||||
@ -45,7 +46,13 @@ import {
|
|||||||
FloorRegistryEntry,
|
FloorRegistryEntry,
|
||||||
subscribeFloorRegistry,
|
subscribeFloorRegistry,
|
||||||
} from "../data/floor_registry";
|
} from "../data/floor_registry";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
import { AreaRegistryEntry } from "../data/area_registry";
|
import { AreaRegistryEntry } from "../data/area_registry";
|
||||||
|
import { hex2rgb } from "../common/color/convert-color";
|
||||||
|
|
||||||
@customElement("ha-target-picker")
|
@customElement("ha-target-picker")
|
||||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||||
@ -83,7 +90,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public addOnTop = false;
|
@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?;
|
@query("#input") private _inputElement?;
|
||||||
|
|
||||||
@ -91,6 +102,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
|
@state() private _labels?: LabelRegistryEntry[];
|
||||||
|
|
||||||
private _opened = false;
|
private _opened = false;
|
||||||
|
|
||||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
@ -98,6 +111,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||||
this._floors = floors;
|
this._floors = floors;
|
||||||
}),
|
}),
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +154,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
mdiSofa
|
mdiSofa
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: ""}
|
: nothing}
|
||||||
${this.value?.device_id
|
${this.value?.device_id
|
||||||
? ensureArray(this.value.device_id).map((device_id) => {
|
? ensureArray(this.value.device_id).map((device_id) => {
|
||||||
const device = this.hass.devices![device_id];
|
const device = this.hass.devices![device_id];
|
||||||
@ -151,7 +167,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
mdiDevices
|
mdiDevices
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: ""}
|
: nothing}
|
||||||
${this.value?.entity_id
|
${this.value?.entity_id
|
||||||
? ensureArray(this.value.entity_id).map((entity_id) => {
|
? ensureArray(this.value.entity_id).map((entity_id) => {
|
||||||
const entity = this.hass.states[entity_id];
|
const entity = this.hass.states[entity_id];
|
||||||
@ -162,7 +178,35 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
entity
|
entity
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: ""}
|
: nothing}
|
||||||
|
${this.value?.label_id
|
||||||
|
? ensureArray(this.value.label_id).map((label_id) => {
|
||||||
|
const label = this._labels?.find(
|
||||||
|
(lbl) => lbl.label_id === label_id
|
||||||
|
);
|
||||||
|
let color = label?.color
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
if (color?.startsWith("var(")) {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
color = computedStyles.getPropertyValue(
|
||||||
|
color.substring(4, color.length - 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (color?.startsWith("#")) {
|
||||||
|
color = hex2rgb(color).join(",");
|
||||||
|
}
|
||||||
|
return this._renderChip(
|
||||||
|
"label_id",
|
||||||
|
label_id,
|
||||||
|
label ? label.name : label_id,
|
||||||
|
undefined,
|
||||||
|
label?.icon,
|
||||||
|
mdiLabel,
|
||||||
|
color
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -230,6 +274,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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()}
|
${this._renderPicker()}
|
||||||
</div>
|
</div>
|
||||||
${this.helper
|
${this.helper
|
||||||
@ -243,18 +307,22 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderChip(
|
private _renderChip(
|
||||||
type: "floor_id" | "area_id" | "device_id" | "entity_id",
|
type: "floor_id" | "area_id" | "device_id" | "entity_id" | "label_id",
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
entityState?: HassEntity,
|
entityState?: HassEntity,
|
||||||
icon?: string | null,
|
icon?: string | null,
|
||||||
fallbackIconPath?: string
|
fallbackIconPath?: string,
|
||||||
|
color?: string
|
||||||
) {
|
) {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="mdc-chip ${classMap({
|
class="mdc-chip ${classMap({
|
||||||
[type]: true,
|
[type]: true,
|
||||||
})}"
|
})}"
|
||||||
|
style=${color
|
||||||
|
? `--color: rgb(${color}); --background-color: rgba(${color}, .3)`
|
||||||
|
: ""}
|
||||||
>
|
>
|
||||||
${icon
|
${icon
|
||||||
? html`<ha-icon
|
? html`<ha-icon
|
||||||
@ -368,23 +436,42 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
@click=${this._preventDefault}
|
@click=${this._preventDefault}
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
`
|
`
|
||||||
: html`
|
: this._addMode === "label_id"
|
||||||
<ha-entity-picker
|
? html`
|
||||||
.hass=${this.hass}
|
<ha-label-picker
|
||||||
id="input"
|
.hass=${this.hass}
|
||||||
.type=${"entity_id"}
|
id="input"
|
||||||
.label=${this.hass.localize(
|
.type=${"label_id"}
|
||||||
"ui.components.target-picker.add_entity_id"
|
.label=${this.hass.localize(
|
||||||
)}
|
"ui.components.target-picker.add_label_id"
|
||||||
.entityFilter=${this.entityFilter}
|
)}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
no-add
|
||||||
.includeDomains=${this.includeDomains}
|
.deviceFilter=${this.deviceFilter}
|
||||||
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
.entityFilter=${this.entityFilter}
|
||||||
@value-changed=${this._targetPicked}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
@click=${this._preventDefault}
|
.includeDomains=${this.includeDomains}
|
||||||
allow-custom-entity
|
.excludeLabels=${ensureArray(this.value?.label_id)}
|
||||||
></ha-entity-picker>
|
@value-changed=${this._targetPicked}
|
||||||
`}</mwc-menu-surface
|
@click=${this._preventDefault}
|
||||||
|
></ha-label-picker>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-entity-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
id="input"
|
||||||
|
.type=${"entity_id"}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.components.target-picker.add_entity_id"
|
||||||
|
)}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
||||||
|
@value-changed=${this._targetPicked}
|
||||||
|
@click=${this._preventDefault}
|
||||||
|
allow-custom-entity
|
||||||
|
></ha-entity-picker>
|
||||||
|
`}</mwc-menu-surface
|
||||||
>`;
|
>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,6 +558,34 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
newEntities.push(entity.entity_id);
|
newEntities.push(entity.entity_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (target.type === "label_id") {
|
||||||
|
Object.values(this.hass.areas).forEach((area) => {
|
||||||
|
if (
|
||||||
|
area.labels.includes(target.id) &&
|
||||||
|
!this.value!.area_id?.includes(area.area_id) &&
|
||||||
|
this._areaMeetsFilter(area)
|
||||||
|
) {
|
||||||
|
newAreas.push(area.area_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 {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -578,39 +693,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
(entity) => entity.device_id === device.id
|
(entity) => entity.device_id === device.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.includeDomains) {
|
if (!devEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
|
||||||
if (!devEntities || !devEntities.length) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!devEntities.some((entity) =>
|
|
||||||
this.includeDomains!.includes(computeDomain(entity.entity_id))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.includeDeviceClasses) {
|
|
||||||
if (!devEntities || !devEntities.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!devEntities.some((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (!stateObj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
stateObj.attributes.device_class &&
|
|
||||||
this.includeDeviceClasses!.includes(
|
|
||||||
stateObj.attributes.device_class
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.deviceFilter) {
|
if (this.deviceFilter) {
|
||||||
@ -619,19 +703,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.entityFilter) {
|
|
||||||
if (
|
|
||||||
!devEntities.some((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (!stateObj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.entityFilter!(stateObj);
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,8 +790,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin-left: -14px !important;
|
margin-left: -13px !important;
|
||||||
margin-inline-start: -14px !important;
|
margin-inline-start: -13px !important;
|
||||||
margin-inline-end: 4px !important;
|
margin-inline-end: 4px !important;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
@ -731,7 +802,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
.mdc-chip.area_id:not(.add),
|
.mdc-chip.area_id:not(.add),
|
||||||
.mdc-chip.floor_id:not(.add) {
|
.mdc-chip.floor_id:not(.add) {
|
||||||
border: 2px solid #fed6a4;
|
border: 1px solid #fed6a4;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
}
|
}
|
||||||
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
||||||
@ -741,7 +812,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
background: #fed6a4;
|
background: #fed6a4;
|
||||||
}
|
}
|
||||||
.mdc-chip.device_id:not(.add) {
|
.mdc-chip.device_id:not(.add) {
|
||||||
border: 2px solid #a8e1fb;
|
border: 1px solid #a8e1fb;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
}
|
}
|
||||||
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
||||||
@ -749,13 +820,21 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
background: #a8e1fb;
|
background: #a8e1fb;
|
||||||
}
|
}
|
||||||
.mdc-chip.entity_id:not(.add) {
|
.mdc-chip.entity_id:not(.add) {
|
||||||
border: 2px solid #d2e7b9;
|
border: 1px solid #d2e7b9;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
}
|
}
|
||||||
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
||||||
.mdc-chip.entity_id.add {
|
.mdc-chip.entity_id.add {
|
||||||
background: #d2e7b9;
|
background: #d2e7b9;
|
||||||
}
|
}
|
||||||
|
.mdc-chip.label_id:not(.add) {
|
||||||
|
border: 1px solid var(--color, #e0e0e0);
|
||||||
|
background: var(--card-background-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.label_id:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.label_id.add {
|
||||||
|
background: var(--background-color, #e0e0e0);
|
||||||
|
}
|
||||||
.mdc-chip:hover {
|
.mdc-chip:hover {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ export interface AreaRegistryEntry {
|
|||||||
name: string;
|
name: string;
|
||||||
picture: string | null;
|
picture: string | null;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
labels: string[];
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export interface AreaRegistryEntryMutableParams {
|
|||||||
picture?: string | null;
|
picture?: string | null;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
|
labels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createAreaRegistryEntry = (
|
export const createAreaRegistryEntry = (
|
||||||
|
@ -20,6 +20,7 @@ export interface DeviceRegistryEntry {
|
|||||||
manufacturer: string | null;
|
manufacturer: string | null;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
|
labels: string[];
|
||||||
sw_version: string | null;
|
sw_version: string | null;
|
||||||
hw_version: string | null;
|
hw_version: string | null;
|
||||||
serial_number: string | null;
|
serial_number: string | null;
|
||||||
@ -43,6 +44,7 @@ export interface DeviceRegistryEntryMutableParams {
|
|||||||
area_id?: string | null;
|
area_id?: string | null;
|
||||||
name_by_user?: string | null;
|
name_by_user?: string | null;
|
||||||
disabled_by?: string | null;
|
disabled_by?: string | null;
|
||||||
|
labels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fallbackDeviceName = (
|
export const fallbackDeviceName = (
|
||||||
@ -140,7 +142,7 @@ export const getDeviceEntityDisplayLookup = (
|
|||||||
|
|
||||||
export const getDeviceIntegrationLookup = (
|
export const getDeviceIntegrationLookup = (
|
||||||
entitySources: EntitySources,
|
entitySources: EntitySources,
|
||||||
entities: EntityRegistryDisplayEntry[]
|
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[]
|
||||||
): Record<string, string[]> => {
|
): Record<string, string[]> => {
|
||||||
const deviceIntegrations: Record<string, string[]> = {};
|
const deviceIntegrations: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export interface EntityRegistryDisplayEntry {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
device_id?: string;
|
device_id?: string;
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
|
labels: string[];
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
entity_category?: entityCategory;
|
entity_category?: entityCategory;
|
||||||
translation_key?: string;
|
translation_key?: string;
|
||||||
@ -30,6 +31,7 @@ export interface EntityRegistryDisplayEntryResponse {
|
|||||||
ei: string;
|
ei: string;
|
||||||
di?: string;
|
di?: string;
|
||||||
ai?: string;
|
ai?: string;
|
||||||
|
lb: string[];
|
||||||
ec?: number;
|
ec?: number;
|
||||||
en?: string;
|
en?: string;
|
||||||
ic?: string;
|
ic?: string;
|
||||||
@ -50,6 +52,7 @@ export interface EntityRegistryEntry {
|
|||||||
config_entry_id: string | null;
|
config_entry_id: string | null;
|
||||||
device_id: string | null;
|
device_id: string | null;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
|
labels: string[];
|
||||||
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
|
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
|
||||||
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
|
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
|
||||||
entity_category: entityCategory | null;
|
entity_category: entityCategory | null;
|
||||||
@ -133,6 +136,7 @@ export interface EntityRegistryEntryUpdateParams {
|
|||||||
| WeatherEntityOptions
|
| WeatherEntityOptions
|
||||||
| LightEntityOptions;
|
| LightEntityOptions;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
|
labels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const batteryPriorities = ["sensor", "binary_sensor"];
|
const batteryPriorities = ["sensor", "binary_sensor"];
|
||||||
|
86
src/data/label_registry.ts
Normal file
86
src/data/label_registry.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||||
|
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import { debounce } from "../common/util/debounce";
|
||||||
|
|
||||||
|
export interface LabelRegistryEntry {
|
||||||
|
label_id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
color: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelRegistryEntryMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon?: string | null;
|
||||||
|
color?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchLabelRegistry = (conn: Connection) =>
|
||||||
|
conn
|
||||||
|
.sendMessagePromise({
|
||||||
|
type: "config/label_registry/list",
|
||||||
|
})
|
||||||
|
.then((labels) =>
|
||||||
|
(labels as LabelRegistryEntry[]).sort((ent1, ent2) =>
|
||||||
|
stringCompare(ent1.name, ent2.name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const subscribeLabelRegistryUpdates = (
|
||||||
|
conn: Connection,
|
||||||
|
store: Store<LabelRegistryEntry[]>
|
||||||
|
) =>
|
||||||
|
conn.subscribeEvents(
|
||||||
|
debounce(
|
||||||
|
() =>
|
||||||
|
fetchLabelRegistry(conn).then((labels: LabelRegistryEntry[]) =>
|
||||||
|
store.setState(labels, true)
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
"label_registry_updated"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const subscribeLabelRegistry = (
|
||||||
|
conn: Connection,
|
||||||
|
onChange: (labels: LabelRegistryEntry[]) => void
|
||||||
|
) =>
|
||||||
|
createCollection<LabelRegistryEntry[]>(
|
||||||
|
"_labelRegistry",
|
||||||
|
fetchLabelRegistry,
|
||||||
|
subscribeLabelRegistryUpdates,
|
||||||
|
conn,
|
||||||
|
onChange
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createLabelRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: LabelRegistryEntryMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<LabelRegistryEntry>({
|
||||||
|
type: "config/label_registry/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateLabelRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
labelId: string,
|
||||||
|
updates: Partial<LabelRegistryEntryMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<LabelRegistryEntry>({
|
||||||
|
type: "config/label_registry/update",
|
||||||
|
label_id: labelId,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteLabelRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
labelId: string
|
||||||
|
) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "config/label_registry/delete",
|
||||||
|
label_id: labelId,
|
||||||
|
});
|
@ -8,7 +8,10 @@ import {
|
|||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
getDeviceIntegrationLookup,
|
getDeviceIntegrationLookup,
|
||||||
} from "./device_registry";
|
} from "./device_registry";
|
||||||
import { EntityRegistryDisplayEntry } from "./entity_registry";
|
import {
|
||||||
|
EntityRegistryDisplayEntry,
|
||||||
|
EntityRegistryEntry,
|
||||||
|
} from "./entity_registry";
|
||||||
import { EntitySources } from "./entity_sources";
|
import { EntitySources } from "./entity_sources";
|
||||||
|
|
||||||
export type Selector =
|
export type Selector =
|
||||||
@ -34,6 +37,7 @@ export type Selector =
|
|||||||
| LegacyEntitySelector
|
| LegacyEntitySelector
|
||||||
| FileSelector
|
| FileSelector
|
||||||
| IconSelector
|
| IconSelector
|
||||||
|
| LabelSelector
|
||||||
| LanguageSelector
|
| LanguageSelector
|
||||||
| LocationSelector
|
| LocationSelector
|
||||||
| MediaSelector
|
| MediaSelector
|
||||||
@ -242,6 +246,12 @@ export interface IconSelector {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LabelSelector {
|
||||||
|
label: {
|
||||||
|
multiple?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface LanguageSelector {
|
export interface LanguageSelector {
|
||||||
language: {
|
language: {
|
||||||
languages?: string[];
|
languages?: string[];
|
||||||
@ -421,9 +431,69 @@ export interface UiActionSelector {
|
|||||||
|
|
||||||
export interface UiColorSelector {
|
export interface UiColorSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
ui_color: {} | null;
|
ui_color: { default_color?: boolean } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const expandLabelTarget = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
labelId: string,
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
|
devices: HomeAssistant["devices"],
|
||||||
|
entities: HomeAssistant["entities"],
|
||||||
|
targetSelector: TargetSelector,
|
||||||
|
entitySources?: EntitySources
|
||||||
|
) => {
|
||||||
|
const newEntities: string[] = [];
|
||||||
|
const newDevices: string[] = [];
|
||||||
|
const newAreas: string[] = [];
|
||||||
|
|
||||||
|
Object.values(areas).forEach((area) => {
|
||||||
|
if (
|
||||||
|
area.labels.includes(labelId) &&
|
||||||
|
areaMeetsTargetSelector(
|
||||||
|
hass,
|
||||||
|
entities,
|
||||||
|
devices,
|
||||||
|
area.area_id,
|
||||||
|
targetSelector,
|
||||||
|
entitySources
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
newAreas.push(area.area_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { areas: newAreas, devices: newDevices, entities: newEntities };
|
||||||
|
};
|
||||||
|
|
||||||
export const expandFloorTarget = (
|
export const expandFloorTarget = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
floorId: string,
|
floorId: string,
|
||||||
@ -555,7 +625,7 @@ export const areaMeetsTargetSelector = (
|
|||||||
|
|
||||||
export const deviceMeetsTargetSelector = (
|
export const deviceMeetsTargetSelector = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryDisplayEntry[],
|
entityRegistry: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
|
||||||
device: DeviceRegistryEntry,
|
device: DeviceRegistryEntry,
|
||||||
targetSelector: TargetSelector,
|
targetSelector: TargetSelector,
|
||||||
entitySources?: EntitySources
|
entitySources?: EntitySources
|
||||||
|
@ -12,6 +12,7 @@ import "../../../components/ha-settings-row";
|
|||||||
import "../../../components/ha-icon-picker";
|
import "../../../components/ha-icon-picker";
|
||||||
import "../../../components/ha-floor-picker";
|
import "../../../components/ha-floor-picker";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
|
import "../../../components/ha-labels-picker";
|
||||||
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
|
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
|
||||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||||
import { haStyleDialog } from "../../../resources/styles";
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
@ -32,6 +33,8 @@ class DialogAreaDetail extends LitElement {
|
|||||||
|
|
||||||
@state() private _aliases!: string[];
|
@state() private _aliases!: string[];
|
||||||
|
|
||||||
|
@state() private _labels!: string[];
|
||||||
|
|
||||||
@state() private _picture!: string | null;
|
@state() private _picture!: string | null;
|
||||||
|
|
||||||
@state() private _icon!: string | null;
|
@state() private _icon!: string | null;
|
||||||
@ -51,6 +54,7 @@ class DialogAreaDetail extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._name = this._params.entry ? this._params.entry.name : "";
|
this._name = this._params.entry ? this._params.entry.name : "";
|
||||||
this._aliases = this._params.entry ? this._params.entry.aliases : [];
|
this._aliases = this._params.entry ? this._params.entry.aliases : [];
|
||||||
|
this._labels = this._params.entry ? this._params.entry.labels : [];
|
||||||
this._picture = this._params.entry?.picture || null;
|
this._picture = this._params.entry?.picture || null;
|
||||||
this._icon = this._params.entry?.icon || null;
|
this._icon = this._params.entry?.icon || null;
|
||||||
this._floor = this._params.entry?.floor_id || null;
|
this._floor = this._params.entry?.floor_id || null;
|
||||||
@ -123,6 +127,12 @@ class DialogAreaDetail extends LitElement {
|
|||||||
.label=${this.hass.localize("ui.panel.config.areas.editor.floor")}
|
.label=${this.hass.localize("ui.panel.config.areas.editor.floor")}
|
||||||
></ha-floor-picker>
|
></ha-floor-picker>
|
||||||
|
|
||||||
|
<ha-labels-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._labels}
|
||||||
|
@value-changed=${this._labelsChanged}
|
||||||
|
></ha-labels-picker>
|
||||||
|
|
||||||
<ha-picture-upload
|
<ha-picture-upload
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._picture}
|
.value=${this._picture}
|
||||||
@ -184,6 +194,11 @@ class DialogAreaDetail extends LitElement {
|
|||||||
this._icon = ev.detail.value;
|
this._icon = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _labelsChanged(ev) {
|
||||||
|
this._error = undefined;
|
||||||
|
this._labels = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._picture = (ev.target as HaPictureUpload).value;
|
this._picture = (ev.target as HaPictureUpload).value;
|
||||||
@ -198,6 +213,7 @@ class DialogAreaDetail extends LitElement {
|
|||||||
picture: this._picture || (create ? undefined : null),
|
picture: this._picture || (create ? undefined : null),
|
||||||
icon: this._icon || (create ? undefined : null),
|
icon: this._icon || (create ? undefined : null),
|
||||||
floor_id: this._floor || (create ? undefined : null),
|
floor_id: this._floor || (create ? undefined : null),
|
||||||
|
labels: this._labels || null,
|
||||||
aliases: this._aliases,
|
aliases: this._aliases,
|
||||||
};
|
};
|
||||||
if (create) {
|
if (create) {
|
||||||
@ -226,6 +242,7 @@ class DialogAreaDetail extends LitElement {
|
|||||||
ha-textfield,
|
ha-textfield,
|
||||||
ha-icon-picker,
|
ha-icon-picker,
|
||||||
ha-floor-picker,
|
ha-floor-picker,
|
||||||
|
ha-labels-picker,
|
||||||
ha-picture-upload {
|
ha-picture-upload {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
@ -5,6 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
|||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-area-picker";
|
import "../../../../components/ha-area-picker";
|
||||||
import "../../../../components/ha-dialog";
|
import "../../../../components/ha-dialog";
|
||||||
|
import "../../../../components/ha-labels-picker";
|
||||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||||
import "../../../../components/ha-textfield";
|
import "../../../../components/ha-textfield";
|
||||||
import {
|
import {
|
||||||
@ -27,6 +28,8 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
|
|
||||||
@state() private _areaId!: string;
|
@state() private _areaId!: string;
|
||||||
|
|
||||||
|
@state() private _labels!: string[];
|
||||||
|
|
||||||
@state() private _disabledBy!: DeviceRegistryEntry["disabled_by"];
|
@state() private _disabledBy!: DeviceRegistryEntry["disabled_by"];
|
||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
@ -38,6 +41,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._nameByUser = this._params.device.name_by_user || "";
|
this._nameByUser = this._params.device.name_by_user || "";
|
||||||
this._areaId = this._params.device.area_id || "";
|
this._areaId = this._params.device.area_id || "";
|
||||||
|
this._labels = this._params.device.labels || [];
|
||||||
this._disabledBy = this._params.device.disabled_by;
|
this._disabledBy = this._params.device.disabled_by;
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
}
|
}
|
||||||
@ -79,6 +83,11 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
.value=${this._areaId}
|
.value=${this._areaId}
|
||||||
@value-changed=${this._areaPicked}
|
@value-changed=${this._areaPicked}
|
||||||
></ha-area-picker>
|
></ha-area-picker>
|
||||||
|
<ha-labels-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._labels}
|
||||||
|
@value-changed=${this._labelsChanged}
|
||||||
|
></ha-labels-picker>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<ha-switch
|
<ha-switch
|
||||||
.checked=${!this._disabledBy}
|
.checked=${!this._disabledBy}
|
||||||
@ -150,6 +159,10 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
this._areaId = event.detail.value;
|
this._areaId = event.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _labelsChanged(event: CustomEvent): void {
|
||||||
|
this._labels = event.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private _disabledByChanged(ev: Event): void {
|
private _disabledByChanged(ev: Event): void {
|
||||||
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
|
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
|
||||||
}
|
}
|
||||||
@ -160,6 +173,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
await this._params!.updateEntry({
|
await this._params!.updateEntry({
|
||||||
name_by_user: this._nameByUser.trim() || null,
|
name_by_user: this._nameByUser.trim() || null,
|
||||||
area_id: this._areaId || null,
|
area_id: this._areaId || null,
|
||||||
|
labels: this._labels || null,
|
||||||
disabled_by: this._disabledBy || null,
|
disabled_by: this._disabledBy || null,
|
||||||
});
|
});
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
@ -182,7 +196,9 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield,
|
||||||
|
ha-labels-picker,
|
||||||
|
ha-area-picker {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import "../../../components/ha-select";
|
|||||||
import "../../../components/ha-settings-row";
|
import "../../../components/ha-settings-row";
|
||||||
import "../../../components/ha-state-icon";
|
import "../../../components/ha-state-icon";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
|
import "../../../components/ha-labels-picker";
|
||||||
import type { HaSwitch } from "../../../components/ha-switch";
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import {
|
import {
|
||||||
@ -162,6 +163,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _areaId?: string | null;
|
@state() private _areaId?: string | null;
|
||||||
|
|
||||||
|
@state() private _labels?: string[] | null;
|
||||||
|
|
||||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
||||||
|
|
||||||
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
|
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
|
||||||
@ -215,6 +218,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||||||
this.entry.device_class || this.entry.original_device_class;
|
this.entry.device_class || this.entry.original_device_class;
|
||||||
this._origEntityId = this.entry.entity_id;
|
this._origEntityId = this.entry.entity_id;
|
||||||
this._areaId = this.entry.area_id;
|
this._areaId = this.entry.area_id;
|
||||||
|
this._labels = this.entry.labels;
|
||||||
this._entityId = this.entry.entity_id;
|
this._entityId = this.entry.entity_id;
|
||||||
this._disabledBy = this.entry.disabled_by;
|
this._disabledBy = this.entry.disabled_by;
|
||||||
this._hiddenBy = this.entry.hidden_by;
|
this._hiddenBy = this.entry.hidden_by;
|
||||||
@ -759,6 +763,12 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||||||
@value-changed=${this._areaPicked}
|
@value-changed=${this._areaPicked}
|
||||||
></ha-area-picker>`
|
></ha-area-picker>`
|
||||||
: ""}
|
: ""}
|
||||||
|
<ha-labels-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._labels}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@value-changed=${this._labelsChanged}
|
||||||
|
></ha-labels-picker>
|
||||||
${this._cameraPrefs
|
${this._cameraPrefs
|
||||||
? html`
|
? html`
|
||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
@ -1008,6 +1018,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||||||
name: this._name.trim() || null,
|
name: this._name.trim() || null,
|
||||||
icon: this._icon.trim() || null,
|
icon: this._icon.trim() || null,
|
||||||
area_id: this._areaId || null,
|
area_id: this._areaId || null,
|
||||||
|
labels: this._labels || [],
|
||||||
new_entity_id: this._entityId.trim(),
|
new_entity_id: this._entityId.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1350,6 +1361,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||||||
this._areaId = ev.detail.value;
|
this._areaId = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _labelsChanged(ev: CustomEvent) {
|
||||||
|
this._labels = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private async _fetchCameraPrefs() {
|
private async _fetchCameraPrefs() {
|
||||||
this._cameraPrefs = await fetchCameraPrefs(this.hass, this.entry.entity_id);
|
this._cameraPrefs = await fetchCameraPrefs(this.hass, this.entry.entity_id);
|
||||||
}
|
}
|
||||||
|
@ -736,6 +736,7 @@ export class HaConfigEntities extends LitElement {
|
|||||||
entity_category: null,
|
entity_category: null,
|
||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
options: null,
|
options: null,
|
||||||
|
labels: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
mdiDevices,
|
mdiDevices,
|
||||||
mdiInformation,
|
mdiInformation,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
|
mdiLabel,
|
||||||
mdiLightningBolt,
|
mdiLightningBolt,
|
||||||
mdiMapMarkerRadius,
|
mdiMapMarkerRadius,
|
||||||
mdiMathLog,
|
mdiMathLog,
|
||||||
@ -267,6 +268,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
|||||||
iconColor: "#2D338F",
|
iconColor: "#2D338F",
|
||||||
core: true,
|
core: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: "labels",
|
||||||
|
path: "/config/labels",
|
||||||
|
translationKey: "ui.panel.config.labels.caption",
|
||||||
|
iconPath: mdiLabel,
|
||||||
|
iconColor: "#2D338F",
|
||||||
|
core: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: "zone",
|
component: "zone",
|
||||||
path: "/config/zone",
|
path: "/config/zone",
|
||||||
@ -451,6 +460,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
|||||||
tag: "ha-config-integrations",
|
tag: "ha-config-integrations",
|
||||||
load: () => import("./integrations/ha-config-integrations"),
|
load: () => import("./integrations/ha-config-integrations"),
|
||||||
},
|
},
|
||||||
|
labels: {
|
||||||
|
tag: "ha-config-labels",
|
||||||
|
load: () => import("./labels/ha-config-labels"),
|
||||||
|
},
|
||||||
lovelace: {
|
lovelace: {
|
||||||
tag: "ha-config-lovelace",
|
tag: "ha-config-lovelace",
|
||||||
load: () => import("./lovelace/ha-config-lovelace"),
|
load: () => import("./lovelace/ha-config-lovelace"),
|
||||||
|
214
src/panels/config/labels/dialog-label-detail.ts
Normal file
214
src/panels/config/labels/dialog-label-detail.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
|
import "../../../components/ha-formfield";
|
||||||
|
import "../../../components/ha-switch";
|
||||||
|
import "../../../components/ha-textfield";
|
||||||
|
import "../../../components/ha-icon-picker";
|
||||||
|
import "../../../components/ha-color-picker";
|
||||||
|
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { LabelDetailDialogParams } from "./show-dialog-label-detail";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
LabelRegistryEntryMutableParams,
|
||||||
|
} from "../../../data/label_registry";
|
||||||
|
|
||||||
|
@customElement("dialog-label-detail")
|
||||||
|
class DialogLabelDetail
|
||||||
|
extends LitElement
|
||||||
|
implements HassDialog<LabelDetailDialogParams>
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _name!: string;
|
||||||
|
|
||||||
|
@state() private _icon!: string;
|
||||||
|
|
||||||
|
@state() private _color!: string;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _params?: LabelDetailDialogParams;
|
||||||
|
|
||||||
|
@state() private _submitting = false;
|
||||||
|
|
||||||
|
public showDialog(params: LabelDetailDialogParams): void {
|
||||||
|
this._params = params;
|
||||||
|
this._error = undefined;
|
||||||
|
if (this._params.entry) {
|
||||||
|
this._name = this._params.entry.name || "";
|
||||||
|
this._icon = this._params.entry.icon || "";
|
||||||
|
this._color = this._params.entry.color || "";
|
||||||
|
} else {
|
||||||
|
this._name = this._params.suggestedName || "";
|
||||||
|
this._icon = "";
|
||||||
|
this._color = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
scrimClickAction
|
||||||
|
escapeKeyAction
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
this._params.entry
|
||||||
|
? this._params.entry.name || this._params.entry.label_id
|
||||||
|
: this.hass!.localize("ui.panel.config.labels.detail.new_label")
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
<div class="form">
|
||||||
|
<ha-textfield
|
||||||
|
dialogInitialFocus
|
||||||
|
.value=${this._name}
|
||||||
|
.configValue=${"name"}
|
||||||
|
@input=${this._input}
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
"ui.panel.config.labels.detail.name"
|
||||||
|
)}
|
||||||
|
.validationMessage=${this.hass!.localize(
|
||||||
|
"ui.panel.config.labels.detail.required_error_msg"
|
||||||
|
)}
|
||||||
|
required
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-icon-picker
|
||||||
|
.value=${this._icon}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.configValue=${"icon"}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
"ui.panel.config.labels.detail.icon"
|
||||||
|
)}
|
||||||
|
></ha-icon-picker>
|
||||||
|
<ha-color-picker
|
||||||
|
.value=${this._color}
|
||||||
|
.configValue=${"color"}
|
||||||
|
.hass=${this.hass}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
"ui.panel.config.labels.detail.color"
|
||||||
|
)}
|
||||||
|
></ha-color-picker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this._params.entry && this._params.removeEntry
|
||||||
|
? html`
|
||||||
|
<mwc-button
|
||||||
|
slot="secondaryAction"
|
||||||
|
class="warning"
|
||||||
|
@click=${this._deleteEntry}
|
||||||
|
.disabled=${this._submitting}
|
||||||
|
>
|
||||||
|
${this.hass!.localize("ui.panel.config.labels.detail.delete")}
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._updateEntry}
|
||||||
|
.disabled=${this._submitting || !this._name}
|
||||||
|
>
|
||||||
|
${this._params.entry
|
||||||
|
? this.hass!.localize("ui.panel.config.labels.detail.update")
|
||||||
|
: this.hass!.localize("ui.panel.config.labels.detail.create")}
|
||||||
|
</mwc-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _input(ev: Event) {
|
||||||
|
const target = ev.target as any;
|
||||||
|
const configValue = target.configValue;
|
||||||
|
|
||||||
|
this._error = undefined;
|
||||||
|
this[`_${configValue}`] = target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent) {
|
||||||
|
const target = ev.target as any;
|
||||||
|
const configValue = target.configValue;
|
||||||
|
|
||||||
|
this._error = undefined;
|
||||||
|
this[`_${configValue}`] = ev.detail.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateEntry() {
|
||||||
|
this._submitting = true;
|
||||||
|
let newValue: LabelRegistryEntry | undefined;
|
||||||
|
try {
|
||||||
|
const values: LabelRegistryEntryMutableParams = {
|
||||||
|
name: this._name.trim(),
|
||||||
|
icon: this._icon.trim() || null,
|
||||||
|
color: this._color.trim() || null,
|
||||||
|
};
|
||||||
|
if (this._params!.entry) {
|
||||||
|
newValue = await this._params!.updateEntry!(values);
|
||||||
|
} else {
|
||||||
|
newValue = await this._params!.createEntry!(values);
|
||||||
|
}
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = err ? err.message : "Unknown error";
|
||||||
|
} finally {
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteEntry() {
|
||||||
|
this._submitting = true;
|
||||||
|
try {
|
||||||
|
if (await this._params!.removeEntry!()) {
|
||||||
|
this._params = undefined;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-textfield,
|
||||||
|
ha-icon-picker,
|
||||||
|
ha-color-picker {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-color-picker {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-label-detail": DialogLabelDetail;
|
||||||
|
}
|
||||||
|
}
|
212
src/panels/config/labels/ha-config-labels.ts
Normal file
212
src/panels/config/labels/ha-config-labels.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
||||||
|
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
|
import {
|
||||||
|
DataTableColumnContainer,
|
||||||
|
RowClickedEvent,
|
||||||
|
} from "../../../components/data-table/ha-data-table";
|
||||||
|
import "../../../components/ha-fab";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-relative-time";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
LabelRegistryEntryMutableParams,
|
||||||
|
createLabelRegistryEntry,
|
||||||
|
deleteLabelRegistryEntry,
|
||||||
|
fetchLabelRegistry,
|
||||||
|
updateLabelRegistryEntry,
|
||||||
|
} from "../../../data/label_registry";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
|
import { configSections } from "../ha-panel-config";
|
||||||
|
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||||
|
|
||||||
|
@customElement("ha-config-labels")
|
||||||
|
export class HaConfigLabels extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isWide = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
|
@state() private _labels: LabelRegistryEntry[] = [];
|
||||||
|
|
||||||
|
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||||
|
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
|
||||||
|
icon: {
|
||||||
|
title: "",
|
||||||
|
label: localize("ui.panel.config.labels.headers.icon"),
|
||||||
|
type: "icon",
|
||||||
|
template: (label) =>
|
||||||
|
label.icon ? html`<ha-icon .icon=${label.icon}></ha-icon>` : nothing,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
title: "",
|
||||||
|
label: localize("ui.panel.config.labels.headers.color"),
|
||||||
|
type: "icon",
|
||||||
|
template: (label) =>
|
||||||
|
label.color
|
||||||
|
? html`<div
|
||||||
|
style="
|
||||||
|
background-color: ${computeCssColor(label.color)};
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;"
|
||||||
|
></div>`
|
||||||
|
: nothing,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
title: localize("ui.panel.config.labels.headers.name"),
|
||||||
|
main: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
grows: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return columns;
|
||||||
|
});
|
||||||
|
|
||||||
|
private _data = memoizeOne(
|
||||||
|
(labels: LabelRegistryEntry[]): LabelRegistryEntry[] =>
|
||||||
|
labels.map((label) => ({
|
||||||
|
...label,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this._fetchLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage-data-table
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
back-path="/config"
|
||||||
|
.route=${this.route}
|
||||||
|
.tabs=${configSections.areas}
|
||||||
|
.columns=${this._columns(this.hass.localize)}
|
||||||
|
.data=${this._data(this._labels)}
|
||||||
|
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
|
||||||
|
hasFab
|
||||||
|
@row-click=${this._editLabel}
|
||||||
|
clickable
|
||||||
|
id="label_id"
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="toolbar-icon"
|
||||||
|
@click=${this._showHelp}
|
||||||
|
.label=${this.hass.localize("ui.common.help")}
|
||||||
|
.path=${mdiHelpCircle}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-fab
|
||||||
|
slot="fab"
|
||||||
|
.label=${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||||
|
extended
|
||||||
|
@click=${this._addLabel}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</ha-fab>
|
||||||
|
</hass-tabs-subpage-data-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editLabel(ev: CustomEvent<RowClickedEvent>) {
|
||||||
|
const label = this._labels.find((lbl) => lbl.label_id === ev.detail.id);
|
||||||
|
this._openDialog(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showHelp() {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize("ui.panel.config.labels.caption"),
|
||||||
|
text: html`
|
||||||
|
${this.hass.localize("ui.panel.config.labels.introduction")}
|
||||||
|
<p>${this.hass.localize("ui.panel.config.labels.introduction2")}</p>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchLabels() {
|
||||||
|
this._labels = await fetchLabelRegistry(this.hass.connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addLabel() {
|
||||||
|
this._openDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openDialog(entry?: LabelRegistryEntry) {
|
||||||
|
showLabelDetailDialog(this, {
|
||||||
|
entry,
|
||||||
|
createEntry: (values) => this._createLabel(values),
|
||||||
|
updateEntry: entry
|
||||||
|
? (values) => this._updateLabel(entry, values)
|
||||||
|
: undefined,
|
||||||
|
removeEntry: entry ? () => this._removeLabel(entry) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createLabel(
|
||||||
|
values: LabelRegistryEntryMutableParams
|
||||||
|
): Promise<LabelRegistryEntry> {
|
||||||
|
const newTag = await createLabelRegistryEntry(this.hass, values);
|
||||||
|
this._labels = [...this._labels, newTag];
|
||||||
|
return newTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateLabel(
|
||||||
|
selectedLabel: LabelRegistryEntry,
|
||||||
|
values: Partial<LabelRegistryEntryMutableParams>
|
||||||
|
): Promise<LabelRegistryEntry> {
|
||||||
|
const updated = await updateLabelRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
selectedLabel.label_id,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
this._labels = this._labels.map((label) =>
|
||||||
|
label.label_id === selectedLabel.label_id ? updated : label
|
||||||
|
);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _removeLabel(selectedLabel: LabelRegistryEntry) {
|
||||||
|
if (
|
||||||
|
!(await showConfirmationDialog(this, {
|
||||||
|
title: this.hass!.localize(
|
||||||
|
"ui.panel.config.labels.confirm_remove_title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize("ui.panel.config.labels.confirm_remove", {
|
||||||
|
label: selectedLabel.name || selectedLabel.label_id,
|
||||||
|
}),
|
||||||
|
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||||
|
confirmText: this.hass!.localize("ui.common.remove"),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deleteLabelRegistryEntry(this.hass, selectedLabel.label_id);
|
||||||
|
this._labels = this._labels.filter(
|
||||||
|
(label) => label.label_id !== selectedLabel.label_id
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-labels": HaConfigLabels;
|
||||||
|
}
|
||||||
|
}
|
31
src/panels/config/labels/show-dialog-label-detail.ts
Normal file
31
src/panels/config/labels/show-dialog-label-detail.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
LabelRegistryEntryMutableParams,
|
||||||
|
} from "../../../data/label_registry";
|
||||||
|
|
||||||
|
export interface LabelDetailDialogParams {
|
||||||
|
entry?: LabelRegistryEntry;
|
||||||
|
suggestedName?: string;
|
||||||
|
createEntry?: (
|
||||||
|
values: LabelRegistryEntryMutableParams,
|
||||||
|
labelId?: string
|
||||||
|
) => Promise<LabelRegistryEntry>;
|
||||||
|
updateEntry?: (
|
||||||
|
updates: Partial<LabelRegistryEntryMutableParams>
|
||||||
|
) => Promise<LabelRegistryEntry>;
|
||||||
|
removeEntry?: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadLabelDetailDialog = () => import("./dialog-label-detail");
|
||||||
|
|
||||||
|
export const showLabelDetailDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: LabelDetailDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-label-detail",
|
||||||
|
dialogImport: loadLabelDetailDialog,
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -155,7 +155,7 @@ export class HuiTileCardEditor
|
|||||||
{
|
{
|
||||||
name: "color",
|
name: "color",
|
||||||
selector: {
|
selector: {
|
||||||
ui_color: {},
|
ui_color: { default_color: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -232,6 +232,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
entity_id: entity.ei,
|
entity_id: entity.ei,
|
||||||
device_id: entity.di,
|
device_id: entity.di,
|
||||||
area_id: entity.ai,
|
area_id: entity.ai,
|
||||||
|
labels: entity.lb,
|
||||||
translation_key: entity.tk,
|
translation_key: entity.tk,
|
||||||
platform: entity.pl,
|
platform: entity.pl,
|
||||||
entity_category:
|
entity_category:
|
||||||
|
@ -487,14 +487,17 @@
|
|||||||
"expand_floor_id": "Split this floor into separate areas.",
|
"expand_floor_id": "Split this floor into separate areas.",
|
||||||
"expand_area_id": "Split this area into separate devices and entities.",
|
"expand_area_id": "Split this area into separate devices and entities.",
|
||||||
"expand_device_id": "Split this device into separate entities.",
|
"expand_device_id": "Split this device into separate entities.",
|
||||||
|
"expand_label_id": "Split this label into separate area, devices and entities.",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"remove_floor_id": "Remove floor",
|
"remove_floor_id": "Remove floor",
|
||||||
"remove_area_id": "Remove area",
|
"remove_area_id": "Remove area",
|
||||||
"remove_device_id": "Remove device",
|
"remove_device_id": "Remove device",
|
||||||
"remove_entity_id": "Remove entity",
|
"remove_entity_id": "Remove entity",
|
||||||
|
"remove_label_id": "Remove label",
|
||||||
"add_area_id": "Choose area",
|
"add_area_id": "Choose area",
|
||||||
"add_device_id": "Choose device",
|
"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-picker": {
|
||||||
"config_entry": "Integration"
|
"config_entry": "Integration"
|
||||||
@ -544,6 +547,23 @@
|
|||||||
"device": "Device",
|
"device": "Device",
|
||||||
"no_area": "No area"
|
"no_area": "No area"
|
||||||
},
|
},
|
||||||
|
"label-picker": {
|
||||||
|
"clear": "Clear",
|
||||||
|
"show_labels": "Show labels",
|
||||||
|
"labels": "Labels",
|
||||||
|
"add_label": "Add 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."
|
||||||
|
}
|
||||||
|
},
|
||||||
"area-picker": {
|
"area-picker": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"show_areas": "Show areas",
|
"show_areas": "Show areas",
|
||||||
@ -585,6 +605,16 @@
|
|||||||
"show": "Show {area}",
|
"show": "Show {area}",
|
||||||
"hide": "Hide {area}"
|
"hide": "Hide {area}"
|
||||||
},
|
},
|
||||||
|
"label-picker": {
|
||||||
|
"clear": "Clear",
|
||||||
|
"show_labels": "Show labels",
|
||||||
|
"label": "Label",
|
||||||
|
"add_new_sugestion": "Add new label ''{name}''",
|
||||||
|
"add_new": "Add new label…",
|
||||||
|
"add_label": "Add label",
|
||||||
|
"no_labels": "You don't have any labels",
|
||||||
|
"no_match": "No matching labels found"
|
||||||
|
},
|
||||||
"statistic-picker": {
|
"statistic-picker": {
|
||||||
"statistic": "Statistic",
|
"statistic": "Statistic",
|
||||||
"no_statistics": "You don't have any statistics",
|
"no_statistics": "You don't have any statistics",
|
||||||
@ -634,6 +664,38 @@
|
|||||||
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
||||||
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image."
|
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image."
|
||||||
},
|
},
|
||||||
|
"color-picker": {
|
||||||
|
"default_color": "Default color (state)",
|
||||||
|
"colors": {
|
||||||
|
"primary": "Primary",
|
||||||
|
"accent": "Accent",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"red": "Red",
|
||||||
|
"pink": "Pink",
|
||||||
|
"purple": "Purple",
|
||||||
|
"deep-purple": "Deep purple",
|
||||||
|
"indigo": "Indigo",
|
||||||
|
"blue": "Blue",
|
||||||
|
"light-blue": "Light blue",
|
||||||
|
"cyan": "Cyan",
|
||||||
|
"teal": "Teal",
|
||||||
|
"green": "Green",
|
||||||
|
"light-green": "Light Green",
|
||||||
|
"lime": "Lime",
|
||||||
|
"yellow": "Yellow",
|
||||||
|
"amber": "Amber",
|
||||||
|
"orange": "Orange",
|
||||||
|
"deep-orange": "Deep orange",
|
||||||
|
"brown": "Brown",
|
||||||
|
"light-grey": "Light grey",
|
||||||
|
"grey": "Grey",
|
||||||
|
"dark-grey": "Dark grey",
|
||||||
|
"blue-grey": "Blue grey",
|
||||||
|
"black": "Black",
|
||||||
|
"white": "White"
|
||||||
|
}
|
||||||
|
},
|
||||||
"date-range-picker": {
|
"date-range-picker": {
|
||||||
"start_date": "Start date",
|
"start_date": "Start date",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
@ -1709,7 +1771,7 @@
|
|||||||
"secondary": "Manage who can access your home"
|
"secondary": "Manage who can access your home"
|
||||||
},
|
},
|
||||||
"areas": {
|
"areas": {
|
||||||
"main": "Areas & zones",
|
"main": "Areas, labels & zones",
|
||||||
"secondary": "Manage locations in and around your house"
|
"secondary": "Manage locations in and around your house"
|
||||||
},
|
},
|
||||||
"companion": {
|
"companion": {
|
||||||
@ -1796,6 +1858,28 @@
|
|||||||
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor."
|
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"caption": "Labels",
|
||||||
|
"description": "Group devices and entities",
|
||||||
|
"headers": { "name": "Name", "icon": "Icon", "color": "Color" },
|
||||||
|
"add_label": "Add label",
|
||||||
|
"no_labels": "You don't have any labels",
|
||||||
|
"introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.",
|
||||||
|
"introduction2": "Go to the area, device or entity you want to add a label to, and click on the edit button to assign labels to them.",
|
||||||
|
"confirm_remove_title": "Remove label?",
|
||||||
|
"confirm_remove": "Are you sure you want to remove label {label}? It will be removed from all areas, devices and entities.",
|
||||||
|
"detail": {
|
||||||
|
"new_label": "New label",
|
||||||
|
"name": "Name",
|
||||||
|
"icon": "Icon",
|
||||||
|
"color": "Color",
|
||||||
|
"description": "Description",
|
||||||
|
"delete": "Delete",
|
||||||
|
"update": "Update",
|
||||||
|
"create": "Create",
|
||||||
|
"required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"areas": {
|
"areas": {
|
||||||
"caption": "Areas",
|
"caption": "Areas",
|
||||||
"description": "Group devices and entities into areas",
|
"description": "Group devices and entities into areas",
|
||||||
@ -5841,38 +5925,6 @@
|
|||||||
"warning_multiple_cards": "This view contains more than one card, but a panel view can only show 1 card."
|
"warning_multiple_cards": "This view contains more than one card, but a panel view can only show 1 card."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"color-picker": {
|
|
||||||
"default_color": "Default color (state)",
|
|
||||||
"colors": {
|
|
||||||
"primary": "Primary",
|
|
||||||
"accent": "Accent",
|
|
||||||
"disabled": "Disabled",
|
|
||||||
"inactive": "Inactive",
|
|
||||||
"red": "Red",
|
|
||||||
"pink": "Pink",
|
|
||||||
"purple": "Purple",
|
|
||||||
"deep-purple": "Deep purple",
|
|
||||||
"indigo": "Indigo",
|
|
||||||
"blue": "Blue",
|
|
||||||
"light-blue": "Light blue",
|
|
||||||
"cyan": "Cyan",
|
|
||||||
"teal": "Teal",
|
|
||||||
"green": "Green",
|
|
||||||
"light-green": "Light Green",
|
|
||||||
"lime": "Lime",
|
|
||||||
"yellow": "Yellow",
|
|
||||||
"amber": "Amber",
|
|
||||||
"orange": "Orange",
|
|
||||||
"deep-orange": "Deep orange",
|
|
||||||
"brown": "Brown",
|
|
||||||
"light-grey": "Light grey",
|
|
||||||
"grey": "Grey",
|
|
||||||
"dark-grey": "Dark grey",
|
|
||||||
"blue-grey": "Blue grey",
|
|
||||||
"black": "Black",
|
|
||||||
"white": "White"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cardpicker": {
|
"cardpicker": {
|
||||||
"no_description": "No description available.",
|
"no_description": "No description available.",
|
||||||
"custom_card": "Custom",
|
"custom_card": "Custom",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user