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",
|
||||
name: null,
|
||||
icon: null,
|
||||
labels: [],
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
|
@ -88,6 +89,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||
id: "sensor.co2_intensity",
|
||||
name: null,
|
||||
icon: null,
|
||||
labels: [],
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
|
|
|
@ -59,6 +59,7 @@ const DEVICES = [
|
|||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
|
@ -77,6 +78,7 @@ const DEVICES = [
|
|||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
|
@ -95,6 +97,7 @@ const DEVICES = [
|
|||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -106,6 +109,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
|
@ -114,6 +118,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
|
@ -122,6 +127,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ const DEVICES = [
|
|||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
|
@ -73,6 +74,7 @@ const DEVICES = [
|
|||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
|
@ -91,6 +93,7 @@ const DEVICES = [
|
|||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -102,6 +105,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
|
@ -110,6 +114,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
|
@ -118,6 +123,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -406,6 +406,7 @@ export class DemoEntityState extends LitElement {
|
|||
entity_id: "select.speed",
|
||||
translation_key: "speed",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -199,6 +199,7 @@ const createEntityRegistryEntries = (
|
|||
has_entity_name: false,
|
||||
unique_id: "updater",
|
||||
options: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -222,6 +223,7 @@ const createDeviceRegistryEntries = (
|
|||
name_by_user: null,
|
||||
disabled_by: null,
|
||||
configuration_url: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -19,12 +19,16 @@ export class HaInputChip extends MdInputChip {
|
|||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-input-chip-selected-container-opacity: 1;
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
--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,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -288,6 +289,7 @@ export class HaAreaPicker extends LitElement {
|
|||
picture: null,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -303,6 +305,7 @@ export class HaAreaPicker extends LitElement {
|
|||
picture: null,
|
||||
icon: "mdi:plus",
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,17 +2,15 @@ import "@material/mwc-list/mwc-list-item";
|
|||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import {
|
||||
computeCssColor,
|
||||
THEME_COLORS,
|
||||
} from "../../../common/color/compute-color";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import "../../../components/ha-select";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-select";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LocalizeKeys } from "../common/translations/localize";
|
||||
|
||||
@customElement("hui-color-picker")
|
||||
export class HuiColorPicker extends LitElement {
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
@ -21,6 +19,8 @@ export class HuiColorPicker extends LitElement {
|
|||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: Boolean }) public defaultColor = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
_valueSelected(ev) {
|
||||
|
@ -52,16 +52,16 @@ export class HuiColorPicker extends LitElement {
|
|||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<mwc-list-item value="default">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.color-picker.default_color`
|
||||
)}
|
||||
</mwc-list-item>
|
||||
${this.defaultColor
|
||||
? html` <mwc-list-item value="default">
|
||||
${this.hass.localize(`ui.components.color-picker.default_color`)}
|
||||
</mwc-list-item>`
|
||||
: nothing}
|
||||
${Array.from(THEME_COLORS).map(
|
||||
(color) => html`
|
||||
<mwc-list-item .value=${color} graphic="icon">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.color-picker.colors.${color}`
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
) || color}
|
||||
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
||||
</mwc-list-item>
|
||||
|
@ -100,6 +100,6 @@ export class HuiColorPicker extends LitElement {
|
|||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-color-picker": HuiColorPicker;
|
||||
"ha-color-picker": HaColorPicker;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 { fireEvent } from "../../common/dom/fire_event";
|
||||
import { UiColorSelector } from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-color-picker";
|
||||
import "../ha-color-picker";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-ui_color")
|
||||
|
@ -19,13 +19,14 @@ export class HaSelectorUiColor extends LitElement {
|
|||
|
||||
protected render() {
|
||||
return html`
|
||||
<hui-color-picker
|
||||
<ha-color-picker
|
||||
.label=${this.label}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.defaultColor=${this.selector.ui_color?.default_color}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-color-picker>
|
||||
></ha-color-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const LOAD_ELEMENTS = {
|
|||
entity: () => import("./ha-selector-entity"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
label: () => import("./ha-selector-label"),
|
||||
language: () => import("./ha-selector-language"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
number: () => import("./ha-selector-number"),
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
expandAreaTarget,
|
||||
expandDeviceTarget,
|
||||
expandFloorTarget,
|
||||
expandLabelTarget,
|
||||
Selector,
|
||||
} from "../data/selector";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
|
@ -274,6 +275,24 @@ export class HaServiceControl extends LitElement {
|
|||
const targetFloors = ensureArray(
|
||||
value?.target?.floor_id || value?.data?.floor_id
|
||||
)?.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) {
|
||||
targetFloors.forEach((floorId) => {
|
||||
const expanded = expandFloorTarget(
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiFloorPlan,
|
||||
mdiLabel,
|
||||
mdiPlus,
|
||||
mdiSofa,
|
||||
mdiUnfoldMoreVertical,
|
||||
|
@ -45,7 +46,13 @@ import {
|
|||
FloorRegistryEntry,
|
||||
subscribeFloorRegistry,
|
||||
} 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 { hex2rgb } from "../common/color/convert-color";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
@ -83,7 +90,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
|
||||
@property({ type: Boolean }) public addOnTop = false;
|
||||
|
||||
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
|
||||
@state() private _addMode?:
|
||||
| "area_id"
|
||||
| "entity_id"
|
||||
| "device_id"
|
||||
| "label_id";
|
||||
|
||||
@query("#input") private _inputElement?;
|
||||
|
||||
|
@ -91,6 +102,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _labels?: LabelRegistryEntry[];
|
||||
|
||||
private _opened = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
|
@ -98,6 +111,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labels = labels;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -138,7 +154,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
mdiSofa
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.value?.device_id
|
||||
? ensureArray(this.value.device_id).map((device_id) => {
|
||||
const device = this.hass.devices![device_id];
|
||||
|
@ -151,7 +167,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
mdiDevices
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.value?.entity_id
|
||||
? ensureArray(this.value.entity_id).map((entity_id) => {
|
||||
const entity = this.hass.states[entity_id];
|
||||
|
@ -162,7 +178,35 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
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>
|
||||
`;
|
||||
}
|
||||
|
@ -230,6 +274,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip label_id add"
|
||||
.type=${"label_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_label_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
</div>
|
||||
${this.helper
|
||||
|
@ -243,18 +307,22 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
}
|
||||
|
||||
private _renderChip(
|
||||
type: "floor_id" | "area_id" | "device_id" | "entity_id",
|
||||
type: "floor_id" | "area_id" | "device_id" | "entity_id" | "label_id",
|
||||
id: string,
|
||||
name: string,
|
||||
entityState?: HassEntity,
|
||||
icon?: string | null,
|
||||
fallbackIconPath?: string
|
||||
fallbackIconPath?: string,
|
||||
color?: string
|
||||
) {
|
||||
return html`
|
||||
<div
|
||||
class="mdc-chip ${classMap({
|
||||
[type]: true,
|
||||
})}"
|
||||
style=${color
|
||||
? `--color: rgb(${color}); --background-color: rgba(${color}, .3)`
|
||||
: ""}
|
||||
>
|
||||
${icon
|
||||
? html`<ha-icon
|
||||
|
@ -368,23 +436,42 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
@click=${this._preventDefault}
|
||||
></ha-device-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
|
||||
: this._addMode === "label_id"
|
||||
? html`
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"label_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_label_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeLabels=${ensureArray(this.value?.label_id)}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-label-picker>
|
||||
`
|
||||
: 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);
|
||||
}
|
||||
});
|
||||
} 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 {
|
||||
return;
|
||||
}
|
||||
|
@ -578,39 +693,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
(entity) => entity.device_id === device.id
|
||||
);
|
||||
|
||||
if (this.includeDomains) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
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 (!devEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -719,8 +790,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
--mdc-icon-size: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
margin-left: -14px !important;
|
||||
margin-inline-start: -14px !important;
|
||||
margin-left: -13px !important;
|
||||
margin-inline-start: -13px !important;
|
||||
margin-inline-end: 4px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
@ -731,7 +802,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
}
|
||||
.mdc-chip.area_id:not(.add),
|
||||
.mdc-chip.floor_id:not(.add) {
|
||||
border: 2px solid #fed6a4;
|
||||
border: 1px solid #fed6a4;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
||||
|
@ -741,7 +812,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
background: #fed6a4;
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) {
|
||||
border: 2px solid #a8e1fb;
|
||||
border: 1px solid #a8e1fb;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
||||
|
@ -749,13 +820,21 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
background: #a8e1fb;
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) {
|
||||
border: 2px solid #d2e7b9;
|
||||
border: 1px solid #d2e7b9;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.entity_id.add {
|
||||
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 {
|
||||
z-index: 5;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ export interface AreaRegistryEntry {
|
|||
name: string;
|
||||
picture: string | null;
|
||||
icon: string | null;
|
||||
labels: string[];
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
|
@ -28,6 +29,7 @@ export interface AreaRegistryEntryMutableParams {
|
|||
picture?: string | null;
|
||||
icon?: string | null;
|
||||
aliases?: string[];
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export const createAreaRegistryEntry = (
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface DeviceRegistryEntry {
|
|||
manufacturer: string | null;
|
||||
model: string | null;
|
||||
name: string | null;
|
||||
labels: string[];
|
||||
sw_version: string | null;
|
||||
hw_version: string | null;
|
||||
serial_number: string | null;
|
||||
|
@ -43,6 +44,7 @@ export interface DeviceRegistryEntryMutableParams {
|
|||
area_id?: string | null;
|
||||
name_by_user?: string | null;
|
||||
disabled_by?: string | null;
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export const fallbackDeviceName = (
|
||||
|
@ -140,7 +142,7 @@ export const getDeviceEntityDisplayLookup = (
|
|||
|
||||
export const getDeviceIntegrationLookup = (
|
||||
entitySources: EntitySources,
|
||||
entities: EntityRegistryDisplayEntry[]
|
||||
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[]
|
||||
): Record<string, string[]> => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface EntityRegistryDisplayEntry {
|
|||
icon?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
labels: string[];
|
||||
hidden?: boolean;
|
||||
entity_category?: entityCategory;
|
||||
translation_key?: string;
|
||||
|
@ -30,6 +31,7 @@ export interface EntityRegistryDisplayEntryResponse {
|
|||
ei: string;
|
||||
di?: string;
|
||||
ai?: string;
|
||||
lb: string[];
|
||||
ec?: number;
|
||||
en?: string;
|
||||
ic?: string;
|
||||
|
@ -50,6 +52,7 @@ export interface EntityRegistryEntry {
|
|||
config_entry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
labels: string[];
|
||||
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
|
||||
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
|
||||
entity_category: entityCategory | null;
|
||||
|
@ -133,6 +136,7 @@ export interface EntityRegistryEntryUpdateParams {
|
|||
| WeatherEntityOptions
|
||||
| LightEntityOptions;
|
||||
aliases?: string[];
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
const batteryPriorities = ["sensor", "binary_sensor"];
|
||||
|
|
|
@ -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,
|
||||
getDeviceIntegrationLookup,
|
||||
} from "./device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { EntitySources } from "./entity_sources";
|
||||
|
||||
export type Selector =
|
||||
|
@ -34,6 +37,7 @@ export type Selector =
|
|||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
| LabelSelector
|
||||
| LanguageSelector
|
||||
| LocationSelector
|
||||
| MediaSelector
|
||||
|
@ -242,6 +246,12 @@ export interface IconSelector {
|
|||
} | null;
|
||||
}
|
||||
|
||||
export interface LabelSelector {
|
||||
label: {
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LanguageSelector {
|
||||
language: {
|
||||
languages?: string[];
|
||||
|
@ -421,9 +431,69 @@ export interface UiActionSelector {
|
|||
|
||||
export interface UiColorSelector {
|
||||
// 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 = (
|
||||
hass: HomeAssistant,
|
||||
floorId: string,
|
||||
|
@ -555,7 +625,7 @@ export const areaMeetsTargetSelector = (
|
|||
|
||||
export const deviceMeetsTargetSelector = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryDisplayEntry[],
|
||||
entityRegistry: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
|
||||
device: DeviceRegistryEntry,
|
||||
targetSelector: TargetSelector,
|
||||
entitySources?: EntitySources
|
||||
|
|
|
@ -12,6 +12,7 @@ import "../../../components/ha-settings-row";
|
|||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-floor-picker";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
|
||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
|
@ -32,6 +33,8 @@ class DialogAreaDetail extends LitElement {
|
|||
|
||||
@state() private _aliases!: string[];
|
||||
|
||||
@state() private _labels!: string[];
|
||||
|
||||
@state() private _picture!: string | null;
|
||||
|
||||
@state() private _icon!: string | null;
|
||||
|
@ -51,6 +54,7 @@ class DialogAreaDetail extends LitElement {
|
|||
this._error = undefined;
|
||||
this._name = this._params.entry ? this._params.entry.name : "";
|
||||
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._icon = this._params.entry?.icon || 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")}
|
||||
></ha-floor-picker>
|
||||
|
||||
<ha-labels-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._labels}
|
||||
@value-changed=${this._labelsChanged}
|
||||
></ha-labels-picker>
|
||||
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
|
@ -184,6 +194,11 @@ class DialogAreaDetail extends LitElement {
|
|||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
private _labelsChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._labels = ev.detail.value;
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
|
@ -198,6 +213,7 @@ class DialogAreaDetail extends LitElement {
|
|||
picture: this._picture || (create ? undefined : null),
|
||||
icon: this._icon || (create ? undefined : null),
|
||||
floor_id: this._floor || (create ? undefined : null),
|
||||
labels: this._labels || null,
|
||||
aliases: this._aliases,
|
||||
};
|
||||
if (create) {
|
||||
|
@ -226,6 +242,7 @@ class DialogAreaDetail extends LitElement {
|
|||
ha-textfield,
|
||||
ha-icon-picker,
|
||||
ha-floor-picker,
|
||||
ha-labels-picker,
|
||||
ha-picture-upload {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
|||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-area-picker";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-labels-picker";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-textfield";
|
||||
import {
|
||||
|
@ -27,6 +28,8 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||
|
||||
@state() private _areaId!: string;
|
||||
|
||||
@state() private _labels!: string[];
|
||||
|
||||
@state() private _disabledBy!: DeviceRegistryEntry["disabled_by"];
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
@ -38,6 +41,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||
this._error = undefined;
|
||||
this._nameByUser = this._params.device.name_by_user || "";
|
||||
this._areaId = this._params.device.area_id || "";
|
||||
this._labels = this._params.device.labels || [];
|
||||
this._disabledBy = this._params.device.disabled_by;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
@ -79,6 +83,11 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||
.value=${this._areaId}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
<ha-labels-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._labels}
|
||||
@value-changed=${this._labelsChanged}
|
||||
></ha-labels-picker>
|
||||
<div class="row">
|
||||
<ha-switch
|
||||
.checked=${!this._disabledBy}
|
||||
|
@ -150,6 +159,10 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||
this._areaId = event.detail.value;
|
||||
}
|
||||
|
||||
private _labelsChanged(event: CustomEvent): void {
|
||||
this._labels = event.detail.value;
|
||||
}
|
||||
|
||||
private _disabledByChanged(ev: Event): void {
|
||||
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
|
||||
}
|
||||
|
@ -160,6 +173,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||
await this._params!.updateEntry({
|
||||
name_by_user: this._nameByUser.trim() || null,
|
||||
area_id: this._areaId || null,
|
||||
labels: this._labels || null,
|
||||
disabled_by: this._disabledBy || null,
|
||||
});
|
||||
this.closeDialog();
|
||||
|
@ -182,7 +196,9 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-textfield,
|
||||
ha-labels-picker,
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import "../../../components/ha-select";
|
|||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import {
|
||||
|
@ -162,6 +163,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _labels?: string[] | null;
|
||||
|
||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_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._origEntityId = this.entry.entity_id;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._labels = this.entry.labels;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
|
@ -759,6 +763,12 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>`
|
||||
: ""}
|
||||
<ha-labels-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._labels}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._labelsChanged}
|
||||
></ha-labels-picker>
|
||||
${this._cameraPrefs
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
|
@ -1008,6 +1018,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
area_id: this._areaId || null,
|
||||
labels: this._labels || [],
|
||||
new_entity_id: this._entityId.trim(),
|
||||
};
|
||||
|
||||
|
@ -1350,6 +1361,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
|||
this._areaId = ev.detail.value;
|
||||
}
|
||||
|
||||
private _labelsChanged(ev: CustomEvent) {
|
||||
this._labels = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _fetchCameraPrefs() {
|
||||
this._cameraPrefs = await fetchCameraPrefs(this.hass, this.entry.entity_id);
|
||||
}
|
||||
|
|
|
@ -736,6 +736,7 @@ export class HaConfigEntities extends LitElement {
|
|||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
options: null,
|
||||
labels: [],
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
mdiDevices,
|
||||
mdiInformation,
|
||||
mdiInformationOutline,
|
||||
mdiLabel,
|
||||
mdiLightningBolt,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMathLog,
|
||||
|
@ -267,6 +268,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
|||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
component: "labels",
|
||||
path: "/config/labels",
|
||||
translationKey: "ui.panel.config.labels.caption",
|
||||
iconPath: mdiLabel,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
component: "zone",
|
||||
path: "/config/zone",
|
||||
|
@ -451,6 +460,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
|||
tag: "ha-config-integrations",
|
||||
load: () => import("./integrations/ha-config-integrations"),
|
||||
},
|
||||
labels: {
|
||||
tag: "ha-config-labels",
|
||||
load: () => import("./labels/ha-config-labels"),
|
||||
},
|
||||
lovelace: {
|
||||
tag: "ha-config-lovelace",
|
||||
load: () => import("./lovelace/ha-config-lovelace"),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
selector: {
|
||||
ui_color: {},
|
||||
ui_color: { default_color: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -232,6 +232,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||
entity_id: entity.ei,
|
||||
device_id: entity.di,
|
||||
area_id: entity.ai,
|
||||
labels: entity.lb,
|
||||
translation_key: entity.tk,
|
||||
platform: entity.pl,
|
||||
entity_category:
|
||||
|
|
|
@ -487,14 +487,17 @@
|
|||
"expand_floor_id": "Split this floor into separate areas.",
|
||||
"expand_area_id": "Split this area into separate devices and entities.",
|
||||
"expand_device_id": "Split this device into separate entities.",
|
||||
"expand_label_id": "Split this label into separate area, devices and entities.",
|
||||
"remove": "Remove",
|
||||
"remove_floor_id": "Remove floor",
|
||||
"remove_area_id": "Remove area",
|
||||
"remove_device_id": "Remove device",
|
||||
"remove_entity_id": "Remove entity",
|
||||
"remove_label_id": "Remove label",
|
||||
"add_area_id": "Choose area",
|
||||
"add_device_id": "Choose device",
|
||||
"add_entity_id": "Choose entity"
|
||||
"add_entity_id": "Choose entity",
|
||||
"add_label_id": "Choose label"
|
||||
},
|
||||
"config-entry-picker": {
|
||||
"config_entry": "Integration"
|
||||
|
@ -544,6 +547,23 @@
|
|||
"device": "Device",
|
||||
"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": {
|
||||
"clear": "Clear",
|
||||
"show_areas": "Show areas",
|
||||
|
@ -585,6 +605,16 @@
|
|||
"show": "Show {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": "Statistic",
|
||||
"no_statistics": "You don't have any statistics",
|
||||
|
@ -634,6 +664,38 @@
|
|||
"supported_formats": "Supports 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": {
|
||||
"start_date": "Start date",
|
||||
"end_date": "End date",
|
||||
|
@ -1709,7 +1771,7 @@
|
|||
"secondary": "Manage who can access your home"
|
||||
},
|
||||
"areas": {
|
||||
"main": "Areas & zones",
|
||||
"main": "Areas, labels & zones",
|
||||
"secondary": "Manage locations in and around your house"
|
||||
},
|
||||
"companion": {
|
||||
|
@ -1796,6 +1858,28 @@
|
|||
"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": {
|
||||
"caption": "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."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"no_description": "No description available.",
|
||||
"custom_card": "Custom",
|
||||
|
|
Loading…
Reference in New Issue