mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 19:09:48 +00:00
New target picker (#27284)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -8,10 +8,10 @@ interface AreaContext {
|
|||||||
}
|
}
|
||||||
export const getAreaContext = (
|
export const getAreaContext = (
|
||||||
area: AreaRegistryEntry,
|
area: AreaRegistryEntry,
|
||||||
hass: HomeAssistant
|
hassFloors: HomeAssistant["floors"]
|
||||||
): AreaContext => {
|
): AreaContext => {
|
||||||
const floorId = area.floor_id;
|
const floorId = area.floor_id;
|
||||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
const floor = floorId ? hassFloors[floorId] : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
area: area,
|
area: area,
|
||||||
|
|||||||
@@ -5,24 +5,18 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
import {
|
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||||
computeDeviceName,
|
|
||||||
computeDeviceNameDisplay,
|
|
||||||
} from "../../common/entity/compute_device_name";
|
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
|
||||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
getDeviceEntityDisplayLookup,
|
getDevices,
|
||||||
type DeviceEntityDisplayLookup,
|
type DevicePickerItem,
|
||||||
type DeviceRegistryEntry,
|
type DeviceRegistryEntry,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
|
||||||
|
|
||||||
export type HaDevicePickerDeviceFilterFunc = (
|
export type HaDevicePickerDeviceFilterFunc = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
@@ -30,11 +24,6 @@ export type HaDevicePickerDeviceFilterFunc = (
|
|||||||
|
|
||||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
interface DevicePickerItem extends PickerComboBoxItem {
|
|
||||||
domain?: string;
|
|
||||||
domain_name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-device-picker")
|
@customElement("ha-device-picker")
|
||||||
export class HaDevicePicker extends LitElement {
|
export class HaDevicePicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -104,6 +93,8 @@ export class HaDevicePicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||||
|
|
||||||
|
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||||
|
|
||||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(_changedProperties);
|
super.firstUpdated(_changedProperties);
|
||||||
this._loadConfigEntries();
|
this._loadConfigEntries();
|
||||||
@@ -117,160 +108,16 @@ export class HaDevicePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getItems = () =>
|
private _getItems = () =>
|
||||||
this._getDevices(
|
this._getDevicesMemoized(
|
||||||
this.hass.devices,
|
this.hass,
|
||||||
this.hass.entities,
|
|
||||||
this._configEntryLookup,
|
this._configEntryLookup,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
this.deviceFilter,
|
this.deviceFilter,
|
||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.excludeDevices
|
this.excludeDevices,
|
||||||
);
|
this.value
|
||||||
|
|
||||||
private _getDevices = memoizeOne(
|
|
||||||
(
|
|
||||||
haDevices: HomeAssistant["devices"],
|
|
||||||
haEntities: HomeAssistant["entities"],
|
|
||||||
configEntryLookup: Record<string, ConfigEntry>,
|
|
||||||
includeDomains: this["includeDomains"],
|
|
||||||
excludeDomains: this["excludeDomains"],
|
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
|
||||||
deviceFilter: this["deviceFilter"],
|
|
||||||
entityFilter: this["entityFilter"],
|
|
||||||
excludeDevices: this["excludeDevices"]
|
|
||||||
): DevicePickerItem[] => {
|
|
||||||
const devices = Object.values(haDevices);
|
|
||||||
const entities = Object.values(haEntities);
|
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
includeDomains ||
|
|
||||||
excludeDomains ||
|
|
||||||
includeDeviceClasses ||
|
|
||||||
entityFilter
|
|
||||||
) {
|
|
||||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputDevices = devices.filter(
|
|
||||||
(device) => device.id === this.value || !device.disabled_by
|
|
||||||
);
|
|
||||||
|
|
||||||
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))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeDevices) {
|
|
||||||
inputDevices = inputDevices.filter(
|
|
||||||
(device) => !excludeDevices!.includes(device.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)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityFilter) {
|
|
||||||
inputDevices = inputDevices.filter((device) => {
|
|
||||||
const devEntities = deviceEntityLookup[device.id];
|
|
||||||
if (!devEntities || !devEntities.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return devEntities.some((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (!stateObj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return entityFilter(stateObj);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceFilter) {
|
|
||||||
inputDevices = inputDevices.filter(
|
|
||||||
(device) =>
|
|
||||||
// We always want to include the device of the current value
|
|
||||||
device.id === this.value || deviceFilter!(device)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
|
|
||||||
const deviceName = computeDeviceNameDisplay(
|
|
||||||
device,
|
|
||||||
this.hass,
|
|
||||||
deviceEntityLookup[device.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { area } = getDeviceContext(device, this.hass);
|
|
||||||
|
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
|
||||||
|
|
||||||
const configEntry = device.primary_config_entry
|
|
||||||
? configEntryLookup?.[device.primary_config_entry]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const domain = configEntry?.domain;
|
|
||||||
const domainName = domain
|
|
||||||
? domainToName(this.hass.localize, domain)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: device.id,
|
|
||||||
label: "",
|
|
||||||
primary:
|
|
||||||
deviceName ||
|
|
||||||
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
|
||||||
secondary: areaName,
|
|
||||||
domain: configEntry?.domain,
|
|
||||||
domain_name: domainName,
|
|
||||||
search_labels: [deviceName, areaName, domain, domainName].filter(
|
|
||||||
Boolean
|
|
||||||
) as string[],
|
|
||||||
sorting_label: deviceName || "zzz",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return outputDevices;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _valueRenderer = memoizeOne(
|
private _valueRenderer = memoizeOne(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
|||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
import "./ha-entity-picker";
|
import "./ha-entity-picker";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
|
|
||||||
@customElement("ha-entities-picker")
|
@customElement("ha-entities-picker")
|
||||||
class HaEntitiesPicker extends LitElement {
|
class HaEntitiesPicker extends LitElement {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@@ -8,8 +7,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
|
||||||
|
|
||||||
interface AttributeOption {
|
interface AttributeOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
|
||||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
|
import {
|
||||||
|
getEntities,
|
||||||
|
type EntityComboBoxItem,
|
||||||
|
} from "../../data/entity_registry";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import {
|
import {
|
||||||
isHelperDomain,
|
isHelperDomain,
|
||||||
@@ -20,21 +22,11 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box-item";
|
||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import type {
|
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
|
||||||
PickerComboBoxItem,
|
|
||||||
PickerComboBoxSearchFn,
|
|
||||||
} from "../ha-picker-combo-box";
|
|
||||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
|
|
||||||
interface EntityComboBoxItem extends PickerComboBoxItem {
|
|
||||||
domain_name?: string;
|
|
||||||
stateObj?: HassEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
|
||||||
|
|
||||||
const CREATE_ID = "___create-new-entity___";
|
const CREATE_ID = "___create-new-entity___";
|
||||||
|
|
||||||
@customElement("ha-entity-picker")
|
@customElement("ha-entity-picker")
|
||||||
@@ -255,8 +247,10 @@ export class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||||
|
|
||||||
private _getItems = () =>
|
private _getItems = () =>
|
||||||
this._getEntities(
|
this._getEntitiesMemoized(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
@@ -264,126 +258,8 @@ export class HaEntityPicker extends LitElement {
|
|||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
this.includeUnitOfMeasurement,
|
this.includeUnitOfMeasurement,
|
||||||
this.includeEntities,
|
this.includeEntities,
|
||||||
this.excludeEntities
|
this.excludeEntities,
|
||||||
);
|
this.value
|
||||||
|
|
||||||
private _getEntities = memoizeOne(
|
|
||||||
(
|
|
||||||
hass: this["hass"],
|
|
||||||
includeDomains: this["includeDomains"],
|
|
||||||
excludeDomains: this["excludeDomains"],
|
|
||||||
entityFilter: this["entityFilter"],
|
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
|
||||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
|
||||||
includeEntities: this["includeEntities"],
|
|
||||||
excludeEntities: this["excludeEntities"]
|
|
||||||
): EntityComboBoxItem[] => {
|
|
||||||
let items: EntityComboBoxItem[] = [];
|
|
||||||
|
|
||||||
let entityIds = Object.keys(hass.states);
|
|
||||||
|
|
||||||
if (includeEntities) {
|
|
||||||
entityIds = entityIds.filter((entityId) =>
|
|
||||||
includeEntities.includes(entityId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeEntities) {
|
|
||||||
entityIds = entityIds.filter(
|
|
||||||
(entityId) => !excludeEntities.includes(entityId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeDomains) {
|
|
||||||
entityIds = entityIds.filter((eid) =>
|
|
||||||
includeDomains.includes(computeDomain(eid))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeDomains) {
|
|
||||||
entityIds = entityIds.filter(
|
|
||||||
(eid) => !excludeDomains.includes(computeDomain(eid))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRTL = computeRTL(hass);
|
|
||||||
|
|
||||||
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
|
||||||
const stateObj = hass.states[entityId];
|
|
||||||
|
|
||||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
|
||||||
|
|
||||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
|
||||||
stateObj,
|
|
||||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
|
||||||
hass.entities,
|
|
||||||
hass.devices,
|
|
||||||
hass.areas,
|
|
||||||
hass.floors
|
|
||||||
);
|
|
||||||
|
|
||||||
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
|
||||||
|
|
||||||
const primary = entityName || deviceName || entityId;
|
|
||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
|
||||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: entityId,
|
|
||||||
primary: primary,
|
|
||||||
secondary: secondary,
|
|
||||||
domain_name: domainName,
|
|
||||||
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
|
||||||
search_labels: [
|
|
||||||
entityName,
|
|
||||||
deviceName,
|
|
||||||
areaName,
|
|
||||||
domainName,
|
|
||||||
friendlyName,
|
|
||||||
entityId,
|
|
||||||
].filter(Boolean) as string[],
|
|
||||||
a11y_label: a11yLabel,
|
|
||||||
stateObj: stateObj,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeDeviceClasses) {
|
|
||||||
items = items.filter(
|
|
||||||
(item) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
item.id === this.value ||
|
|
||||||
(item.stateObj?.attributes.device_class &&
|
|
||||||
includeDeviceClasses.includes(
|
|
||||||
item.stateObj.attributes.device_class
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeUnitOfMeasurement) {
|
|
||||||
items = items.filter(
|
|
||||||
(item) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
item.id === this.value ||
|
|
||||||
(item.stateObj?.attributes.unit_of_measurement &&
|
|
||||||
includeUnitOfMeasurement.includes(
|
|
||||||
item.stateObj.attributes.unit_of_measurement
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityFilter) {
|
|
||||||
items = items.filter(
|
|
||||||
(item) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
item.id === this.value ||
|
|
||||||
(item.stateObj && entityFilter!(item.stateObj))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@@ -9,8 +8,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
|
||||||
|
|
||||||
interface StateOption {
|
interface StateOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -8,21 +8,13 @@ import { styleMap } from "lit/directives/style-map";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
import { stringCompare } from "../common/string/compare";
|
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import type { AreaRegistryEntry } from "../data/area_registry";
|
|
||||||
import type {
|
|
||||||
DeviceEntityDisplayLookup,
|
|
||||||
DeviceRegistryEntry,
|
|
||||||
} from "../data/device_registry";
|
|
||||||
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
|
||||||
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
|
||||||
import {
|
import {
|
||||||
getFloorAreaLookup,
|
getAreasAndFloors,
|
||||||
type FloorRegistryEntry,
|
type AreaFloorValue,
|
||||||
} from "../data/floor_registry";
|
type FloorComboBoxItem,
|
||||||
|
} from "../data/area_floor";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
@@ -30,24 +22,12 @@ import "./ha-floor-icon";
|
|||||||
import "./ha-generic-picker";
|
import "./ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
|
||||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-tree-indicator";
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
const SEPARATOR = "________";
|
const SEPARATOR = "________";
|
||||||
|
|
||||||
interface FloorComboBoxItem extends PickerComboBoxItem {
|
|
||||||
type: "floor" | "area";
|
|
||||||
floor?: FloorRegistryEntry;
|
|
||||||
area?: AreaRegistryEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AreaFloorValue {
|
|
||||||
id: string;
|
|
||||||
type: "floor" | "area";
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-area-floor-picker")
|
@customElement("ha-area-floor-picker")
|
||||||
export class HaAreaFloorPicker extends LitElement {
|
export class HaAreaFloorPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -154,243 +134,6 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getAreasAndFloors = memoizeOne(
|
|
||||||
(
|
|
||||||
haFloors: HomeAssistant["floors"],
|
|
||||||
haAreas: HomeAssistant["areas"],
|
|
||||||
haDevices: HomeAssistant["devices"],
|
|
||||||
haEntities: HomeAssistant["entities"],
|
|
||||||
includeDomains: this["includeDomains"],
|
|
||||||
excludeDomains: this["excludeDomains"],
|
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
|
||||||
deviceFilter: this["deviceFilter"],
|
|
||||||
entityFilter: this["entityFilter"],
|
|
||||||
excludeAreas: this["excludeAreas"],
|
|
||||||
excludeFloors: this["excludeFloors"]
|
|
||||||
): FloorComboBoxItem[] => {
|
|
||||||
const floors = Object.values(haFloors);
|
|
||||||
const areas = Object.values(haAreas);
|
|
||||||
const devices = Object.values(haDevices);
|
|
||||||
const entities = Object.values(haEntities);
|
|
||||||
|
|
||||||
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.area_id);
|
|
||||||
|
|
||||||
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 outputAreas = areas;
|
|
||||||
|
|
||||||
let areaIds: string[] | undefined;
|
|
||||||
|
|
||||||
if (inputDevices) {
|
|
||||||
areaIds = inputDevices
|
|
||||||
.filter((device) => device.area_id)
|
|
||||||
.map((device) => device.area_id!);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputEntities) {
|
|
||||||
areaIds = (areaIds ?? []).concat(
|
|
||||||
inputEntities
|
|
||||||
.filter((entity) => entity.area_id)
|
|
||||||
.map((entity) => entity.area_id!)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (areaIds) {
|
|
||||||
outputAreas = outputAreas.filter((area) =>
|
|
||||||
areaIds!.includes(area.area_id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeAreas) {
|
|
||||||
outputAreas = outputAreas.filter(
|
|
||||||
(area) => !excludeAreas!.includes(area.area_id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeFloors) {
|
|
||||||
outputAreas = outputAreas.filter(
|
|
||||||
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
|
||||||
const unassisgnedAreas = Object.values(outputAreas).filter(
|
|
||||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const floorAreaEntries: [
|
|
||||||
FloorRegistryEntry | undefined,
|
|
||||||
AreaRegistryEntry[],
|
|
||||||
][] = Object.entries(floorAreaLookup)
|
|
||||||
.map(([floorId, floorAreas]) => {
|
|
||||||
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
|
||||||
return [floor, floorAreas] as const;
|
|
||||||
})
|
|
||||||
.sort(([floorA], [floorB]) => {
|
|
||||||
if (floorA.level !== floorB.level) {
|
|
||||||
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
|
||||||
}
|
|
||||||
return stringCompare(floorA.name, floorB.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const items: FloorComboBoxItem[] = [];
|
|
||||||
|
|
||||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
|
||||||
if (floor) {
|
|
||||||
const floorName = computeFloorName(floor);
|
|
||||||
|
|
||||||
const areaSearchLabels = floorAreas
|
|
||||||
.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
|
||||||
return [area.area_id, areaName, ...area.aliases];
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
id: this._formatValue({ id: floor.floor_id, type: "floor" }),
|
|
||||||
type: "floor",
|
|
||||||
primary: floorName,
|
|
||||||
floor: floor,
|
|
||||||
search_labels: [
|
|
||||||
floor.floor_id,
|
|
||||||
floorName,
|
|
||||||
...floor.aliases,
|
|
||||||
...areaSearchLabels,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
items.push(
|
|
||||||
...floorAreas.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
|
||||||
return {
|
|
||||||
id: this._formatValue({ id: area.area_id, type: "area" }),
|
|
||||||
type: "area" as const,
|
|
||||||
primary: areaName,
|
|
||||||
area: area,
|
|
||||||
icon: area.icon || undefined,
|
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
...unassisgnedAreas.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
|
||||||
return {
|
|
||||||
id: this._formatValue({ id: area.area_id, type: "area" }),
|
|
||||||
type: "area" as const,
|
|
||||||
primary: areaName,
|
|
||||||
icon: area.icon || undefined,
|
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
|
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
|
||||||
item,
|
item,
|
||||||
{ index },
|
{ index },
|
||||||
@@ -445,12 +188,16 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||||
|
|
||||||
private _getItems = () =>
|
private _getItems = () =>
|
||||||
this._getAreasAndFloors(
|
this._getAreasAndFloorsMemoized(
|
||||||
|
this.hass.states,
|
||||||
this.hass.floors,
|
this.hass.floors,
|
||||||
this.hass.areas,
|
this.hass.areas,
|
||||||
this.hass.devices,
|
this.hass.devices,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
|
this._formatValue,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { floor } = getAreaContext(area, this.hass);
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
|
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||||
@@ -279,7 +279,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||||
const { floor } = getAreaContext(area, this.hass);
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||||
const areaName = computeAreaName(area);
|
const areaName = computeAreaName(area);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items: DisplayItem[] = areas.map((area) => {
|
const items: DisplayItem[] = areas.map((area) => {
|
||||||
const { floor } = getAreaContext(area, this.hass!);
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
return {
|
return {
|
||||||
value: area.area_id,
|
value: area.area_id,
|
||||||
label: area.name,
|
label: area.name,
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||||
(acc, area) => {
|
(acc, area) => {
|
||||||
const { floor } = getAreaContext(area, this.hass!);
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
|
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
|
||||||
|
|
||||||
if (!acc[floorId]) {
|
if (!acc[floorId]) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
|
||||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||||
|
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
|
||||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||||
@@ -8,6 +8,9 @@ export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
|||||||
export class HaBottomSheet extends LitElement {
|
export class HaBottomSheet extends LitElement {
|
||||||
@property({ type: Boolean }) public open = false;
|
@property({ type: Boolean }) public open = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||||
|
public flexContent = false;
|
||||||
|
|
||||||
@state() private _drawerOpen = false;
|
@state() private _drawerOpen = false;
|
||||||
|
|
||||||
private _handleAfterHide() {
|
private _handleAfterHide() {
|
||||||
@@ -41,16 +44,19 @@ export class HaBottomSheet extends LitElement {
|
|||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
wa-drawer {
|
wa-drawer {
|
||||||
--wa-color-surface-raised: var(
|
--wa-color-surface-raised: transparent;
|
||||||
--ha-bottom-sheet-surface-background,
|
|
||||||
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
|
|
||||||
);
|
|
||||||
--spacing: 0;
|
--spacing: 0;
|
||||||
--size: auto;
|
--size: var(--ha-bottom-sheet-height, auto);
|
||||||
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
||||||
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
||||||
}
|
}
|
||||||
wa-drawer::part(dialog) {
|
wa-drawer::part(dialog) {
|
||||||
|
max-height: var(--ha-bottom-sheet-max-height, 90vh);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
wa-drawer::part(body) {
|
||||||
|
max-width: var(--ha-bottom-sheet-max-width);
|
||||||
|
width: 100%;
|
||||||
border-top-left-radius: var(
|
border-top-left-radius: var(
|
||||||
--ha-bottom-sheet-border-radius,
|
--ha-bottom-sheet-border-radius,
|
||||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||||
@@ -59,10 +65,19 @@ export class HaBottomSheet extends LitElement {
|
|||||||
--ha-bottom-sheet-border-radius,
|
--ha-bottom-sheet-border-radius,
|
||||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||||
);
|
);
|
||||||
max-height: 90vh;
|
background-color: var(
|
||||||
padding-bottom: var(--safe-area-inset-bottom);
|
--ha-bottom-sheet-surface-background,
|
||||||
padding-left: var(--safe-area-inset-left);
|
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
|
||||||
padding-right: var(--safe-area-inset-right);
|
);
|
||||||
|
padding: var(
|
||||||
|
--ha-bottom-sheet-padding,
|
||||||
|
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
|
||||||
|
var(--safe-area-inset-left)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([flexcontent]) wa-drawer::part(body) {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
tabindex=${this.noCollapse ? -1 : 0}
|
tabindex=${this.noCollapse ? -1 : 0}
|
||||||
aria-expanded=${this.expanded}
|
aria-expanded=${this.expanded}
|
||||||
aria-controls="sect1"
|
aria-controls="sect1"
|
||||||
|
part="summary"
|
||||||
>
|
>
|
||||||
${this.leftChevron ? chevronIcon : nothing}
|
${this.leftChevron ? chevronIcon : nothing}
|
||||||
<slot name="leading-icon"></slot>
|
<slot name="leading-icon"></slot>
|
||||||
@@ -170,6 +171,11 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-inline-start: 8px;
|
margin-inline-start: 8px;
|
||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
|
border-radius: var(--ha-border-radius-circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary:focus-visible ha-svg-icon.summary-icon {
|
||||||
|
background-color: var(--ha-color-fill-neutral-normal-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([left-chevron]) .summary-icon,
|
:host([left-chevron]) .summary-icon,
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
${!this._opened
|
${!this._opened
|
||||||
? html`
|
? html`
|
||||||
<ha-picker-field
|
<ha-picker-field
|
||||||
|
id="picker"
|
||||||
type="button"
|
type="button"
|
||||||
compact
|
compact
|
||||||
aria-label=${ifDefined(this.label)}
|
aria-label=${ifDefined(this.label)}
|
||||||
|
|||||||
@@ -5,16 +5,10 @@ import { LitElement, html } from "lit";
|
|||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
import type {
|
|
||||||
DeviceEntityDisplayLookup,
|
|
||||||
DeviceRegistryEntry,
|
|
||||||
} from "../data/device_registry";
|
|
||||||
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
|
||||||
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
|
||||||
import type { LabelRegistryEntry } from "../data/label_registry";
|
import type { LabelRegistryEntry } from "../data/label_registry";
|
||||||
import {
|
import {
|
||||||
createLabelRegistryEntry,
|
createLabelRegistryEntry,
|
||||||
|
getLabels,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../data/label_registry";
|
} from "../data/label_registry";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
@@ -137,20 +131,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _getLabels = memoizeOne(
|
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||||
(
|
|
||||||
labels: LabelRegistryEntry[] | undefined,
|
private _getItems = () => {
|
||||||
haAreas: HomeAssistant["areas"],
|
if (!this._labels || this._labels.length === 0) {
|
||||||
haDevices: HomeAssistant["devices"],
|
|
||||||
haEntities: HomeAssistant["entities"],
|
|
||||||
includeDomains: this["includeDomains"],
|
|
||||||
excludeDomains: this["excludeDomains"],
|
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
|
||||||
deviceFilter: this["deviceFilter"],
|
|
||||||
entityFilter: this["entityFilter"],
|
|
||||||
excludeLabels: this["excludeLabels"]
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
if (!labels || labels.length === 0) {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: NO_LABELS,
|
id: NO_LABELS,
|
||||||
@@ -160,178 +144,9 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = Object.values(haDevices);
|
return this._getLabelsMemoized(
|
||||||
const entities = Object.values(haEntities);
|
this.hass,
|
||||||
|
|
||||||
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 = haAreas[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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
|
|
||||||
id: label.label_id,
|
|
||||||
primary: label.name,
|
|
||||||
icon: label.icon || undefined,
|
|
||||||
icon_path: label.icon ? undefined : mdiLabel,
|
|
||||||
sorting_label: label.name,
|
|
||||||
search_labels: [label.name, label.label_id, label.description].filter(
|
|
||||||
(v): v is string => Boolean(v)
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getItems = () =>
|
|
||||||
this._getLabels(
|
|
||||||
this._labels,
|
this._labels,
|
||||||
this.hass.areas,
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
@@ -339,6 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.excludeLabels
|
this.excludeLabels
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||||
if (!labels) {
|
if (!labels) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
104
src/components/target-picker/dialog/dialog-target-details.ts
Normal file
104
src/components/target-picker/dialog/dialog-target-details.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import "../../ha-dialog-header";
|
||||||
|
import "../../ha-icon-button";
|
||||||
|
import "../../ha-icon-next";
|
||||||
|
import "../../ha-md-dialog";
|
||||||
|
import type { HaMdDialog } from "../../ha-md-dialog";
|
||||||
|
import "../../ha-md-list";
|
||||||
|
import "../../ha-md-list-item";
|
||||||
|
import "../../ha-svg-icon";
|
||||||
|
import "../ha-target-picker-item-row";
|
||||||
|
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
|
||||||
|
|
||||||
|
@customElement("ha-dialog-target-details")
|
||||||
|
class DialogTargetDetails extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: TargetDetailsDialogParams;
|
||||||
|
|
||||||
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
|
public showDialog(params: TargetDetailsDialogParams): void {
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._dialog?.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed() {
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
this._params = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.components.target-picker.target_details"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span slot="subtitle"
|
||||||
|
>${this.hass.localize(
|
||||||
|
`ui.components.target-picker.type.${this._params.type}`
|
||||||
|
)}:
|
||||||
|
${this._params.title}</span
|
||||||
|
>
|
||||||
|
</ha-dialog-header>
|
||||||
|
<div slot="content">
|
||||||
|
<ha-target-picker-item-row
|
||||||
|
.hass=${this.hass}
|
||||||
|
.type=${this._params.type}
|
||||||
|
.itemId=${this._params.itemId}
|
||||||
|
.deviceFilter=${this._params.deviceFilter}
|
||||||
|
.entityFilter=${this._params.entityFilter}
|
||||||
|
.includeDomains=${this._params.includeDomains}
|
||||||
|
.includeDeviceClasses=${this._params.includeDeviceClasses}
|
||||||
|
expand
|
||||||
|
></ha-target-picker-item-row>
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-md-dialog {
|
||||||
|
min-width: 400px;
|
||||||
|
max-height: 90%;
|
||||||
|
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6)
|
||||||
|
max(var(--safe-area-inset-bottom, var(--ha-space-0)), var(--ha-space-8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 600px), all and (max-height: 500px) {
|
||||||
|
ha-md-dialog {
|
||||||
|
--md-dialog-container-shape: var(--ha-space-0);
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-target-details": DialogTargetDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
|
||||||
|
import type { TargetType } from "../../../data/target";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||||
|
|
||||||
|
export type NewBackupType = "automatic" | "manual";
|
||||||
|
|
||||||
|
export interface TargetDetailsDialogParams {
|
||||||
|
title: string;
|
||||||
|
type: TargetType;
|
||||||
|
itemId: string;
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
|
includeDomains?: string[];
|
||||||
|
includeDeviceClasses?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadTargetDetailsDialog = () => import("./dialog-target-details");
|
||||||
|
|
||||||
|
export const showTargetDetailsDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params: TargetDetailsDialogParams
|
||||||
|
) =>
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-target-details",
|
||||||
|
dialogImport: loadTargetDetailsDialog,
|
||||||
|
dialogParams: params,
|
||||||
|
});
|
||||||
105
src/components/target-picker/ha-target-picker-item-group.ts
Normal file
105
src/components/target-picker/ha-target-picker-item-group.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
|
import type { TargetType, TargetTypeFloorless } from "../../data/target";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
|
||||||
|
import "../ha-expansion-panel";
|
||||||
|
import "../ha-md-list";
|
||||||
|
import "./ha-target-picker-item-row";
|
||||||
|
|
||||||
|
@customElement("ha-target-picker-item-group")
|
||||||
|
export class HaTargetPickerItemGroup extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public type!: TargetTypeFloorless;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public items!: Partial<
|
||||||
|
Record<TargetType, string[]>
|
||||||
|
>;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public collapsed = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only targets with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only targets with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
let count = 0;
|
||||||
|
Object.values(this.items).forEach((items) => {
|
||||||
|
if (items) {
|
||||||
|
count += items.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`<ha-expansion-panel .expanded=${!this.collapsed} left-chevron>
|
||||||
|
<div slot="header" class="heading">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.components.target-picker.selected.${this.type}`,
|
||||||
|
{
|
||||||
|
count,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${Object.entries(this.items).map(([type, items]) =>
|
||||||
|
items
|
||||||
|
? items.map(
|
||||||
|
(item) =>
|
||||||
|
html`<ha-target-picker-item-row
|
||||||
|
.hass=${this.hass}
|
||||||
|
.type=${type as TargetTypeFloorless}
|
||||||
|
.itemId=${item}
|
||||||
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
.includeDomains=${this.includeDomains}
|
||||||
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
|
></ha-target-picker-item-row>`
|
||||||
|
)
|
||||||
|
: nothing
|
||||||
|
)}
|
||||||
|
</ha-expansion-panel>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
--expansion-panel-content-padding: var(--ha-space-0);
|
||||||
|
}
|
||||||
|
ha-expansion-panel::part(summary) {
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-2);
|
||||||
|
font-weight: var(--ha-font-weight-bold);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
ha-md-list {
|
||||||
|
padding: var(--ha-space-0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-target-picker-item-group": HaTargetPickerItemGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
690
src/components/target-picker/ha-target-picker-item-row.ts
Normal file
690
src/components/target-picker/ha-target-picker-item-row.ts
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
import { consume } from "@lit/context";
|
||||||
|
import {
|
||||||
|
mdiClose,
|
||||||
|
mdiDevices,
|
||||||
|
mdiHome,
|
||||||
|
mdiLabel,
|
||||||
|
mdiTextureBox,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
|
import {
|
||||||
|
computeDeviceName,
|
||||||
|
computeDeviceNameDisplay,
|
||||||
|
} from "../../common/entity/compute_device_name";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import { getConfigEntry } from "../../data/config_entries";
|
||||||
|
import { labelsContext } from "../../data/context";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
|
import {
|
||||||
|
areaMeetsFilter,
|
||||||
|
deviceMeetsFilter,
|
||||||
|
entityRegMeetsFilter,
|
||||||
|
extractFromTarget,
|
||||||
|
type ExtractFromTargetResult,
|
||||||
|
type ExtractFromTargetResultReferenced,
|
||||||
|
type TargetType,
|
||||||
|
} from "../../data/target";
|
||||||
|
import { buttonLinkStyle } from "../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
|
||||||
|
import { floorDefaultIconPath } from "../ha-floor-icon";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
import "../ha-md-list";
|
||||||
|
import type { HaMdList } from "../ha-md-list";
|
||||||
|
import "../ha-md-list-item";
|
||||||
|
import type { HaMdListItem } from "../ha-md-list-item";
|
||||||
|
import "../ha-state-icon";
|
||||||
|
import "../ha-svg-icon";
|
||||||
|
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
|
||||||
|
|
||||||
|
@customElement("ha-target-picker-item-row")
|
||||||
|
export class HaTargetPickerItemRow extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ reflect: true }) public type!: TargetType;
|
||||||
|
|
||||||
|
@property({ attribute: "item-id" }) public itemId!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public expand = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "sub-entry", reflect: true })
|
||||||
|
public subEntry = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "hide-context" })
|
||||||
|
public hideContext = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public parentEntries?: ExtractFromTargetResultReferenced;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only targets with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only targets with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
@state() private _iconImg?: string;
|
||||||
|
|
||||||
|
@state() private _domainName?: string;
|
||||||
|
|
||||||
|
@state() private _entries?: ExtractFromTargetResult;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
|
_labelRegistry!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@query("ha-md-list-item") public item?: HaMdListItem;
|
||||||
|
|
||||||
|
@query("ha-md-list") public list?: HaMdList;
|
||||||
|
|
||||||
|
@query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow;
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (!this.subEntry && changedProps.has("itemId")) {
|
||||||
|
this._updateItemData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const { name, context, iconPath, fallbackIconPath, stateObject } =
|
||||||
|
this._itemData(this.type, this.itemId);
|
||||||
|
|
||||||
|
const showDevices = ["floor", "area", "label"].includes(this.type);
|
||||||
|
const showEntities = this.type !== "entity";
|
||||||
|
|
||||||
|
const entries = this.parentEntries || this._entries;
|
||||||
|
|
||||||
|
// Don't show sub entries that have no entities
|
||||||
|
if (
|
||||||
|
this.subEntry &&
|
||||||
|
this.type !== "entity" &&
|
||||||
|
(!entries || entries.referenced_entities.length === 0)
|
||||||
|
) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-list-item type="text">
|
||||||
|
<div slot="start">
|
||||||
|
${this.subEntry
|
||||||
|
? html`
|
||||||
|
<div class="horizontal-line-wrapper">
|
||||||
|
<div class="horizontal-line"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${iconPath
|
||||||
|
? html`<ha-icon .icon=${iconPath}></ha-icon>`
|
||||||
|
: this._iconImg
|
||||||
|
? html`<img
|
||||||
|
alt=${this._domainName || ""}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${this._iconImg}
|
||||||
|
/>`
|
||||||
|
: fallbackIconPath
|
||||||
|
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||||
|
: stateObject
|
||||||
|
? html`
|
||||||
|
<ha-state-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${stateObject}
|
||||||
|
>
|
||||||
|
</ha-state-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="headline">${name}</div>
|
||||||
|
${context && !this.hideContext
|
||||||
|
? html`<span slot="supporting-text">${context}</span>`
|
||||||
|
: this._domainName && this.subEntry
|
||||||
|
? html`<span slot="supporting-text" class="domain"
|
||||||
|
>${this._domainName}</span
|
||||||
|
>`
|
||||||
|
: nothing}
|
||||||
|
${!this.subEntry &&
|
||||||
|
((entries && (showEntities || showDevices)) || this._domainName)
|
||||||
|
? html`
|
||||||
|
<div slot="end" class="summary">
|
||||||
|
${showEntities && !this.expand
|
||||||
|
? html`<button class="main link" @click=${this._openDetails}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.target-picker.entities_count",
|
||||||
|
{
|
||||||
|
count: entries?.referenced_entities.length,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</button>`
|
||||||
|
: showEntities
|
||||||
|
? html`<span class="main">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.target-picker.entities_count",
|
||||||
|
{
|
||||||
|
count: entries?.referenced_entities.length,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>`
|
||||||
|
: nothing}
|
||||||
|
${showDevices
|
||||||
|
? html`<span class="secondary"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.components.target-picker.devices_count",
|
||||||
|
{
|
||||||
|
count: entries?.referenced_devices.length,
|
||||||
|
}
|
||||||
|
)}</span
|
||||||
|
>`
|
||||||
|
: nothing}
|
||||||
|
${this._domainName && !showDevices
|
||||||
|
? html`<span class="secondary domain"
|
||||||
|
>${this._domainName}</span
|
||||||
|
>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${!this.expand && !this.subEntry
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiClose}
|
||||||
|
slot="end"
|
||||||
|
@click=${this._removeItem}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list-item>
|
||||||
|
${this.expand && entries && entries.referenced_entities
|
||||||
|
? this._renderEntries()
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderEntries() {
|
||||||
|
const entries = this.parentEntries || this._entries;
|
||||||
|
|
||||||
|
let nextType: TargetType =
|
||||||
|
this.type === "floor"
|
||||||
|
? "area"
|
||||||
|
: this.type === "area"
|
||||||
|
? "device"
|
||||||
|
: "entity";
|
||||||
|
|
||||||
|
if (this.type === "label") {
|
||||||
|
if (entries?.referenced_areas.length) {
|
||||||
|
nextType = "area";
|
||||||
|
} else if (entries?.referenced_devices.length) {
|
||||||
|
nextType = "device";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows1 =
|
||||||
|
(nextType === "area"
|
||||||
|
? entries?.referenced_areas
|
||||||
|
: nextType === "device"
|
||||||
|
? entries?.referenced_devices
|
||||||
|
: entries?.referenced_entities) || [];
|
||||||
|
|
||||||
|
const devicesInAreas = [] as string[];
|
||||||
|
|
||||||
|
const rows1Entries =
|
||||||
|
nextType === "entity"
|
||||||
|
? undefined
|
||||||
|
: rows1.map((rowItem) => {
|
||||||
|
const nextEntries = {
|
||||||
|
referenced_areas: [] as string[],
|
||||||
|
referenced_devices: [] as string[],
|
||||||
|
referenced_entities: [] as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nextType === "area") {
|
||||||
|
nextEntries.referenced_devices =
|
||||||
|
entries?.referenced_devices.filter(
|
||||||
|
(device_id) =>
|
||||||
|
this.hass.devices?.[device_id]?.area_id === rowItem &&
|
||||||
|
entries?.referenced_entities.some(
|
||||||
|
(entity_id) =>
|
||||||
|
this.hass.entities?.[entity_id]?.device_id === device_id
|
||||||
|
)
|
||||||
|
) || ([] as string[]);
|
||||||
|
|
||||||
|
devicesInAreas.push(...nextEntries.referenced_devices);
|
||||||
|
|
||||||
|
nextEntries.referenced_entities =
|
||||||
|
entries?.referenced_entities.filter((entity_id) => {
|
||||||
|
const entity = this.hass.entities[entity_id];
|
||||||
|
return (
|
||||||
|
entity.area_id === rowItem ||
|
||||||
|
!entity.device_id ||
|
||||||
|
nextEntries.referenced_devices.includes(entity.device_id)
|
||||||
|
);
|
||||||
|
}) || ([] as string[]);
|
||||||
|
|
||||||
|
return nextEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEntries.referenced_entities =
|
||||||
|
entries?.referenced_entities.filter(
|
||||||
|
(entity_id) =>
|
||||||
|
this.hass.entities?.[entity_id]?.device_id === rowItem
|
||||||
|
) || ([] as string[]);
|
||||||
|
|
||||||
|
return nextEntries;
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityRows =
|
||||||
|
this.type === "label" && entries
|
||||||
|
? entries.referenced_entities.filter((entity_id) =>
|
||||||
|
this.hass.entities[entity_id].labels.includes(this.itemId)
|
||||||
|
)
|
||||||
|
: nextType === "device" && entries
|
||||||
|
? entries.referenced_entities.filter(
|
||||||
|
(entity_id) =>
|
||||||
|
this.hass.entities[entity_id].area_id === this.itemId
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const deviceRows =
|
||||||
|
this.type === "label" && entries
|
||||||
|
? entries.referenced_devices.filter(
|
||||||
|
(device_id) =>
|
||||||
|
!devicesInAreas.includes(device_id) &&
|
||||||
|
this.hass.devices[device_id].labels.includes(this.itemId)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const deviceRowsEntries =
|
||||||
|
deviceRows.length === 0
|
||||||
|
? undefined
|
||||||
|
: deviceRows.map((device_id) => ({
|
||||||
|
referenced_areas: [] as string[],
|
||||||
|
referenced_devices: [] as string[],
|
||||||
|
referenced_entities:
|
||||||
|
entries?.referenced_entities.filter(
|
||||||
|
(entity_id) =>
|
||||||
|
this.hass.entities?.[entity_id]?.device_id === device_id
|
||||||
|
) || ([] as string[]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="entries-tree">
|
||||||
|
<div class="line-wrapper">
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
<ha-md-list class="entries">
|
||||||
|
${rows1.map(
|
||||||
|
(itemId, index) => html`
|
||||||
|
<ha-target-picker-item-row
|
||||||
|
sub-entry
|
||||||
|
.hass=${this.hass}
|
||||||
|
.type=${nextType}
|
||||||
|
.itemId=${itemId}
|
||||||
|
.parentEntries=${rows1Entries?.[index]}
|
||||||
|
.hideContext=${this.hideContext || this.type !== "label"}
|
||||||
|
expand
|
||||||
|
></ha-target-picker-item-row>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${deviceRows.map(
|
||||||
|
(itemId, index) => html`
|
||||||
|
<ha-target-picker-item-row
|
||||||
|
sub-entry
|
||||||
|
.hass=${this.hass}
|
||||||
|
type="device"
|
||||||
|
.itemId=${itemId}
|
||||||
|
.parentEntries=${deviceRowsEntries?.[index]}
|
||||||
|
.hideContext=${this.hideContext || this.type !== "label"}
|
||||||
|
expand
|
||||||
|
></ha-target-picker-item-row>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${entityRows.map(
|
||||||
|
(itemId) => html`
|
||||||
|
<ha-target-picker-item-row
|
||||||
|
sub-entry
|
||||||
|
.hass=${this.hass}
|
||||||
|
type="entity"
|
||||||
|
.itemId=${itemId}
|
||||||
|
.hideContext=${this.hideContext || this.type !== "label"}
|
||||||
|
></ha-target-picker-item-row>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateItemData() {
|
||||||
|
if (this.type === "entity") {
|
||||||
|
this._entries = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const entries = await extractFromTarget(this.hass, {
|
||||||
|
[`${this.type}_id`]: [this.itemId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const hiddenAreaIds: string[] = [];
|
||||||
|
if (this.type === "floor" || this.type === "label") {
|
||||||
|
entries.referenced_areas = entries.referenced_areas.filter(
|
||||||
|
(area_id) => {
|
||||||
|
const area = this.hass.areas[area_id];
|
||||||
|
if (
|
||||||
|
(this.type === "floor" || area.labels.includes(this.itemId)) &&
|
||||||
|
areaMeetsFilter(
|
||||||
|
area,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.includeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.hass.states,
|
||||||
|
this.entityFilter
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hiddenAreaIds.push(area_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenDeviceIds: string[] = [];
|
||||||
|
if (
|
||||||
|
this.type === "floor" ||
|
||||||
|
this.type === "area" ||
|
||||||
|
this.type === "label"
|
||||||
|
) {
|
||||||
|
entries.referenced_devices = entries.referenced_devices.filter(
|
||||||
|
(device_id) => {
|
||||||
|
const device = this.hass.devices[device_id];
|
||||||
|
if (
|
||||||
|
!hiddenAreaIds.includes(device.area_id || "") &&
|
||||||
|
(this.type !== "label" || device.labels.includes(this.itemId)) &&
|
||||||
|
deviceMeetsFilter(
|
||||||
|
device,
|
||||||
|
this.hass.entities,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.includeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.hass.states,
|
||||||
|
this.entityFilter
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hiddenDeviceIds.push(device_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.referenced_entities = entries.referenced_entities.filter(
|
||||||
|
(entity_id) => {
|
||||||
|
const entity = this.hass.entities[entity_id];
|
||||||
|
if (hiddenDeviceIds.includes(entity.device_id || "")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(this.type === "area" && entity.area_id === this.itemId) ||
|
||||||
|
(this.type === "label" && entity.labels.includes(this.itemId)) ||
|
||||||
|
entries.referenced_devices.includes(entity.device_id || "")
|
||||||
|
) {
|
||||||
|
return entityRegMeetsFilter(
|
||||||
|
entity,
|
||||||
|
this.type === "label",
|
||||||
|
this.includeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.hass.states,
|
||||||
|
this.entityFilter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this._entries = entries;
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Failed to extract target", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemData = memoizeOne((type: TargetType, item: string) => {
|
||||||
|
if (type === "floor") {
|
||||||
|
const floor = this.hass.floors?.[item];
|
||||||
|
return {
|
||||||
|
name: floor?.name || item,
|
||||||
|
iconPath: floor?.icon,
|
||||||
|
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "area") {
|
||||||
|
const area = this.hass.areas?.[item];
|
||||||
|
return {
|
||||||
|
name: area?.name || item,
|
||||||
|
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
|
||||||
|
iconPath: area?.icon,
|
||||||
|
fallbackIconPath: mdiTextureBox,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "device") {
|
||||||
|
const device = this.hass.devices?.[item];
|
||||||
|
|
||||||
|
if (device.primary_config_entry) {
|
||||||
|
this._getDeviceDomain(device.primary_config_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
|
||||||
|
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
|
||||||
|
fallbackIconPath: mdiDevices,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "entity") {
|
||||||
|
this._setDomainName(computeDomain(item));
|
||||||
|
|
||||||
|
const stateObject = this.hass.states[item];
|
||||||
|
const entityName = computeEntityName(
|
||||||
|
stateObject,
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices
|
||||||
|
);
|
||||||
|
const { area, device } = getEntityContext(
|
||||||
|
stateObject,
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.floors
|
||||||
|
);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
const context = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||||
|
return {
|
||||||
|
name: entityName || deviceName || item,
|
||||||
|
context,
|
||||||
|
stateObject,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// type label
|
||||||
|
const label = this._labelRegistry.find((lab) => lab.label_id === item);
|
||||||
|
return {
|
||||||
|
name: label?.name || item,
|
||||||
|
iconPath: label?.icon,
|
||||||
|
fallbackIconPath: mdiLabel,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
private _setDomainName(domain: string) {
|
||||||
|
this._domainName = domainToName(this.hass.localize, domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeItem(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "remove-target-item", {
|
||||||
|
type: this.type,
|
||||||
|
id: this.itemId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getDeviceDomain(configEntryId: string) {
|
||||||
|
try {
|
||||||
|
const data = await getConfigEntry(this.hass, configEntryId);
|
||||||
|
const domain = data.config_entry.domain;
|
||||||
|
this._iconImg = brandsUrl({
|
||||||
|
domain: domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._setDomainName(domain);
|
||||||
|
} catch {
|
||||||
|
// failed to load config entry -> ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openDetails() {
|
||||||
|
showTargetDetailsDialog(this, {
|
||||||
|
title: this._itemData(this.type, this.itemId).name,
|
||||||
|
type: this.type,
|
||||||
|
itemId: this.itemId,
|
||||||
|
deviceFilter: this.deviceFilter,
|
||||||
|
entityFilter: this.entityFilter,
|
||||||
|
includeDomains: this.includeDomains,
|
||||||
|
includeDeviceClasses: this.includeDeviceClasses,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
buttonLinkStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--md-list-item-top-space: var(--ha-space-0);
|
||||||
|
--md-list-item-bottom-space: var(--ha-space-0);
|
||||||
|
--md-list-item-leading-space: var(--ha-space-2);
|
||||||
|
--md-list-item-trailing-space: var(--ha-space-2);
|
||||||
|
--md-list-item-two-line-container-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([expand]:not([sub-entry])) ha-md-list-item {
|
||||||
|
border: 2px solid var(--ha-color-border-neutral-loud);
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||||
|
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||||
|
}
|
||||||
|
|
||||||
|
state-badge {
|
||||||
|
color: var(--ha-color-on-neutral-quiet);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
ha-icon-button {
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
line-height: var(--ha-line-height-condensed);
|
||||||
|
}
|
||||||
|
:host([sub-entry]) .summary {
|
||||||
|
margin-right: var(--ha-space-12);
|
||||||
|
}
|
||||||
|
.summary .main {
|
||||||
|
font-weight: var(--ha-font-weight-medium);
|
||||||
|
}
|
||||||
|
.summary .secondary {
|
||||||
|
font-size: var(--ha-font-size-s);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.domain {
|
||||||
|
font-family: var(--ha-font-family-code);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entries-tree {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entries-tree .line-wrapper {
|
||||||
|
padding: var(--ha-space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entries-tree .line-wrapper .line {
|
||||||
|
border-left: 2px dashed var(--divider-color);
|
||||||
|
height: calc(100% - 28px);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([sub-entry]) .entries-tree .line-wrapper .line {
|
||||||
|
height: calc(100% - 12px);
|
||||||
|
top: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entries {
|
||||||
|
padding: 0;
|
||||||
|
--md-item-overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-line-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.horizontal-line-wrapper .horizontal-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
margin-inline-start: -28px;
|
||||||
|
width: 29px;
|
||||||
|
border-top: 2px dashed var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.link:hover,
|
||||||
|
button.link:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-target-picker-item-row": HaTargetPickerItemRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
1107
src/components/target-picker/ha-target-picker-selector.ts
Normal file
1107
src/components/target-picker/ha-target-picker-selector.ts
Normal file
File diff suppressed because it is too large
Load Diff
354
src/components/target-picker/ha-target-picker-value-chip.ts
Normal file
354
src/components/target-picker/ha-target-picker-value-chip.ts
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import { consume } from "@lit/context";
|
||||||
|
// @ts-ignore
|
||||||
|
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||||
|
import {
|
||||||
|
mdiClose,
|
||||||
|
mdiDevices,
|
||||||
|
mdiHome,
|
||||||
|
mdiLabel,
|
||||||
|
mdiTextureBox,
|
||||||
|
mdiUnfoldMoreVertical,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing, unsafeCSS } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../common/color/compute-color";
|
||||||
|
import { hex2rgb } from "../../common/color/convert-color";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
computeDeviceName,
|
||||||
|
computeDeviceNameDisplay,
|
||||||
|
} from "../../common/entity/compute_device_name";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
|
import { getConfigEntry } from "../../data/config_entries";
|
||||||
|
import { labelsContext } from "../../data/context";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
|
import type { TargetType } from "../../data/target";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
|
import { floorDefaultIconPath } from "../ha-floor-icon";
|
||||||
|
import "../ha-icon";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
import "../ha-md-list";
|
||||||
|
import "../ha-md-list-item";
|
||||||
|
import "../ha-state-icon";
|
||||||
|
import "../ha-tooltip";
|
||||||
|
|
||||||
|
@customElement("ha-target-picker-value-chip")
|
||||||
|
export class HaTargetPickerValueChip extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public type!: TargetType;
|
||||||
|
|
||||||
|
@property({ attribute: "item-id" }) public itemId!: string;
|
||||||
|
|
||||||
|
@state() private _domainName?: string;
|
||||||
|
|
||||||
|
@state() private _iconImg?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
|
_labelRegistry!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const { name, iconPath, fallbackIconPath, stateObject, color } =
|
||||||
|
this._itemData(this.type, this.itemId);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="mdc-chip ${classMap({
|
||||||
|
[this.type]: true,
|
||||||
|
})}"
|
||||||
|
style=${color
|
||||||
|
? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
${iconPath
|
||||||
|
? html`<ha-icon
|
||||||
|
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||||
|
.icon=${iconPath}
|
||||||
|
></ha-icon>`
|
||||||
|
: this._iconImg
|
||||||
|
? html`<img
|
||||||
|
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||||
|
alt=${this._domainName || ""}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${this._iconImg}
|
||||||
|
/>`
|
||||||
|
: fallbackIconPath
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||||
|
.path=${fallbackIconPath}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: stateObject
|
||||||
|
? html`<ha-state-icon
|
||||||
|
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${stateObject}
|
||||||
|
></ha-state-icon>`
|
||||||
|
: nothing}
|
||||||
|
<span role="gridcell">
|
||||||
|
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||||
|
<span id="title-${this.itemId}" class="mdc-chip__text"
|
||||||
|
>${name}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
${this.type === "entity"
|
||||||
|
? nothing
|
||||||
|
: html`<span role="gridcell">
|
||||||
|
<ha-tooltip .for="expand-${this.itemId}"
|
||||||
|
>${this.hass.localize(
|
||||||
|
`ui.components.target-picker.expand_${this.type}_id`
|
||||||
|
)}
|
||||||
|
</ha-tooltip>
|
||||||
|
<ha-icon-button
|
||||||
|
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.components.target-picker.expand"
|
||||||
|
)}
|
||||||
|
.path=${mdiUnfoldMoreVertical}
|
||||||
|
hide-title
|
||||||
|
.id="expand-${this.itemId}"
|
||||||
|
.type=${this.type}
|
||||||
|
@click=${this._handleExpand}
|
||||||
|
></ha-icon-button>
|
||||||
|
</span>`}
|
||||||
|
<span role="gridcell">
|
||||||
|
<ha-tooltip .for="remove-${this.itemId}">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.components.target-picker.remove_${this.type}_id`
|
||||||
|
)}
|
||||||
|
</ha-tooltip>
|
||||||
|
<ha-icon-button
|
||||||
|
class="mdc-chip__icon mdc-chip__icon--trailing"
|
||||||
|
.label=${this.hass.localize("ui.components.target-picker.remove")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
hide-title
|
||||||
|
.id="remove-${this.itemId}"
|
||||||
|
.type=${this.type}
|
||||||
|
@click=${this._removeItem}
|
||||||
|
></ha-icon-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemData = memoizeOne((type: TargetType, itemId: string) => {
|
||||||
|
if (type === "floor") {
|
||||||
|
const floor = this.hass.floors?.[itemId];
|
||||||
|
return {
|
||||||
|
name: floor?.name || itemId,
|
||||||
|
iconPath: floor?.icon,
|
||||||
|
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "area") {
|
||||||
|
const area = this.hass.areas?.[itemId];
|
||||||
|
return {
|
||||||
|
name: area?.name || itemId,
|
||||||
|
iconPath: area?.icon,
|
||||||
|
fallbackIconPath: mdiTextureBox,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "device") {
|
||||||
|
const device = this.hass.devices?.[itemId];
|
||||||
|
|
||||||
|
if (device.primary_config_entry) {
|
||||||
|
this._getDeviceDomain(device.primary_config_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
|
||||||
|
fallbackIconPath: mdiDevices,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "entity") {
|
||||||
|
this._setDomainName(computeDomain(itemId));
|
||||||
|
|
||||||
|
const stateObject = this.hass.states[itemId];
|
||||||
|
const entityName = computeEntityName(
|
||||||
|
stateObject,
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices
|
||||||
|
);
|
||||||
|
const { device } = getEntityContext(
|
||||||
|
stateObject,
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.floors
|
||||||
|
);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
return {
|
||||||
|
name: entityName || deviceName || itemId,
|
||||||
|
stateObject,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// type label
|
||||||
|
const label = this._labelRegistry.find((lab) => lab.label_id === itemId);
|
||||||
|
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 {
|
||||||
|
name: label?.name || itemId,
|
||||||
|
iconPath: label?.icon,
|
||||||
|
fallbackIconPath: mdiLabel,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
private _setDomainName(domain: string) {
|
||||||
|
this._domainName = domainToName(this.hass.localize, domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getDeviceDomain(configEntryId: string) {
|
||||||
|
try {
|
||||||
|
const data = await getConfigEntry(this.hass, configEntryId);
|
||||||
|
const domain = data.config_entry.domain;
|
||||||
|
this._iconImg = brandsUrl({
|
||||||
|
domain: domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._setDomainName(domain);
|
||||||
|
} catch {
|
||||||
|
// failed to load config entry -> ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeItem(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "remove-target-item", {
|
||||||
|
type: this.type,
|
||||||
|
id: this.itemId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleExpand(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "expand-target-item", {
|
||||||
|
type: this.type,
|
||||||
|
id: this.itemId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
${unsafeCSS(chipStyles)}
|
||||||
|
.mdc-chip {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.add {
|
||||||
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
}
|
||||||
|
.add-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
.mdc-chip:not(.add) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.mdc-chip ha-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.mdc-chip ha-icon-button ha-svg-icon {
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.mdc-chip__icon.mdc-chip__icon--trailing {
|
||||||
|
width: var(--ha-space-4);
|
||||||
|
height: var(--ha-space-4);
|
||||||
|
--mdc-icon-size: 14px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-inline-start: var(--ha-space-1) !important;
|
||||||
|
margin-inline-end: calc(-1 * var(--ha-space-1)) !important;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.mdc-chip__icon--leading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
border-radius: var(--ha-border-radius-circle);
|
||||||
|
padding: 6px;
|
||||||
|
margin-left: -13px !important;
|
||||||
|
margin-inline-start: -13px !important;
|
||||||
|
margin-inline-end: var(--ha-space-1) !important;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.expand-btn {
|
||||||
|
margin-right: var(--ha-space-0);
|
||||||
|
margin-inline-end: var(--ha-space-0);
|
||||||
|
margin-inline-start: initial;
|
||||||
|
}
|
||||||
|
.mdc-chip.area:not(.add),
|
||||||
|
.mdc-chip.floor:not(.add) {
|
||||||
|
border: 1px solid #fed6a4;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.area:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.area.add,
|
||||||
|
.mdc-chip.floor:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.floor.add {
|
||||||
|
background: #fed6a4;
|
||||||
|
}
|
||||||
|
.mdc-chip.device:not(.add) {
|
||||||
|
border: 1px solid #a8e1fb;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.device:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.device.add {
|
||||||
|
background: #a8e1fb;
|
||||||
|
}
|
||||||
|
.mdc-chip.entity:not(.add) {
|
||||||
|
border: 1px solid #d2e7b9;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.entity:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.entity.add {
|
||||||
|
background: #d2e7b9;
|
||||||
|
}
|
||||||
|
.mdc-chip.label:not(.add) {
|
||||||
|
border: 1px solid var(--color, #e0e0e0);
|
||||||
|
background: var(--card-background-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.label:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.label.add {
|
||||||
|
background: var(--background-color, #e0e0e0);
|
||||||
|
}
|
||||||
|
.mdc-chip:hover {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
:host([disabled]) .mdc-chip {
|
||||||
|
opacity: var(--light-disabled-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tooltip-icon-img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-target-picker-value-chip": HaTargetPickerValueChip;
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/data/area_floor.ts
Normal file
259
src/data/area_floor.ts
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import type { AreaRegistryEntry } from "./area_registry";
|
||||||
|
import {
|
||||||
|
getDeviceEntityDisplayLookup,
|
||||||
|
type DeviceEntityDisplayLookup,
|
||||||
|
type DeviceRegistryEntry,
|
||||||
|
} from "./device_registry";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
|
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||||
|
import { getFloorAreaLookup, type FloorRegistryEntry } from "./floor_registry";
|
||||||
|
|
||||||
|
export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||||
|
type: "floor" | "area";
|
||||||
|
floor?: FloorRegistryEntry;
|
||||||
|
area?: AreaRegistryEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AreaFloorValue {
|
||||||
|
id: string;
|
||||||
|
type: "floor" | "area";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAreasAndFloors = (
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
haFloors: HomeAssistant["floors"],
|
||||||
|
haAreas: HomeAssistant["areas"],
|
||||||
|
haDevices: HomeAssistant["devices"],
|
||||||
|
haEntities: HomeAssistant["entities"],
|
||||||
|
formatId: (value: AreaFloorValue) => string,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeAreas?: string[],
|
||||||
|
excludeFloors?: string[]
|
||||||
|
): FloorComboBoxItem[] => {
|
||||||
|
const floors = Object.values(haFloors);
|
||||||
|
const areas = Object.values(haAreas);
|
||||||
|
const devices = Object.values(haDevices);
|
||||||
|
const entities = Object.values(haEntities);
|
||||||
|
|
||||||
|
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.area_id);
|
||||||
|
|
||||||
|
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 = 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 = 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 = states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter(stateObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter!(stateObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputAreas = areas;
|
||||||
|
|
||||||
|
let areaIds: string[] | undefined;
|
||||||
|
|
||||||
|
if (inputDevices) {
|
||||||
|
areaIds = inputDevices
|
||||||
|
.filter((device) => device.area_id)
|
||||||
|
.map((device) => device.area_id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputEntities) {
|
||||||
|
areaIds = (areaIds ?? []).concat(
|
||||||
|
inputEntities
|
||||||
|
.filter((entity) => entity.area_id)
|
||||||
|
.map((entity) => entity.area_id!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds) {
|
||||||
|
outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeAreas) {
|
||||||
|
outputAreas = outputAreas.filter(
|
||||||
|
(area) => !excludeAreas!.includes(area.area_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeFloors) {
|
||||||
|
outputAreas = outputAreas.filter(
|
||||||
|
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||||
|
const unassignedAreas = Object.values(outputAreas).filter(
|
||||||
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const floorAreaEntries: [
|
||||||
|
FloorRegistryEntry | undefined,
|
||||||
|
AreaRegistryEntry[],
|
||||||
|
][] = Object.entries(floorAreaLookup)
|
||||||
|
.map(([floorId, floorAreas]) => {
|
||||||
|
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
||||||
|
return [floor, floorAreas] as const;
|
||||||
|
})
|
||||||
|
.sort(([floorA], [floorB]) => {
|
||||||
|
if (floorA.level !== floorB.level) {
|
||||||
|
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
||||||
|
}
|
||||||
|
return stringCompare(floorA.name, floorB.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const items: FloorComboBoxItem[] = [];
|
||||||
|
|
||||||
|
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||||
|
if (floor) {
|
||||||
|
const floorName = computeFloorName(floor);
|
||||||
|
|
||||||
|
const areaSearchLabels = floorAreas
|
||||||
|
.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return [area.area_id, areaName, ...area.aliases];
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||||
|
type: "floor",
|
||||||
|
primary: floorName,
|
||||||
|
floor: floor,
|
||||||
|
search_labels: [
|
||||||
|
floor.floor_id,
|
||||||
|
floorName,
|
||||||
|
...floor.aliases,
|
||||||
|
...areaSearchLabels,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items.push(
|
||||||
|
...floorAreas.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return {
|
||||||
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
|
type: "area" as const,
|
||||||
|
primary: areaName,
|
||||||
|
area: area,
|
||||||
|
icon: area.icon || undefined,
|
||||||
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
...unassignedAreas.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return {
|
||||||
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
|
type: "area" as const,
|
||||||
|
primary: areaName,
|
||||||
|
area: area,
|
||||||
|
icon: area.icon || undefined,
|
||||||
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
|
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||||
|
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { getDeviceContext } from "../common/entity/context/get_device_context";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { ConfigEntry } from "./config_entries";
|
import type { ConfigEntry } from "./config_entries";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
} from "./entity_registry";
|
} from "./entity_registry";
|
||||||
import type { EntitySources } from "./entity_sources";
|
import type { EntitySources } from "./entity_sources";
|
||||||
|
import { domainToName } from "./integration";
|
||||||
import type { RegistryEntry } from "./registry";
|
import type { RegistryEntry } from "./registry";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -163,3 +171,147 @@ export const getDeviceIntegrationLookup = (
|
|||||||
}
|
}
|
||||||
return deviceIntegrations;
|
return deviceIntegrations;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DevicePickerItem extends PickerComboBoxItem {
|
||||||
|
domain?: string;
|
||||||
|
domain_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDevices = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
configEntryLookup: Record<string, ConfigEntry>,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeDevices?: string[],
|
||||||
|
value?: string
|
||||||
|
): DevicePickerItem[] => {
|
||||||
|
const devices = Object.values(hass.devices);
|
||||||
|
const entities = Object.values(hass.entities);
|
||||||
|
|
||||||
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
includeDomains ||
|
||||||
|
excludeDomains ||
|
||||||
|
includeDeviceClasses ||
|
||||||
|
entityFilter
|
||||||
|
) {
|
||||||
|
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputDevices = devices.filter(
|
||||||
|
(device) => device.id === value || !device.disabled_by
|
||||||
|
);
|
||||||
|
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDevices) {
|
||||||
|
inputDevices = inputDevices.filter(
|
||||||
|
(device) => !excludeDevices!.includes(device.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 = hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
inputDevices = inputDevices.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return devEntities.some((entity) => {
|
||||||
|
const stateObj = hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter(stateObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceFilter) {
|
||||||
|
inputDevices = inputDevices.filter(
|
||||||
|
(device) =>
|
||||||
|
// We always want to include the device of the current value
|
||||||
|
device.id === value || deviceFilter!(device)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
|
||||||
|
const deviceName = computeDeviceNameDisplay(
|
||||||
|
device,
|
||||||
|
hass,
|
||||||
|
deviceEntityLookup[device.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { area } = getDeviceContext(device, hass);
|
||||||
|
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const configEntry = device.primary_config_entry
|
||||||
|
? configEntryLookup?.[device.primary_config_entry]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const domain = configEntry?.domain;
|
||||||
|
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: device.id,
|
||||||
|
label: "",
|
||||||
|
primary:
|
||||||
|
deviceName ||
|
||||||
|
hass.localize("ui.components.device-picker.unnamed_device"),
|
||||||
|
secondary: areaName,
|
||||||
|
domain: configEntry?.domain,
|
||||||
|
domain_name: domainName,
|
||||||
|
search_labels: [deviceName, areaName, domain, domainName].filter(
|
||||||
|
Boolean
|
||||||
|
) as string[],
|
||||||
|
sorting_label: deviceName || "zzz",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputDevices;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
||||||
|
|
||||||
export const UNAVAILABLE = "unavailable";
|
export const UNAVAILABLE = "unavailable";
|
||||||
@@ -10,3 +11,5 @@ export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
|
|||||||
|
|
||||||
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
|
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
|
||||||
export const isOffState = arrayLiteralIncludes(OFF_STATES);
|
export const isOffState = arrayLiteralIncludes(OFF_STATES);
|
||||||
|
|
||||||
|
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import type { Connection } from "home-assistant-js-websocket";
|
import type { Connection, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { createCollection } from "home-assistant-js-websocket";
|
import { createCollection } from "home-assistant-js-websocket";
|
||||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { computeEntityNameList } from "../common/entity/compute_entity_name_display";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
|
import { domainToName } from "./integration";
|
||||||
import type { LightColor } from "./light";
|
import type { LightColor } from "./light";
|
||||||
import type { RegistryEntry } from "./registry";
|
import type { RegistryEntry } from "./registry";
|
||||||
|
|
||||||
@@ -324,3 +329,122 @@ export const getAutomaticEntityIds = (
|
|||||||
type: "config/entity_registry/get_automatic_entity_ids",
|
type: "config/entity_registry/get_automatic_entity_ids",
|
||||||
entity_ids,
|
entity_ids,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface EntityComboBoxItem extends PickerComboBoxItem {
|
||||||
|
domain_name?: string;
|
||||||
|
stateObj?: HassEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
includeUnitOfMeasurement?: string[],
|
||||||
|
includeEntities?: string[],
|
||||||
|
excludeEntities?: string[],
|
||||||
|
value?: string
|
||||||
|
): EntityComboBoxItem[] => {
|
||||||
|
let items: EntityComboBoxItem[] = [];
|
||||||
|
|
||||||
|
let entityIds = Object.keys(hass.states);
|
||||||
|
|
||||||
|
if (includeEntities) {
|
||||||
|
entityIds = entityIds.filter((entityId) =>
|
||||||
|
includeEntities.includes(entityId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeEntities) {
|
||||||
|
entityIds = entityIds.filter(
|
||||||
|
(entityId) => !excludeEntities.includes(entityId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDomains) {
|
||||||
|
entityIds = entityIds.filter((eid) =>
|
||||||
|
includeDomains.includes(computeDomain(eid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDomains) {
|
||||||
|
entityIds = entityIds.filter(
|
||||||
|
(eid) => !excludeDomains.includes(computeDomain(eid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
||||||
|
const stateObj = hass.states[entityId];
|
||||||
|
|
||||||
|
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||||
|
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||||
|
stateObj,
|
||||||
|
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||||
|
hass.entities,
|
||||||
|
hass.devices,
|
||||||
|
hass.areas,
|
||||||
|
hass.floors
|
||||||
|
);
|
||||||
|
|
||||||
|
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
||||||
|
|
||||||
|
const isRTL = computeRTL(hass);
|
||||||
|
|
||||||
|
const primary = entityName || deviceName || entityId;
|
||||||
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entityId,
|
||||||
|
primary: primary,
|
||||||
|
secondary: secondary,
|
||||||
|
domain_name: domainName,
|
||||||
|
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
||||||
|
search_labels: [
|
||||||
|
entityName,
|
||||||
|
deviceName,
|
||||||
|
areaName,
|
||||||
|
domainName,
|
||||||
|
friendlyName,
|
||||||
|
entityId,
|
||||||
|
].filter(Boolean) as string[],
|
||||||
|
a11y_label: a11yLabel,
|
||||||
|
stateObj: stateObj,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includeDeviceClasses) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
// We always want to include the entity of the current value
|
||||||
|
item.id === value ||
|
||||||
|
(item.stateObj?.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(item.stateObj.attributes.device_class))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeUnitOfMeasurement) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
// We always want to include the entity of the current value
|
||||||
|
item.id === value ||
|
||||||
|
(item.stateObj?.attributes.unit_of_measurement &&
|
||||||
|
includeUnitOfMeasurement.includes(
|
||||||
|
item.stateObj.attributes.unit_of_measurement
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
// We always want to include the entity of the current value
|
||||||
|
item.id === value || (item.stateObj && entityFilter!(item.stateObj))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
|
import { mdiLabel } from "@mdi/js";
|
||||||
import type { Connection } from "home-assistant-js-websocket";
|
import type { Connection } from "home-assistant-js-websocket";
|
||||||
import { createCollection } from "home-assistant-js-websocket";
|
import { createCollection } from "home-assistant-js-websocket";
|
||||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
import {
|
||||||
|
getDeviceEntityDisplayLookup,
|
||||||
|
type DeviceEntityDisplayLookup,
|
||||||
|
type DeviceRegistryEntry,
|
||||||
|
} from "./device_registry";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
|
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||||
import type { RegistryEntry } from "./registry";
|
import type { RegistryEntry } from "./registry";
|
||||||
|
|
||||||
export interface LabelRegistryEntry extends RegistryEntry {
|
export interface LabelRegistryEntry extends RegistryEntry {
|
||||||
@@ -88,3 +99,178 @@ export const deleteLabelRegistryEntry = (
|
|||||||
type: "config/label_registry/delete",
|
type: "config/label_registry/delete",
|
||||||
label_id: labelId,
|
label_id: labelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getLabels = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
labels?: LabelRegistryEntry[],
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeLabels?: string[]
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
if (!labels || labels.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = Object.values(hass.devices);
|
||||||
|
const entities = Object.values(hass.entities);
|
||||||
|
|
||||||
|
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 = 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 = 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 = hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter(stateObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = 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 = hass.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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
|
||||||
|
id: label.label_id,
|
||||||
|
primary: label.name,
|
||||||
|
icon: label.icon || undefined,
|
||||||
|
icon_path: label.icon ? undefined : mdiLabel,
|
||||||
|
sorting_label: label.name,
|
||||||
|
search_labels: [label.name, label.label_id, label.description].filter(
|
||||||
|
(v): v is string => Boolean(v)
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|||||||
164
src/data/target.ts
Normal file
164
src/data/target.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import type { AreaRegistryEntry } from "./area_registry";
|
||||||
|
import type { DeviceRegistryEntry } from "./device_registry";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
|
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||||
|
|
||||||
|
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
|
||||||
|
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
|
||||||
|
|
||||||
|
export interface ExtractFromTargetResult {
|
||||||
|
missing_areas: string[];
|
||||||
|
missing_devices: string[];
|
||||||
|
missing_floors: string[];
|
||||||
|
missing_labels: string[];
|
||||||
|
referenced_areas: string[];
|
||||||
|
referenced_devices: string[];
|
||||||
|
referenced_entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtractFromTargetResultReferenced {
|
||||||
|
referenced_areas: string[];
|
||||||
|
referenced_devices: string[];
|
||||||
|
referenced_entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractFromTarget = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
target: HassServiceTarget
|
||||||
|
) =>
|
||||||
|
hass.callWS<ExtractFromTargetResult>({
|
||||||
|
type: "extract_from_target",
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const areaMeetsFilter = (
|
||||||
|
area: AreaRegistryEntry,
|
||||||
|
devices: HomeAssistant["devices"],
|
||||||
|
entities: HomeAssistant["entities"],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
includeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
states?: HomeAssistant["states"],
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc
|
||||||
|
): boolean => {
|
||||||
|
const areaDevices = Object.values(devices).filter(
|
||||||
|
(device) => device.area_id === area.area_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
areaDevices.some((device) =>
|
||||||
|
deviceMeetsFilter(
|
||||||
|
device,
|
||||||
|
entities,
|
||||||
|
deviceFilter,
|
||||||
|
includeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
states,
|
||||||
|
entityFilter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaEntities = Object.values(entities).filter(
|
||||||
|
(entity) => entity.area_id === area.area_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
areaEntities.some((entity) =>
|
||||||
|
entityRegMeetsFilter(
|
||||||
|
entity,
|
||||||
|
false,
|
||||||
|
includeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
states,
|
||||||
|
entityFilter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deviceMeetsFilter = (
|
||||||
|
device: DeviceRegistryEntry,
|
||||||
|
entities: HomeAssistant["entities"],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
includeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
states?: HomeAssistant["states"],
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc
|
||||||
|
): boolean => {
|
||||||
|
const devEntities = Object.values(entities).filter(
|
||||||
|
(entity) => entity.device_id === device.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!devEntities.some((entity) =>
|
||||||
|
entityRegMeetsFilter(
|
||||||
|
entity,
|
||||||
|
false,
|
||||||
|
includeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
states,
|
||||||
|
entityFilter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceFilter) {
|
||||||
|
return deviceFilter(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const entityRegMeetsFilter = (
|
||||||
|
entity: EntityRegistryDisplayEntry,
|
||||||
|
includeSecondary = false,
|
||||||
|
includeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
states?: HomeAssistant["states"],
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc
|
||||||
|
): boolean => {
|
||||||
|
if (entity.hidden || (entity.entity_category && !includeSecondary)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
includeDomains &&
|
||||||
|
!includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (includeDeviceClasses) {
|
||||||
|
const stateObj = states?.[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!stateObj.attributes.device_class ||
|
||||||
|
!includeDeviceClasses!.includes(stateObj.attributes.device_class)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
const stateObj = states?.[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter!(stateObj);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import type { ActionDetail } from "@material/mwc-list";
|
||||||
import {
|
import {
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiDownload,
|
mdiDownload,
|
||||||
mdiFilterRemove,
|
mdiFilterRemove,
|
||||||
mdiImagePlus,
|
mdiImagePlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { ActionDetail } from "@material/mwc-list";
|
|
||||||
import { differenceInHours } from "date-fns";
|
import { differenceInHours } from "date-fns";
|
||||||
import type {
|
import type {
|
||||||
HassServiceTarget,
|
HassServiceTarget,
|
||||||
@@ -27,21 +27,21 @@ import {
|
|||||||
import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base";
|
import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base";
|
||||||
import "../../components/chart/state-history-charts";
|
import "../../components/chart/state-history-charts";
|
||||||
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
|
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
|
||||||
import "../../components/ha-spinner";
|
import "../../components/ha-button-menu";
|
||||||
import "../../components/ha-date-range-picker";
|
import "../../components/ha-date-range-picker";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-button-menu";
|
|
||||||
import "../../components/ha-list-item";
|
|
||||||
import "../../components/ha-icon-button-arrow-prev";
|
import "../../components/ha-icon-button-arrow-prev";
|
||||||
|
import "../../components/ha-list-item";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
|
import "../../components/ha-spinner";
|
||||||
import "../../components/ha-target-picker";
|
import "../../components/ha-target-picker";
|
||||||
import "../../components/ha-top-app-bar-fixed";
|
import "../../components/ha-top-app-bar-fixed";
|
||||||
import type { HistoryResult } from "../../data/history";
|
import type { HistoryResult } from "../../data/history";
|
||||||
import {
|
import {
|
||||||
computeHistory,
|
computeHistory,
|
||||||
subscribeHistory,
|
|
||||||
mergeHistoryResults,
|
|
||||||
convertStatisticsToHistory,
|
convertStatisticsToHistory,
|
||||||
|
mergeHistoryResults,
|
||||||
|
subscribeHistory,
|
||||||
} from "../../data/history";
|
} from "../../data/history";
|
||||||
import { fetchStatistics } from "../../data/recorder";
|
import { fetchStatistics } from "../../data/recorder";
|
||||||
import { resolveEntityIDs } from "../../data/selector";
|
import { resolveEntityIDs } from "../../data/selector";
|
||||||
@@ -182,6 +182,7 @@ class HaPanelHistory extends LitElement {
|
|||||||
.disabled=${this._isLoading}
|
.disabled=${this._isLoading}
|
||||||
add-on-top
|
add-on-top
|
||||||
@value-changed=${this._targetsChanged}
|
@value-changed=${this._targetsChanged}
|
||||||
|
compact
|
||||||
></ha-target-picker>
|
></ha-target-picker>
|
||||||
</div>
|
</div>
|
||||||
${this._isLoading
|
${this._isLoading
|
||||||
@@ -649,6 +650,10 @@ class HaPanelHistory extends LitElement {
|
|||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-target-picker {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 1025px) {
|
@media all and (max-width: 1025px) {
|
||||||
.filters {
|
.filters {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { mdiRefresh } from "@mdi/js";
|
import { mdiRefresh } from "@mdi/js";
|
||||||
|
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import { storage } from "../../common/decorators/storage";
|
||||||
import { goBack, navigate } from "../../common/navigate";
|
import { goBack, navigate } from "../../common/navigate";
|
||||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||||
import {
|
import {
|
||||||
@@ -16,17 +18,15 @@ import "../../components/ha-date-range-picker";
|
|||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-icon-button-arrow-prev";
|
import "../../components/ha-icon-button-arrow-prev";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import "../../components/ha-top-app-bar-fixed";
|
|
||||||
import "../../components/ha-target-picker";
|
import "../../components/ha-target-picker";
|
||||||
|
import "../../components/ha-top-app-bar-fixed";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
import { filterLogbookCompatibleEntities } from "../../data/logbook";
|
import { filterLogbookCompatibleEntities } from "../../data/logbook";
|
||||||
|
import { resolveEntityIDs } from "../../data/selector";
|
||||||
|
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./ha-logbook";
|
import "./ha-logbook";
|
||||||
import { storage } from "../../common/decorators/storage";
|
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
|
||||||
import { resolveEntityIDs } from "../../data/selector";
|
|
||||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../components/entity/ha-entity-picker";
|
|
||||||
|
|
||||||
@customElement("ha-panel-logbook")
|
@customElement("ha-panel-logbook")
|
||||||
export class HaPanelLogbook extends LitElement {
|
export class HaPanelLogbook extends LitElement {
|
||||||
@@ -108,6 +108,7 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
.value=${this._targetPickerValue}
|
.value=${this._targetPickerValue}
|
||||||
add-on-top
|
add-on-top
|
||||||
@value-changed=${this._targetsChanged}
|
@value-changed=${this._targetsChanged}
|
||||||
|
compact
|
||||||
></ha-target-picker>
|
></ha-target-picker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -363,6 +364,10 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-target-picker {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
:host([narrow]) ha-entity-picker {
|
:host([narrow]) ha-entity-picker {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import { entityUseDeviceName } from "../../../common/entity/compute_entity_name";
|
import { entityUseDeviceName } from "../../../common/entity/compute_entity_name";
|
||||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
import "../../../components/entity/ha-entity-picker";
|
import "../../../components/entity/ha-entity-picker";
|
||||||
import type {
|
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||||
HaEntityPicker,
|
|
||||||
HaEntityPickerEntityFilterFunc,
|
|
||||||
} from "../../../components/entity/ha-entity-picker";
|
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-sortable";
|
import "../../../components/ha-sortable";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { EntityConfig } from "../entity-rows/types";
|
import type { EntityConfig } from "../entity-rows/types";
|
||||||
|
|
||||||
|
|||||||
@@ -148,10 +148,10 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
id="input"
|
id="input"
|
||||||
.placeholder=${this.hass.localize(
|
.placeholder=${this.hass.localize(
|
||||||
"ui.components.target-picker.add_entity_id"
|
"ui.components.entity.entity-picker.choose_entity"
|
||||||
)}
|
)}
|
||||||
.searchLabel=${this.hass.localize(
|
.searchLabel=${this.hass.localize(
|
||||||
"ui.components.target-picker.add_entity_id"
|
"ui.components.entity.entity-picker.choose_entity"
|
||||||
)}
|
)}
|
||||||
@value-changed=${this._entityPicked}
|
@value-changed=${this._entityPicked}
|
||||||
@click=${preventDefault}
|
@click=${preventDefault}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import type { LovelaceCardEditor } from "../../types";
|
|||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
|
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
|
||||||
import { targetStruct } from "../../../../data/script";
|
import { targetStruct } from "../../../../data/script";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../../../components/entity/ha-entity-picker";
|
|
||||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||||
|
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
|
||||||
|
|
||||||
const cardConfigStruct = assign(
|
const cardConfigStruct = assign(
|
||||||
baseLovelaceCardConfig,
|
baseLovelaceCardConfig,
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ export const waColorStyles = css`
|
|||||||
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
||||||
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
||||||
|
|
||||||
|
--wa-color-surface-default: var(--card-background-color);
|
||||||
|
--wa-panel-border-radius: var(--ha-border-radius-3xl);
|
||||||
|
--wa-panel-border-style: solid;
|
||||||
|
--wa-panel-border-width: 1px;
|
||||||
|
--wa-color-surface-border: var(--ha-color-border-neutral-quiet);
|
||||||
|
|
||||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||||
|
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const waMainStyles = css`
|
|||||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||||
--wa-transition-fast: 75ms;
|
--wa-transition-fast: 75ms;
|
||||||
--wa-transition-easing: ease;
|
--wa-transition-easing: ease;
|
||||||
--wa-border-width-l: var(--ha-border-radius-l);
|
--wa-border-width-l: var(--ha-border-radius-lg);
|
||||||
--wa-space-xl: 32px;
|
--wa-space-xl: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ContextProvider } from "@lit/context";
|
import { ContextProvider } from "@lit/context";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
areasContext,
|
areasContext,
|
||||||
configContext,
|
configContext,
|
||||||
@@ -6,6 +7,7 @@ import {
|
|||||||
devicesContext,
|
devicesContext,
|
||||||
entitiesContext,
|
entitiesContext,
|
||||||
floorsContext,
|
floorsContext,
|
||||||
|
labelsContext,
|
||||||
localeContext,
|
localeContext,
|
||||||
localizeContext,
|
localizeContext,
|
||||||
panelsContext,
|
panelsContext,
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
userContext,
|
userContext,
|
||||||
userDataContext,
|
userDataContext,
|
||||||
} from "../data/context";
|
} from "../data/context";
|
||||||
|
import { subscribeLabelRegistry } from "../data/label_registry";
|
||||||
import type { Constructor, HomeAssistant } from "../types";
|
import type { Constructor, HomeAssistant } from "../types";
|
||||||
import type { HassBaseEl } from "./hass-base-mixin";
|
import type { HassBaseEl } from "./hass-base-mixin";
|
||||||
|
|
||||||
@@ -22,6 +25,8 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
superClass: T
|
superClass: T
|
||||||
) =>
|
) =>
|
||||||
class extends superClass {
|
class extends superClass {
|
||||||
|
private _unsubscribeLabels?: UnsubscribeFunc;
|
||||||
|
|
||||||
private __contextProviders: Record<
|
private __contextProviders: Record<
|
||||||
string,
|
string,
|
||||||
ContextProvider<any> | undefined
|
ContextProvider<any> | undefined
|
||||||
@@ -92,6 +97,10 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
context: floorsContext,
|
context: floorsContext,
|
||||||
initialValue: this.hass ? this.hass.floors : this._pendingHass.floors,
|
initialValue: this.hass ? this.hass.floors : this._pendingHass.floors,
|
||||||
}),
|
}),
|
||||||
|
labels: new ContextProvider(this, {
|
||||||
|
context: labelsContext,
|
||||||
|
initialValue: [],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
protected hassConnected() {
|
protected hassConnected() {
|
||||||
@@ -101,6 +110,13 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
this.__contextProviders[key]!.setValue(value);
|
this.__contextProviders[key]!.setValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._unsubscribeLabels = subscribeLabelRegistry(
|
||||||
|
this.hass!.connection!,
|
||||||
|
(labels) => {
|
||||||
|
this.__contextProviders.labels!.setValue(labels);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateHass(obj: Partial<HomeAssistant>) {
|
protected _updateHass(obj: Partial<HomeAssistant>) {
|
||||||
@@ -111,4 +127,9 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._unsubscribeLabels?.();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -646,6 +646,7 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"entity-picker": {
|
"entity-picker": {
|
||||||
|
"choose_entity": "Choose entity",
|
||||||
"entity": "Entity",
|
"entity": "Entity",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -682,16 +683,36 @@
|
|||||||
"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 areas, devices and entities.",
|
"expand_label_id": "Split this label into separate areas, devices and entities.",
|
||||||
|
"add_target": "Add target",
|
||||||
"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",
|
"remove_label_id": "Remove label",
|
||||||
"add_area_id": "Choose area",
|
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
||||||
"add_device_id": "Choose device",
|
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
|
||||||
"add_entity_id": "Choose entity",
|
"target_details": "Target details",
|
||||||
"add_label_id": "Choose label"
|
"no_targets": "No targets",
|
||||||
|
"no_target_found": "No target found for {term}",
|
||||||
|
"selected": {
|
||||||
|
"entity": "Entities: {count}",
|
||||||
|
"device": "Devices: {count}",
|
||||||
|
"area": "Areas: {count}",
|
||||||
|
"label": "Labels: {count}",
|
||||||
|
"floor": "Floors: {count}"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"area": "Area",
|
||||||
|
"areas": "Areas",
|
||||||
|
"device": "Device",
|
||||||
|
"devices": "Devices",
|
||||||
|
"entity": "Entity",
|
||||||
|
"entities": "Entities",
|
||||||
|
"label": "Label",
|
||||||
|
"labels": "Labels",
|
||||||
|
"floor": "Floor"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"subpage-data-table": {
|
"subpage-data-table": {
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getAreaContext } from "../../../../src/common/entity/context/get_area_context";
|
import { getAreaContext } from "../../../../src/common/entity/context/get_area_context";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
|
||||||
import { mockArea, mockFloor } from "./context-mock";
|
import { mockArea, mockFloor } from "./context-mock";
|
||||||
|
|
||||||
describe("getAreaContext", () => {
|
describe("getAreaContext", () => {
|
||||||
@@ -9,14 +8,7 @@ describe("getAreaContext", () => {
|
|||||||
area_id: "area_1",
|
area_id: "area_1",
|
||||||
});
|
});
|
||||||
|
|
||||||
const hass = {
|
const result = getAreaContext(area, {});
|
||||||
areas: {
|
|
||||||
area_1: area,
|
|
||||||
},
|
|
||||||
floors: {},
|
|
||||||
} as unknown as HomeAssistant;
|
|
||||||
|
|
||||||
const result = getAreaContext(area, hass);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
area,
|
area,
|
||||||
@@ -34,16 +26,9 @@ describe("getAreaContext", () => {
|
|||||||
floor_id: "floor_1",
|
floor_id: "floor_1",
|
||||||
});
|
});
|
||||||
|
|
||||||
const hass = {
|
const result = getAreaContext(area, {
|
||||||
areas: {
|
|
||||||
area_2: area,
|
|
||||||
},
|
|
||||||
floors: {
|
|
||||||
floor_1: floor,
|
floor_1: floor,
|
||||||
},
|
});
|
||||||
} as unknown as HomeAssistant;
|
|
||||||
|
|
||||||
const result = getAreaContext(area, hass);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
area,
|
area,
|
||||||
|
|||||||
Reference in New Issue
Block a user