Compare commits

..

11 Commits

Author SHA1 Message Date
Wendelin
b149ddc3d4 Enhance target picker with filtering options for devices and entities 2025-09-16 17:31:46 +02:00
Wendelin
a2e99de828 Do not show entities count for sub entries 2025-09-16 15:51:18 +02:00
Wendelin
e3655feda0 Keep chips in history and logbook 2025-09-16 13:14:11 +02:00
Wendelin
a5be92c277 Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-16 11:08:44 +02:00
Wendelin
043849b057 Group floor and area, add label icon, remove remove group 2025-09-12 14:33:41 +02:00
Wendelin
9a69566000 Fix sublist 2025-09-12 12:02:01 +02:00
Wendelin
9cdb57476a Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-11 15:59:07 +02:00
Wendelin
f8d90d003e fix entity domain name 2025-09-11 14:36:44 +02:00
Wendelin
f53ee52b0e Fix typo 2025-09-11 12:27:05 +02:00
Wendelin
f8cc1531e5 Use extractFromTarget 2025-09-11 12:25:02 +02:00
Wendelin
11f65ef0f7 Add new target selected value view 2025-09-09 15:38:48 +02:00
39 changed files with 1659 additions and 912 deletions

View File

@@ -201,7 +201,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.0.0",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"lint-staged": "16.1.6",
"lit-analyzer": "2.0.3",

View File

@@ -1,18 +0,0 @@
/**
* Orders object properties according to a specified key order.
* Properties not in the order array will be placed at the end.
*/
export function orderProperties<T extends Record<string, any>>(
obj: T,
keys: readonly string[]
): T {
const orderedEntries = keys
.filter((key) => key in obj)
.map((key) => [key, obj[key]] as const);
const extraEntries = Object.entries(obj).filter(
([key]) => !keys.includes(key)
);
return Object.fromEntries([...orderedEntries, ...extraEntries]) as T;
}

View File

@@ -54,9 +54,9 @@ export class HaBottomSheet extends LitElement {
border-top-left-radius: var(--ha-border-radius-lg);
border-top-right-radius: var(--ha-border-radius-lg);
max-height: 90vh;
margin-bottom: var(--safe-area-inset-bottom);
margin-left: var(--safe-area-inset-left);
margin-right: var(--safe-area-inset-right);
}
wa-drawer::part(body) {
padding-bottom: var(--safe-area-inset-bottom);
}
`;
}

View File

@@ -49,6 +49,7 @@ export class HaExpansionPanel extends LitElement {
tabindex=${this.noCollapse ? -1 : 0}
aria-expanded=${this.expanded}
aria-controls="sect1"
part="summary"
>
${this.leftChevron ? chevronIcon : nothing}
<slot name="leading-icon"></slot>

View File

@@ -225,9 +225,7 @@ export class HaResizableBottomSheet extends LitElement {
top: 0;
inset-inline-start: 0;
position: fixed;
width: calc(
100% - 4px - var(--safe-area-inset-left) - var(--safe-area-inset-right)
);
width: calc(100% - 4px);
max-width: 100%;
border: none;
box-shadow: var(--wa-shadow-l);
@@ -254,9 +252,6 @@ export class HaResizableBottomSheet extends LitElement {
border-bottom-width: 0;
border-style: var(--ha-bottom-sheet-border-style);
border-color: var(--ha-bottom-sheet-border-color);
margin-bottom: var(--safe-area-inset-bottom);
margin-left: var(--safe-area-inset-left);
margin-right: var(--safe-area-inset-right);
}
dialog.show {

View File

@@ -1,39 +1,21 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import "@material/mwc-menu/mwc-menu-surface";
import {
mdiClose,
mdiDevices,
mdiHome,
mdiLabel,
mdiPlus,
mdiTextureBox,
mdiUnfoldMoreVertical,
} from "@mdi/js";
import { mdiPlus } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import type {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ensureArray } from "../common/array/ensure-array";
import { computeCssColor } from "../common/color/compute-color";
import { hex2rgb } from "../common/color/convert-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { AreaRegistryEntry } from "../data/area_registry";
import type { DeviceRegistryEntry } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import type { LabelRegistryEntry } from "../data/label_registry";
import { subscribeLabelRegistry } from "../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
} from "../data/target";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import "./device/ha-device-picker";
@@ -41,12 +23,14 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./entity/ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-floor-picker";
import { floorDefaultIconPath } from "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-label-picker";
import "./ha-svg-icon";
import "./ha-tooltip";
import "./target-picker/ha-target-picker-chips-selection";
import "./target-picker/ha-target-picker-item-group";
import type { TargetType } from "./target-picker/ha-target-picker-item-row";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@@ -58,6 +42,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public compact = false;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
@@ -96,18 +82,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@state() private _labels?: LabelRegistryEntry[];
private _opened = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels;
}),
];
}
protected render() {
if (this.addOnTop) {
return html` ${this._renderChips()} ${this._renderItems()} `;
@@ -116,89 +92,154 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
private _renderItems() {
if (
!this.value?.floor_id &&
!this.value?.area_id &&
!this.value?.device_id &&
!this.value?.entity_id &&
!this.value?.label_id
) {
return nothing;
}
return html`
<div class="mdc-chip-set items">
${this.compact
? html`<div class="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map((floor_id) => {
const floor = this.hass.floors[floor_id];
return this._renderChip(
"floor_id",
floor_id,
floor?.name || floor_id,
undefined,
floor?.icon,
floor ? floorDefaultIconPath(floor) : mdiHome
);
})
: ""}
? ensureArray(this.value.floor_id).map(
(floor_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"floor"}
.itemId=${floor_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.area_id
? ensureArray(this.value.area_id).map((area_id) => {
const area = this.hass.areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
area?.icon,
mdiTextureBox
);
})
? ensureArray(this.value.area_id).map(
(area_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"area"}
.itemId=${area_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.device_id
? ensureArray(this.value.device_id).map((device_id) => {
const device = this.hass.devices![device_id];
return this._renderChip(
"device_id",
device_id,
device
? computeDeviceNameDisplay(device, this.hass)
: device_id,
undefined,
undefined,
mdiDevices
);
})
? ensureArray(this.value.device_id).map(
(device_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"device"}
.itemId=${device_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.entity_id
? ensureArray(this.value.entity_id).map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity
);
})
? ensureArray(this.value.entity_id).map(
(entity_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"entity"}
.itemId=${entity_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.label_id
? ensureArray(this.value.label_id).map((label_id) => {
const label = this._labels?.find(
(lbl) => lbl.label_id === label_id
);
let color = label?.color
? computeCssColor(label.color)
: undefined;
if (color?.startsWith("var(")) {
const computedStyles = getComputedStyle(this);
color = computedStyles.getPropertyValue(
color.substring(4, color.length - 1)
);
}
if (color?.startsWith("#")) {
color = hex2rgb(color).join(",");
}
return this._renderChip(
"label_id",
label_id,
label ? label.name : label_id,
undefined,
label?.icon,
mdiLabel,
color
);
})
? ensureArray(this.value.label_id).map(
(label_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"label"}
.itemId=${label_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
</div>
</div>`
: html`<div class="item-groups">
${this.value?.floor_id || this.value?.area_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="area"
.hass=${this.hass}
.items=${{
floor: ensureArray(this.value?.floor_id),
area: ensureArray(this.value?.area_id),
}}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${this.value?.device_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="device"
.hass=${this.hass}
.items=${{ device: ensureArray(this.value?.device_id) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${this.value?.entity_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="entity"
.hass=${this.hass}
.items=${{ entity: ensureArray(this.value?.entity_id) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${this.value?.label_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="label"
.hass=${this.hass}
.items=${{ label: ensureArray(this.value?.label_id) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
</div>`}
`;
}
@@ -299,85 +340,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this._addMode = ev.currentTarget.type;
}
private _renderChip(
type: "floor_id" | "area_id" | "device_id" | "entity_id" | "label_id",
id: string,
name: string,
entityState?: HassEntity,
icon?: string | null,
fallbackIconPath?: string,
color?: string
) {
return html`
<div
class="mdc-chip ${classMap({
[type]: true,
})}"
style=${color
? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
: ""}
>
${icon
? html`<ha-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.icon=${icon}
></ha-icon>`
: fallbackIconPath
? html`<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${fallbackIconPath}
></ha-svg-icon>`
: ""}
${entityState
? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>`
: ""}
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text">${name}</span>
</span>
</span>
${type === "entity_id"
? ""
: html`<span role="gridcell">
<ha-tooltip .for="expand-${id}"
>${this.hass.localize(
`ui.components.target-picker.expand_${type}`
)}
</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-${id}"
.type=${type}
@click=${this._handleExpand}
></ha-icon-button>
</span>`}
<span role="gridcell">
<ha-tooltip .for="remove-${id}">
${this.hass.localize(`ui.components.target-picker.remove_${type}`)}
</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-${id}"
.type=${type}
@click=${this._handleRemove}
></ha-icon-button>
</span>
</div>
`;
}
private _renderPicker() {
if (!this._addMode) {
return nothing;
@@ -520,75 +482,114 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
});
}
private _handleRemove(ev) {
const { type, id } = ev.detail;
fireEvent(this, "value-changed", {
value: this._removeItem(this.value, type, id),
});
}
private _handleExpand(ev) {
const target = ev.currentTarget as any;
const type = ev.detail.type;
const itemId = ev.detail.id;
const newAreas: string[] = [];
const newDevices: string[] = [];
const newEntities: string[] = [];
if (target.type === "floor_id") {
if (type === "floor") {
Object.values(this.hass.areas).forEach((area) => {
if (
area.floor_id === target.id &&
area.floor_id === itemId &&
!this.value!.area_id?.includes(area.area_id) &&
this._areaMeetsFilter(area)
areaMeetsFilter(
area,
this.hass.devices,
this.hass.entities,
this.deviceFilter
)
) {
newAreas.push(area.area_id);
}
});
} else if (target.type === "area_id") {
} else if (type === "area") {
Object.values(this.hass.devices).forEach((device) => {
if (
device.area_id === target.id &&
device.area_id === itemId &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
deviceMeetsFilter(device, this.hass.entities, this.deviceFilter)
) {
newDevices.push(device.id);
}
});
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.area_id === target.id &&
entity.area_id === itemId &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
entityRegMeetsFilter(
entity,
false,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newEntities.push(entity.entity_id);
}
});
} else if (target.type === "device_id") {
} else if (type === "device") {
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.device_id === target.id &&
entity.device_id === itemId &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
entityRegMeetsFilter(
entity,
false,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newEntities.push(entity.entity_id);
}
});
} else if (target.type === "label_id") {
} else if (type === "label") {
Object.values(this.hass.areas).forEach((area) => {
if (
area.labels.includes(target.id) &&
area.labels.includes(itemId) &&
!this.value!.area_id?.includes(area.area_id) &&
this._areaMeetsFilter(area)
areaMeetsFilter(
area,
this.hass.devices,
this.hass.entities,
this.deviceFilter
)
) {
newAreas.push(area.area_id);
}
});
Object.values(this.hass.devices).forEach((device) => {
if (
device.labels.includes(target.id) &&
device.labels.includes(itemId) &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
deviceMeetsFilter(device, this.hass.entities, this.deviceFilter)
) {
newDevices.push(device.id);
}
});
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.labels.includes(target.id) &&
entity.labels.includes(itemId) &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity, true)
entityRegMeetsFilter(
entity,
true,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newEntities.push(entity.entity_id);
}
@@ -606,17 +607,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (newAreas.length) {
value = this._addItems(value, "area_id", newAreas);
}
value = this._removeItem(value, target.type, target.id);
value = this._removeItem(value, type, itemId);
fireEvent(this, "value-changed", { value });
}
private _handleRemove(ev) {
const target = ev.currentTarget as any;
fireEvent(this, "value-changed", {
value: this._removeItem(this.value, target.type, target.id),
});
}
private _addItems(
value: this["value"],
type: string,
@@ -630,20 +624,22 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _removeItem(
value: this["value"],
type: string,
type: TargetType,
id: string
): this["value"] {
const newVal = ensureArray(value![type])!.filter(
const typeId = `${type}_id`;
const newVal = ensureArray(value![typeId])!.filter(
(val) => String(val) !== id
);
if (newVal.length) {
return {
...value,
[type]: newVal,
[typeId]: newVal,
};
}
const val = { ...value }!;
delete val[type];
delete val[typeId];
if (Object.keys(val).length) {
return val;
}
@@ -675,83 +671,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
ev.preventDefault();
}
private _areaMeetsFilter(area: AreaRegistryEntry): boolean {
const areaDevices = Object.values(this.hass.devices).filter(
(device) => device.area_id === area.area_id
);
if (areaDevices.some((device) => this._deviceMeetsFilter(device))) {
return true;
}
const areaEntities = Object.values(this.hass.entities).filter(
(entity) => entity.area_id === area.area_id
);
if (areaEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
return true;
}
return false;
}
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
const devEntities = Object.values(this.hass.entities).filter(
(entity) => entity.device_id === device.id
);
if (!devEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
return false;
}
if (this.deviceFilter) {
if (!this.deviceFilter(device)) {
return false;
}
}
return true;
}
private _entityRegMeetsFilter(
entity: EntityRegistryDisplayEntry,
includeSecondary = false
): boolean {
if (entity.hidden || (entity.entity_category && !includeSecondary)) {
return false;
}
if (
this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id))
) {
return false;
}
if (this.includeDeviceClasses) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (
!stateObj.attributes.device_class ||
!this.includeDeviceClasses!.includes(stateObj.attributes.device_class)
) {
return false;
}
}
if (this.entityFilter) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (!this.entityFilter!(stateObj)) {
return false;
}
}
return true;
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(chipStyles)}
@@ -763,6 +682,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
.mdc-chip-set {
padding: 4px 0;
gap: 8px;
}
.mdc-chip-set .mdc-chip {
margin: 0;
}
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
@@ -784,7 +707,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
border-radius: 50%;
background: var(--secondary-text-color);
}
.mdc-chip__icon.mdc-chip__icon--trailing {
.mdc-chip__icon.mdc-chip__icon--2 {
width: 16px;
height: 16px;
--mdc-icon-size: 14px;
@@ -805,46 +728,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
margin-inline-end: 4px !important;
direction: var(--direction);
}
.expand-btn {
margin-right: 0;
margin-inline-end: 0;
margin-inline-start: initial;
}
.mdc-chip.area_id:not(.add),
.mdc-chip.floor_id:not(.add) {
border: 1px solid #fed6a4;
background: var(--card-background-color);
}
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.area_id.add,
.mdc-chip.floor_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.floor_id.add {
background: #fed6a4;
}
.mdc-chip.device_id:not(.add) {
border: 1px solid #a8e1fb;
background: var(--card-background-color);
}
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.device_id.add {
background: #a8e1fb;
}
.mdc-chip.entity_id:not(.add) {
border: 1px solid #d2e7b9;
background: var(--card-background-color);
}
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.entity_id.add {
background: #d2e7b9;
}
.mdc-chip.label_id:not(.add) {
border: 1px solid var(--color, #e0e0e0);
background: var(--card-background-color);
}
.mdc-chip.label_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.label_id.add {
background: var(--background-color, #e0e0e0);
}
.mdc-chip:hover {
z-index: 5;
}
@@ -861,8 +744,30 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
display: block;
width: 100%;
}
ha-tooltip {
--ha-tooltip-arrow-size: 0;
.item-groups {
overflow: hidden;
border: 2px solid var(--divider-color);
border-radius: var(--ha-border-radius-lg);
}
:host([compact]) .mdc-chip.area_id:not(.add),
.mdc-chip.floor_id:not(.add) {
border: 1px solid #fed6a4;
background: var(--card-background-color);
}
:host([compact]) .mdc-chip.area_id.add,
:host([compact]) .mdc-chip.floor_id.add {
background: #fed6a4;
}
:host([compact]) .mdc-chip.device_id.add {
background: #a8e1fb;
}
:host([compact]) .mdc-chip.entity_id.add {
background: #d2e7b9;
}
:host([compact]) .mdc-chip.label_id.add {
background: var(--background-color, #e0e0e0);
}
`;
}
@@ -872,4 +777,16 @@ declare global {
interface HTMLElementTagNameMap {
"ha-target-picker": HaTargetPicker;
}
interface HASSDomEvents {
"remove-target-item": {
type: string;
id: string;
};
"expand-target-item": {
type: string;
id: string;
};
"remove-target-group": string;
}
}

View 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 { 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";
import type { TargetType } from "./ha-target-picker-item-row";
@customElement("ha-target-picker-chips-selection")
export class HaTargetPickerChipsSelection extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ reflect: true }) 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: 16px;
height: 16px;
--mdc-icon-size: 14px;
color: var(--secondary-text-color);
margin-inline-start: 4px !important;
margin-inline-end: -4px !important;
direction: var(--direction);
}
.mdc-chip__icon--leading {
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: 20px;
border-radius: 50%;
padding: 6px;
margin-left: -13px !important;
margin-inline-start: -13px !important;
margin-inline-end: 4px !important;
direction: var(--direction);
}
.expand-btn {
margin-right: 0;
margin-inline-end: 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-chips-selection": HaTargetPickerChipsSelection;
}
}

View File

@@ -0,0 +1,107 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../entity/ha-entity-picker";
import "../ha-expansion-panel";
import "../ha-md-list";
import "./ha-target-picker-item-row";
import type { TargetType } from "./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!: "entity" | "device" | "area" | "label";
@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>
<ha-md-list>
${Object.entries(this.items).map(([type, items]) =>
items
? items.map(
(item) =>
html`<ha-target-picker-item-row
.hass=${this.hass}
.type=${type as "entity" | "device" | "area" | "label"}
.itemId=${item}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
></ha-target-picker-item-row>`
)
: nothing
)}
</ha-md-list>
</ha-expansion-panel>`;
}
static styles = css`
:host {
display: block;
--expansion-panel-content-padding: 0;
}
ha-expansion-panel::part(summary) {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: 4px 8px;
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: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-item-group": HaTargetPickerItemGroup;
}
}

View File

@@ -0,0 +1,583 @@
import { consume } from "@lit/context";
import {
mdiChevronDown,
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 { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
extractFromTarget,
type ExtractFromTargetResult,
} from "../../data/target";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../entity/ha-entity-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";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
@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, attribute: "sub-entry", reflect: true })
public subEntry = false;
@property({ type: Boolean, attribute: "hide-context" })
public hideContext = false;
@property({ attribute: false })
public parentEntries?: ExtractFromTargetResult;
@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 _expanded = false;
@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();
this._expanded = false;
}
}
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
.disabled=${entries?.referenced_entities.length === 0}
.type=${this.type === "entity" ? "text" : "button"}
@click=${this._toggleExpand}
>
${this.type !== "entity"
? html`<ha-svg-icon
class="expand-button ${entries?.referenced_entities.length &&
this._expanded
? "expanded"
: ""}"
.path=${mdiChevronDown}
slot="start"
></ha-svg-icon>`
: nothing}
${iconPath
? html`<ha-icon slot="start" .icon=${iconPath}></ha-icon>`
: this._iconImg
? html`<img
slot="start"
alt=${this._domainName || ""}
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon
slot="start"
.path=${fallbackIconPath}
></ha-svg-icon>`
: stateObject
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject}
slot="start"
>
</ha-state-icon>
`
: nothing}
<div slot="headline">${name}</div>
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
: nothing}
${!this.subEntry &&
entries &&
(showEntities || showDevices || this._domainName)
? html`
<div slot="end" class="summary">
${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.subEntry
? html`
<ha-icon-button
.path=${mdiClose}
slot="end"
@click=${this._removeItem}
></ha-icon-button>
`
: nothing}
</ha-md-list-item>
${this._expanded && entries && entries.referenced_entities
? this._renderEntries()
: nothing}
`;
}
private _renderEntries() {
const entries = this.parentEntries || this._entries;
let nextType =
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 rows1Entries =
nextType === "entity"
? undefined
: rows1.map((rowItem) => {
const nextEntries = {
missing_areas: [] as string[],
missing_devices: [] as string[],
missing_floors: [] as string[],
missing_labels: [] as string[],
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[]);
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 rows2 =
nextType === "device" && entries
? entries.referenced_entities.filter(
(entity_id) => this.hass.entities[entity_id].area_id === this.itemId
)
: [];
return html`
<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"}
></ha-target-picker-item-row>
`
)}
${rows2.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>
`;
}
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
)
) {
return true;
}
hiddenAreaIds.push(area_id);
return false;
}
);
}
const hiddenDeviceIds: string[] = [];
if (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 === "area" || device.labels.includes(this.itemId)) &&
deviceMeetsFilter(device, this.hass.entities, this.deviceFilter)
) {
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 (entries.referenced_devices.includes(entity.device_id || "")) {
return true;
}
if (
(this.type === "area" && entity.area_id === this.itemId) ||
(this.type === "label" && entity.labels.includes(this.itemId))
) {
return entityRegMeetsFilter(entity, this.type === "label");
}
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 _toggleExpand() {
const entries = this.parentEntries || this._entries;
if (
this.type === "entity" ||
!entries ||
entries.referenced_entities.length === 0
) {
return;
}
this._expanded = !this._expanded;
}
static styles = css`
:host {
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--md-list-item-two-line-container-height: 56px;
}
state-badge {
color: var(--ha-color-on-neutral-quiet);
}
img {
width: 24px;
height: 24px;
}
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.expand-button.expanded {
transform: rotate(180deg);
}
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: 48px;
}
.summary .main {
font-weight: var(--ha-font-weight-medium);
}
.summary .secondary {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
.summary .secondary.domain {
font-family: var(--ha-font-family-code);
}
.entries {
padding: 0;
padding-left: 40px;
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
:host([sub-entry]) .entries {
border-bottom: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-item-row": HaTargetPickerItemRow;
}
}

117
src/data/target.ts Normal file
View File

@@ -0,0 +1,117 @@
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 { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
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 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
): boolean => {
const areaDevices = Object.values(devices).filter(
(device) => device.area_id === area.area_id
);
if (
areaDevices.some((device) =>
deviceMeetsFilter(device, entities, deviceFilter)
)
) {
return true;
}
const areaEntities = Object.values(entities).filter(
(entity) => entity.area_id === area.area_id
);
if (areaEntities.some((entity) => entityRegMeetsFilter(entity))) {
return true;
}
return false;
};
export const deviceMeetsFilter = (
device: DeviceRegistryEntry,
entities: HomeAssistant["entities"],
deviceFilter?: HaDevicePickerDeviceFilterFunc
): boolean => {
const devEntities = Object.values(entities).filter(
(entity) => entity.device_id === device.id
);
if (!devEntities.some((entity) => entityRegMeetsFilter(entity))) {
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;
};

View File

@@ -1,10 +0,0 @@
import type { HomeAssistant } from "../types";
export interface CommonControlResult {
entities: string[];
}
export const getCommonControlUsagePrediction = (hass: HomeAssistant) =>
hass.callWS<CommonControlResult>({
type: "usage_prediction/common_control",
});

View File

@@ -304,7 +304,6 @@ export interface ExternalConfig {
hasBarCodeScanner: number;
canSetupImprov: boolean;
downloadFileSupported: boolean;
appVersion: string;
}
export class ExternalMessaging {

View File

@@ -6,7 +6,6 @@ import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-sortable";
@@ -110,7 +109,6 @@ export default class HaAutomationAction extends LitElement {
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
@click=${stopPropagation}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -6,7 +6,6 @@ import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
@@ -188,7 +187,6 @@ export default class HaAutomationCondition extends LitElement {
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
@click=${stopPropagation}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -211,9 +211,7 @@ export default class HaAutomationSidebar extends LitElement {
:host {
z-index: 6;
outline: none;
height: calc(
100% - var(--safe-area-inset-top) - var(--safe-area-inset-bottom)
);
height: 100%;
--ha-card-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
@@ -222,8 +220,6 @@ export default class HaAutomationSidebar extends LitElement {
--ha-bottom-sheet-border-width: 2px;
--ha-bottom-sheet-border-style: solid;
--ha-bottom-sheet-border-color: var(--primary-color);
margin-top: var(--safe-area-inset-top);
margin-bottom: var(--safe-area-inset-bottom);
}
@media all and (max-width: 870px) {

View File

@@ -6,7 +6,6 @@ import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-sortable";
@@ -97,7 +96,6 @@ export default class HaAutomationOption extends LitElement {
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
@click=${stopPropagation}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -145,7 +145,11 @@ export default class HaAutomationSidebarAction extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.copy}>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.copy}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(

View File

@@ -150,7 +150,11 @@ export default class HaAutomationSidebarCondition extends LitElement {
</div>
</ha-md-menu-item>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.copy}>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.copy}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(

View File

@@ -137,7 +137,11 @@ export default class HaAutomationSidebarTrigger extends LitElement {
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.copy}>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.copy}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(

View File

@@ -148,13 +148,7 @@ export const manualEditorStyles = css`
ha-automation-sidebar {
position: fixed;
top: calc(var(--header-height) + 16px);
height: calc(
-81px +
100dvh - var(--safe-area-inset-top, 0px) - var(
--safe-area-inset-bottom,
0px
)
);
height: calc(-81px + 100dvh);
width: var(--sidebar-width);
display: block;
}

View File

@@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
@@ -105,7 +104,6 @@ export default class HaAutomationTrigger extends LitElement {
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
@click=${stopPropagation}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -133,7 +133,7 @@ class HaConfigInfo extends LitElement {
<li>
<span class="version-label"
>${this.hass.localize(
"ui.panel.config.info.installation_method"
`ui.panel.config.info.installation_method`
)}</span
>
<span class="version">${this._installationMethod || "…"}</span>
@@ -170,20 +170,6 @@ class HaConfigInfo extends LitElement {
${JS_VERSION}${JS_TYPE !== "modern" ? ` · ${JS_TYPE}` : ""}
</span>
</li>
${this.hass.auth.external?.config.appVersion
? html`
<li>
<span class="version-label"
>${this.hass.localize(
"ui.panel.config.info.external_app_version"
)}</span
>
<span class="version"
>${this.hass.auth.external?.config.appVersion}</span
>
</li>
`
: nothing}
</ul>
</ha-card>
<ha-card outlined class="ohf">

View File

@@ -612,11 +612,19 @@ class HaPanelDevAction extends LitElement {
css`
.content {
padding: 16px;
padding: max(16px, var(--safe-area-inset-top))
max(16px, var(--safe-area-inset-right))
max(16px, var(--safe-area-inset-bottom))
max(16px, var(--safe-area-inset-left));
max-width: 1200px;
margin: auto;
}
.button-row {
padding: 8px 16px;
padding: max(8px, var(--safe-area-inset-top))
max(16px, var(--safe-area-inset-right))
max(8px, var(--safe-area-inset-bottom))
max(16px, var(--safe-area-inset-left));
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
background: var(--card-background-color);

View File

@@ -241,6 +241,10 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
css`
.content {
padding: 28px 20px 16px;
padding: max(28px, calc(12px + var(--safe-area-inset-top)))
max(20px, calc(4px + var(--safe-area-inset-right)))
max(16px, var(--safe-area-inset-bottom))
max(20px, calc(4px + var(--safe-area-inset-left)));
max-width: 1040px;
margin: 0 auto;
}

View File

@@ -148,6 +148,10 @@ class HaPanelDevEvent extends LitElement {
.content {
gap: 16px;
padding: 16px;
padding: max(16px, var(--safe-area-inset-top))
max(16px, var(--safe-area-inset-right))
max(16px, var(--safe-area-inset-bottom))
max(16px, var(--safe-area-inset-left));
max-width: 1200px;
margin: auto;
}

View File

@@ -20,7 +20,7 @@ class PanelDeveloperTools extends LitElement {
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public narrow = false;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
@@ -148,29 +148,15 @@ class PanelDeveloperTools extends LitElement {
top: 0;
z-index: 4;
background-color: var(--app-header-background-color);
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
width: var(--mdc-top-app-bar-width, 100%);
padding-top: var(--safe-area-inset-top);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
.toolbar {
height: var(--header-height);
display: flex;
@@ -180,9 +166,11 @@ class PanelDeveloperTools extends LitElement {
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
@media (max-width: 599px) {
.toolbar {
padding: 4px;
}
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
@@ -191,21 +179,11 @@ class PanelDeveloperTools extends LitElement {
developer-tools-router {
display: block;
padding-top: calc(
var(--header-height) + 52px + var(--safe-area-inset-top, 0px)
var(--header-height) + 48px + var(--safe-area-inset-top)
);
padding-bottom: var(--safe-area-inset-bottom);
padding-right: var(--safe-area-inset-right);
padding-bottom: calc(var(--safe-area-inset-bottom));
flex: 1 1 100%;
max-width: calc(100% - var(--safe-area-inset-right, 0px));
}
:host([narrow]) developer-tools-router {
padding-left: var(--safe-area-inset-left);
max-width: calc(
100% - var(--safe-area-inset-left, 0px) - var(
--safe-area-inset-right,
0px
)
);
max-width: 100%;
}
ha-tab-group {
--ha-tab-active-text-color: var(--app-header-text-color, white);

View File

@@ -508,6 +508,10 @@ class HaPanelDevState extends LitElement {
-moz-user-select: initial;
display: block;
padding: 16px;
padding: max(16px, var(--safe-area-inset-top))
max(16px, var(--safe-area-inset-right))
max(16px, var(--safe-area-inset-bottom))
max(16px, var(--safe-area-inset-left));
}
:host search-input {

View File

@@ -714,7 +714,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
height: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
ha-data-table {
width: 100%;

View File

@@ -276,6 +276,10 @@ ${type === "object"
.content {
gap: 16px;
padding: 16px;
padding: max(16px, var(--safe-area-inset-top))
max(16px, var(--safe-area-inset-right))
max(16px, var(--safe-area-inset-bottom))
max(16px, var(--safe-area-inset-left));
}
.content.horizontal {

View File

@@ -252,6 +252,10 @@ export class DeveloperYamlConfig extends LitElement {
.content {
padding: 28px 20px 16px;
padding: max(28px, calc(12px + var(--safe-area-inset-top)))
max(20px, calc(4px + var(--safe-area-inset-right)))
max(16px, var(--safe-area-inset-bottom))
max(20px, calc(4px + var(--safe-area-inset-left)));
max-width: 1040px;
margin: 0 auto;
}

View File

@@ -1,10 +1,11 @@
import { ContextProvider } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiDotsVertical,
mdiDownload,
mdiFilterRemove,
mdiImagePlus,
} from "@mdi/js";
import type { ActionDetail } from "@material/mwc-list";
import { differenceInHours } from "date-fns";
import type {
HassServiceTarget,
@@ -27,32 +28,35 @@ import {
import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base";
import "../../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-icon-button";
import "../../components/ha-button-menu";
import "../../components/ha-list-item";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-spinner";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import { labelsContext } from "../../data/context";
import type { HistoryResult } from "../../data/history";
import {
computeHistory,
subscribeHistory,
mergeHistoryResults,
convertStatisticsToHistory,
mergeHistoryResults,
subscribeHistory,
} from "../../data/history";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { fetchStatistics } from "../../data/recorder";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
class HaPanelHistory extends LitElement {
class HaPanelHistory extends SubscribeMixin(LitElement) {
@property({ attribute: false }) hass!: HomeAssistant;
@property({ reflect: true, type: Boolean }) public narrow = false;
@@ -89,6 +93,11 @@ class HaPanelHistory extends LitElement {
private _interval?: number;
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public constructor() {
super();
@@ -108,6 +117,14 @@ class HaPanelHistory extends LitElement {
}
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeHistory();
@@ -182,6 +199,7 @@ class HaPanelHistory extends LitElement {
.disabled=${this._isLoading}
add-on-top
@value-changed=${this._targetsChanged}
compact
></ha-target-picker>
</div>
${this._isLoading

View File

@@ -1,9 +1,15 @@
import { ContextProvider } from "@lit/context";
import { mdiRefresh } from "@mdi/js";
import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HassServiceTarget } from "home-assistant-js-websocket";
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 { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
@@ -12,24 +18,25 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import "../../components/entity/ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../components/entity/ha-entity-picker";
import "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import { labelsContext } from "../../data/context";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { filterLogbookCompatibleEntities } from "../../data/logbook";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
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")
export class HaPanelLogbook extends LitElement {
export class HaPanelLogbook extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -51,6 +58,11 @@ export class HaPanelLogbook extends LitElement {
@state() private _sensorNumericDeviceClasses?: string[] = [];
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public constructor() {
super();
@@ -63,6 +75,14 @@ export class HaPanelLogbook extends LitElement {
this._time = { range: [start, end] };
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
private _goBack(): void {
goBack();
}
@@ -108,6 +128,7 @@ export class HaPanelLogbook extends LitElement {
.value=${this._targetPickerValue}
add-on-top
@value-changed=${this._targetsChanged}
compact
></ha-target-picker>
</div>

View File

@@ -17,7 +17,6 @@ import {
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { orderProperties } from "../../../../common/util/order-properties";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -45,10 +44,10 @@ const cardConfigStruct = assign(
entity: optional(string()),
name: optional(string()),
icon: optional(string()),
color: optional(string()),
show_entity_picture: optional(boolean()),
hide_state: optional(boolean()),
state_content: optional(union([string(), array(string())])),
color: optional(string()),
show_entity_picture: optional(boolean()),
vertical: optional(boolean()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
@@ -61,8 +60,6 @@ const cardConfigStruct = assign(
})
);
export const fieldOrder = Object.keys(cardConfigStruct.schema);
@customElement("hui-tile-card-editor")
export class HuiTileCardEditor
extends LitElement
@@ -331,7 +328,7 @@ export class HuiTileCardEditor
const newConfig = ev.detail.value as TileCardConfig;
let config: TileCardConfig = {
const config: TileCardConfig = {
features: this._config.features,
...newConfig,
};
@@ -350,8 +347,6 @@ export class HuiTileCardEditor
delete config.content_layout;
}
config = orderProperties(config, fieldOrder);
fireEvent(this, "config-changed", { config });
}
@@ -393,11 +388,10 @@ export class HuiTileCardEditor
private _updateFeature(index: number, feature: LovelaceCardFeatureConfig) {
const features = this._config!.features!.concat();
features[index] = feature;
let config = { ...this._config!, features };
config = orderProperties(config, fieldOrder);
fireEvent(this, "config-changed", { config });
const config = { ...this._config!, features };
fireEvent(this, "config-changed", {
config: config,
});
}
private _computeLabelCallback = (

View File

@@ -51,10 +51,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),
},
section: {
"common-controls": () =>
import("./usage_prediction/common-controls-section-strategy"),
},
section: {},
};
export type LovelaceStrategyConfigType = "dashboard" | "view" | "section";

View File

@@ -4,11 +4,7 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { AreaRegistryEntry } from "../../../../data/area_registry";
import { getEnergyPreferences } from "../../../../data/energy";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
LovelaceStrategySectionConfig,
} from "../../../../data/lovelace/config/section";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type {
@@ -105,19 +101,6 @@ export class HomeMainViewStrategy extends ReactiveElement {
);
}
const commonControlsSection = isComponentLoaded(hass, "usage_prediction")
? ({
strategy: {
type: "common-controls",
title: hass.localize(
"ui.panel.lovelace.strategy.home.common_controls"
),
exclude_entities: favoriteEntities,
},
column_span: maxColumns,
} as LovelaceStrategySectionConfig)
: undefined;
const summarySection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
@@ -230,16 +213,12 @@ export class HomeMainViewStrategy extends ReactiveElement {
}
}
const sections = (
[
favoriteSection.cards && favoriteSection,
commonControlsSection,
const sections = [
...(favoriteSection.cards ? [favoriteSection] : []),
summarySection,
areasSection,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]
).filter(Boolean) as LovelaceSectionRawConfig[];
...(widgetSection.cards ? [widgetSection] : []),
];
return {
type: "sections",
max_columns: maxColumns,

View File

@@ -1,86 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { getCommonControlUsagePrediction } from "../../../../data/usage_prediction";
import type { HomeAssistant } from "../../../../types";
import type { TileCardConfig } from "../../cards/types";
const DEFAULT_LIMIT = 8;
export interface OriginalStatesViewStrategyConfig {
type: "common-controls";
title?: string;
limit?: number;
exclude_entities?: string[];
}
@customElement("common-controls-section-strategy")
export class CommonControlsSectionStrategy extends ReactiveElement {
static async generate(
config: OriginalStatesViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceSectionConfig> {
const section: LovelaceSectionConfig = {
type: "grid",
cards: [],
};
if (config.title) {
section.cards?.push({
type: "heading",
heading: config.title,
});
}
if (!isComponentLoaded(hass, "usage_prediction")) {
section.cards!.push({
type: "markdown",
content: hass.localize(
"ui.panel.lovelace.strategy.common_controls.not_loaded"
),
});
return section;
}
const predictedCommonControl = await getCommonControlUsagePrediction(hass);
let predictedEntities = predictedCommonControl.entities;
if (config.exclude_entities) {
predictedEntities = predictedEntities.filter(
(entity) => !config.exclude_entities!.includes(entity)
);
}
const limit = config.limit ?? DEFAULT_LIMIT;
predictedEntities = predictedEntities.slice(0, limit);
if (predictedEntities.length > 0) {
section.cards!.push(
...predictedEntities.map(
(entityId) =>
({
type: "tile",
entity: entityId,
show_entity_picture: true,
}) satisfies TileCardConfig
)
);
} else {
section.cards!.push({
type: "markdown",
content: hass.localize(
"ui.panel.lovelace.strategy.common_controls.no_data"
),
});
}
return section;
}
}
declare global {
interface HTMLElementTagNameMap {
"common-controls-section-strategy": CommonControlsSectionStrategy;
}
}

View File

@@ -661,20 +661,35 @@
},
"target-picker": {
"expand": "Expand",
"collapse": "Collapse",
"expand_floor_id": "Split this floor into separate areas.",
"expand_area_id": "Split this area into separate devices and entities.",
"expand_device_id": "Split this device into separate entities.",
"expand_label_id": "Split this label into separate areas, devices and entities.",
"remove": "Remove",
"remove_floor_id": "Remove floor",
"remove_floors": "Remove floors",
"remove_area_id": "Remove area",
"remove_areas": "Remove areas",
"remove_device_id": "Remove device",
"remove_devices": "Remove devices",
"remove_entity_id": "Remove entity",
"remove_entitys": "Remove entities",
"remove_label_id": "Remove label",
"remove_labels": "Remove labels",
"add_area_id": "Choose area",
"add_device_id": "Choose device",
"add_entity_id": "Choose entity",
"add_label_id": "Choose label"
"add_label_id": "Choose label",
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"selected": {
"entity": "Entities: {count}",
"device": "Devices: {count}",
"area": "Areas: {count}",
"label": "Labels: {count}",
"floor": "Floors: {count}"
}
},
"subpage-data-table": {
"filters": "Filters",
@@ -3261,7 +3276,6 @@
"info": {
"caption": "About",
"installation_method": "Installation method",
"external_app_version": "Companion app",
"copy_menu": "Copy menu",
"copy_raw": "Raw text",
"copy_github": "For GitHub",
@@ -6878,12 +6892,7 @@
"areas": "Areas",
"other_areas": "Other areas",
"unamed_device": "Unnamed device",
"others": "Others",
"common_controls": "Commonly used"
},
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
"no_data": "This place will soon fill up with the entities you use most often, based on your activity."
"others": "Others"
}
},
"cards": {

View File

@@ -1,167 +0,0 @@
import { describe, it, expect } from "vitest";
import { orderProperties } from "../../../src/common/util/order-properties";
describe("orderProperties", () => {
it("should order properties according to the specified order", () => {
const obj = {
c: "third",
a: "first",
b: "second",
};
const order = ["a", "b", "c"];
const result = orderProperties(obj, order);
expect(Object.keys(result)).toEqual(["a", "b", "c"]);
expect(result).toEqual({
a: "first",
b: "second",
c: "third",
});
});
it("should place properties not in order at the end", () => {
const obj = {
z: "last",
a: "first",
x: "extra",
b: "second",
};
const order = ["a", "b"];
const result = orderProperties(obj, order);
expect(Object.keys(result)).toEqual(["a", "b", "z", "x"]);
expect(result).toEqual({
a: "first",
b: "second",
z: "last",
x: "extra",
});
});
it("should handle empty objects", () => {
const obj = {};
const order = ["a", "b", "c"];
const result = orderProperties(obj, order);
expect(Object.keys(result)).toEqual([]);
expect(result).toEqual({});
});
it("should handle empty order array", () => {
const obj = {
c: "third",
a: "first",
b: "second",
};
const order: string[] = [];
const result = orderProperties(obj, order);
// Should preserve original order when no ordering is specified
expect(Object.keys(result)).toEqual(["c", "a", "b"]);
expect(result).toEqual({
c: "third",
a: "first",
b: "second",
});
});
it("should skip keys in order that don't exist in object", () => {
const obj = {
b: "second",
d: "fourth",
};
const order = ["a", "b", "c", "d"];
const result = orderProperties(obj, order);
expect(Object.keys(result)).toEqual(["b", "d"]);
expect(result).toEqual({
b: "second",
d: "fourth",
});
});
it("should preserve type information", () => {
const obj = {
num: 42,
str: "hello",
bool: true,
arr: [1, 2, 3],
obj: { nested: "value" },
};
const order = ["str", "num", "bool"];
const result = orderProperties(obj, order);
expect(result.num).toBe(42);
expect(result.str).toBe("hello");
expect(result.bool).toBe(true);
expect(result.arr).toEqual([1, 2, 3]);
expect(result.obj).toEqual({ nested: "value" });
});
it("should work with complex card config-like objects", () => {
const config = {
features: ["feature1"],
entity: "sensor.test",
vertical: false,
name: "Test Card",
icon: "mdi:test",
type: "tile",
};
const order = ["type", "entity", "name", "icon", "vertical"];
const result = orderProperties(config, order);
expect(Object.keys(result)).toEqual([
"type",
"entity",
"name",
"icon",
"vertical",
"features", // extra property at the end
]);
expect(result.type).toBe("tile");
expect(result.entity).toBe("sensor.test");
expect(result.features).toEqual(["feature1"]);
});
it("should handle readonly order arrays", () => {
const obj = { c: 3, a: 1, b: 2 };
const order = ["a", "b", "c"] as const;
const result = orderProperties(obj, order);
expect(Object.keys(result)).toEqual(["a", "b", "c"]);
expect(result).toEqual({ a: 1, b: 2, c: 3 });
});
it("should handle objects with undefined and null values", () => {
const obj = {
defined: "value",
nullValue: null,
undefinedValue: undefined,
zero: 0,
emptyString: "",
};
const order = ["nullValue", "defined", "zero"];
const result = orderProperties(obj, order);
expect(Object.keys(result)).toEqual([
"nullValue",
"defined",
"zero",
"undefinedValue",
"emptyString",
]);
expect(result.nullValue).toBeNull();
expect(result.undefinedValue).toBeUndefined();
expect(result.zero).toBe(0);
expect(result.emptyString).toBe("");
});
});

218
yarn.lock
View File

@@ -28,35 +28,16 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/css-color@npm:^4.0.3":
version: 4.0.4
resolution: "@asamuzakjp/css-color@npm:4.0.4"
"@asamuzakjp/css-color@npm:^3.2.0":
version: 3.2.0
resolution: "@asamuzakjp/css-color@npm:3.2.0"
dependencies:
"@csstools/css-calc": "npm:^2.1.4"
"@csstools/css-color-parser": "npm:^3.0.10"
"@csstools/css-parser-algorithms": "npm:^3.0.5"
"@csstools/css-tokenizer": "npm:^3.0.4"
lru-cache: "npm:^11.1.0"
checksum: 10/2c991929d135067843bd768ba6fb9de231b98fdbcc0ac86aeb881fb09c4a12f3e0ae4a55170ebe7ff67ea5b7d9c83c6672f5d57d8c75805cc054ca7e7dcc13eb
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^6.5.4":
version: 6.5.4
resolution: "@asamuzakjp/dom-selector@npm:6.5.4"
dependencies:
"@asamuzakjp/nwsapi": "npm:^2.3.9"
bidi-js: "npm:^1.0.3"
css-tree: "npm:^3.1.0"
is-potential-custom-element-name: "npm:^1.0.1"
checksum: 10/19b624ea3b6fd8565076db2451a40902a9c81668810ef01b30e44bddcdad50e90a89c384427ff70138d6b235fab38ef61a7eefd0d0a8cdfaaa86c6d0ce562604
languageName: node
linkType: hard
"@asamuzakjp/nwsapi@npm:^2.3.9":
version: 2.3.9
resolution: "@asamuzakjp/nwsapi@npm:2.3.9"
checksum: 10/95a6d1c102e1117fe818da087fcc5b914d23e0699855991bae50b891435dd1945ad7d384198f8bcf616207fd85b7ec32e3db6b96e9309d84c6903b8dc4151e34
"@csstools/css-calc": "npm:^2.1.3"
"@csstools/css-color-parser": "npm:^3.0.9"
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
lru-cache: "npm:^10.4.3"
checksum: 10/870f661460173174fef8bfebea0799ba26566f3aa7b307e5adabb7aae84fed2da68e40080104ed0c83b43c5be632ee409e65396af13bfe948a3ef4c2c729ecd9
languageName: node
linkType: hard
@@ -1296,14 +1277,14 @@ __metadata:
languageName: node
linkType: hard
"@csstools/color-helpers@npm:^5.1.0":
version: 5.1.0
resolution: "@csstools/color-helpers@npm:5.1.0"
checksum: 10/0138b3d5ccbe77aeccf6721fd008a53523c70e932f0c82dca24a1277ca780447e1d8357da47512ebf96358476f8764de57002f3e491920d67e69202f5a74c383
"@csstools/color-helpers@npm:^5.0.2":
version: 5.0.2
resolution: "@csstools/color-helpers@npm:5.0.2"
checksum: 10/8763079c54578bd2215c68de0795edb9cfa29bffa29625bff89f3c47d9df420d86296ff3a6fa8c29ca037bbaa64dc10a963461233341de0516a3161a3b549e7b
languageName: node
linkType: hard
"@csstools/css-calc@npm:^2.1.4":
"@csstools/css-calc@npm:^2.1.3, @csstools/css-calc@npm:^2.1.4":
version: 2.1.4
resolution: "@csstools/css-calc@npm:2.1.4"
peerDependencies:
@@ -1313,20 +1294,20 @@ __metadata:
languageName: node
linkType: hard
"@csstools/css-color-parser@npm:^3.0.10":
version: 3.1.0
resolution: "@csstools/css-color-parser@npm:3.1.0"
"@csstools/css-color-parser@npm:^3.0.9":
version: 3.0.10
resolution: "@csstools/css-color-parser@npm:3.0.10"
dependencies:
"@csstools/color-helpers": "npm:^5.1.0"
"@csstools/color-helpers": "npm:^5.0.2"
"@csstools/css-calc": "npm:^2.1.4"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.5
"@csstools/css-tokenizer": ^3.0.4
checksum: 10/4741095fdc4501e8e7ada4ed14fbf9dbbe6fea9b989818790ebca15657c29c62defbebacf18592cde2aa638a1d098bbe86d742d2c84ba932fbc00fac51cb8805
checksum: 10/d5619639f067c0a6ac95ecce6ad6adce55a5500599a4444817ac6bb5ed2a9928a08f0978a148d4687de7ebf05c068c1a1c7f9eaa039984830a84148e011cbc05
languageName: node
linkType: hard
"@csstools/css-parser-algorithms@npm:^3.0.5":
"@csstools/css-parser-algorithms@npm:^3.0.4":
version: 3.0.5
resolution: "@csstools/css-parser-algorithms@npm:3.0.5"
peerDependencies:
@@ -1335,16 +1316,7 @@ __metadata:
languageName: node
linkType: hard
"@csstools/css-syntax-patches-for-csstree@npm:^1.0.14":
version: 1.0.14
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.14"
peerDependencies:
postcss: ^8.4
checksum: 10/c783d5db307552f483d95266452a7765ca138a9e64f12d013c63e960c9c8abbf82c899a34028af1f5ad714e0e94edd97b1aa31784923c1d7d1756d775c3c1d0a
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^3.0.4":
"@csstools/css-tokenizer@npm:^3.0.3":
version: 3.0.4
resolution: "@csstools/css-tokenizer@npm:3.0.4"
checksum: 10/eb6c84c086312f6bb8758dfe2c85addd7475b0927333c5e39a4d59fb210b9810f8c346972046f95e60a721329cffe98895abe451e51de753ad1ca7a8c24ec65f
@@ -6215,15 +6187,6 @@ __metadata:
languageName: node
linkType: hard
"bidi-js@npm:^1.0.3":
version: 1.0.3
resolution: "bidi-js@npm:1.0.3"
dependencies:
require-from-string: "npm:^2.0.2"
checksum: 10/c4341c7a98797efe3d186cd99d6f97e9030a4f959794ca200ef2ec0a678483a916335bba6c2c0608a21d04a221288a31c9fd0faa0cd9b3903b93594b42466a6a
languageName: node
linkType: hard
"binary-extensions@npm:^2.0.0":
version: 2.3.0
resolution: "binary-extensions@npm:2.3.0"
@@ -7052,16 +7015,6 @@ __metadata:
languageName: node
linkType: hard
"css-tree@npm:^3.1.0":
version: 3.1.0
resolution: "css-tree@npm:3.1.0"
dependencies:
mdn-data: "npm:2.12.2"
source-map-js: "npm:^1.0.1"
checksum: 10/e8c5c8e98e3aa4a620fda0b813ce57ccf99281652bf9d23e5cdfc9961c9a93a6769941f9a92e31e65d90f446f42fa83879ab0185206dc7a178d9f656d0913e14
languageName: node
linkType: hard
"cssfilter@npm:0.0.10":
version: 0.0.10
resolution: "cssfilter@npm:0.0.10"
@@ -7069,14 +7022,13 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^5.3.0":
version: 5.3.0
resolution: "cssstyle@npm:5.3.0"
"cssstyle@npm:^4.2.1":
version: 4.6.0
resolution: "cssstyle@npm:4.6.0"
dependencies:
"@asamuzakjp/css-color": "npm:^4.0.3"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.0.14"
css-tree: "npm:^3.1.0"
checksum: 10/7a84aaca0ad58168b3208f8b35e4aa932ac2a040d89f5740b6fe18de9c7674fe11c8872939cd8073591e3d3de704c6f599b7c3f2b0632dbb00d3f11a2a8e7c77
"@asamuzakjp/css-color": "npm:^3.2.0"
rrweb-cssom: "npm:^0.8.0"
checksum: 10/1cb25c9d66b87adb165f978b75cdeb6f225d7e31ba30a8934666046a0be037e4e7200d359bfa79d4f1a4aef1083ea09633b81bcdb36a2f2ac888e8c73ea3a289
languageName: node
linkType: hard
@@ -7094,13 +7046,13 @@ __metadata:
languageName: node
linkType: hard
"data-urls@npm:^6.0.0":
version: 6.0.0
resolution: "data-urls@npm:6.0.0"
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
dependencies:
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^15.0.0"
checksum: 10/a47f0dde184337c4f168d455aedf0b486fed87b6ca583b4b9ad55d1515f4836b418d4bdc5b5b6fc55e321feb826029586a0d47e1c9a9e7ac4d52a78faceb7fb0
whatwg-url: "npm:^14.0.0"
checksum: 10/5c40568c31b02641a70204ff233bc4e42d33717485d074244a98661e5f2a1e80e38fe05a5755dfaf2ee549f2ab509d6a3af2a85f4b2ad2c984e5d176695eaf46
languageName: node
linkType: hard
@@ -9467,7 +9419,7 @@ __metadata:
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:10.7.16"
js-yaml: "npm:4.1.0"
jsdom: "npm:27.0.0"
jsdom: "npm:26.1.0"
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
@@ -10554,36 +10506,36 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:27.0.0":
version: 27.0.0
resolution: "jsdom@npm:27.0.0"
"jsdom@npm:26.1.0":
version: 26.1.0
resolution: "jsdom@npm:26.1.0"
dependencies:
"@asamuzakjp/dom-selector": "npm:^6.5.4"
cssstyle: "npm:^5.3.0"
data-urls: "npm:^6.0.0"
cssstyle: "npm:^4.2.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.5.0"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.6"
is-potential-custom-element-name: "npm:^1.0.1"
parse5: "npm:^7.3.0"
nwsapi: "npm:^2.2.16"
parse5: "npm:^7.2.1"
rrweb-cssom: "npm:^0.8.0"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^6.0.0"
tough-cookie: "npm:^5.1.1"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^8.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^15.0.0"
ws: "npm:^8.18.2"
whatwg-url: "npm:^14.1.1"
ws: "npm:^8.18.0"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
checksum: 10/bd20b5560a2d2528d2494500f1bb2f58c4c674f4a6deb164a9693c6a43f0a0ae0eec44ff56e6bf065022c76fb07f7a2e197e81c964fd60b4d0ce160beb4d5007
checksum: 10/39d78c4889cac20826393400dce1faed1666e9244fe0c8342a8f08c315375878e6be7fcfe339a33d6ff1a083bfe9e71b16d56ecf4d9a87db2da8c795925ea8c1
languageName: node
linkType: hard
@@ -11050,17 +11002,17 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3":
version: 10.4.3
resolution: "lru-cache@npm:10.4.3"
checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a
languageName: node
linkType: hard
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0":
version: 11.2.1
resolution: "lru-cache@npm:11.2.1"
checksum: 10/cd1cc7a8d7617ede28702f80449d9c3f851bb6671d5ec37b34ca10ffb18d5aa068d798ed057e73092f25e563202d33ab6562c356b0c1ff4a6065525dbe577b1c
"lru-cache@npm:^11.0.0":
version: 11.1.0
resolution: "lru-cache@npm:11.1.0"
checksum: 10/5011011675ca98428902de774d0963b68c3a193cd959347cb63b781dad4228924124afab82159fd7b8b4db18285d9aff462b877b8f6efd2b41604f806c1d9db4
languageName: node
linkType: hard
@@ -11167,13 +11119,6 @@ __metadata:
languageName: node
linkType: hard
"mdn-data@npm:2.12.2":
version: 2.12.2
resolution: "mdn-data@npm:2.12.2"
checksum: 10/854e41715a9358e69f9a530117cd6ca7e71d06176469de8d70b1e629753b6827f5bd730995c16ad3750f3c9bad92230f8e4e178de2b34926b05f5205d27d76af
languageName: node
linkType: hard
"media-typer@npm:0.3.0":
version: 0.3.0
resolution: "media-typer@npm:0.3.0"
@@ -11674,6 +11619,13 @@ __metadata:
languageName: node
linkType: hard
"nwsapi@npm:^2.2.16":
version: 2.2.21
resolution: "nwsapi@npm:2.2.21"
checksum: 10/3d84e7e0691640028fd7b1e93f3368cb1b5958332cecdcb31f335178177a6efdd00a07fb68b99cc476f0ca835bed5bd79b1010a16b97d33ce6c3c3c94bebd05c
languageName: node
linkType: hard
"object-assign@npm:^4":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -12047,7 +11999,7 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.1.2, parse5@npm:^7.3.0":
"parse5@npm:^7.1.2, parse5@npm:^7.2.1":
version: 7.3.0
resolution: "parse5@npm:7.3.0"
dependencies:
@@ -13530,7 +13482,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3
@@ -14265,21 +14217,21 @@ __metadata:
languageName: node
linkType: hard
"tldts-core@npm:^7.0.14":
version: 7.0.14
resolution: "tldts-core@npm:7.0.14"
checksum: 10/753b573ea972b9da2deb04df2d2fb4631e33b898cb36506bb4ae0dde272d155f92d19aba3011c296544d1548408ec93289e29ad7d57b9f0bc8de339f7b2ddc4b
"tldts-core@npm:^6.1.86":
version: 6.1.86
resolution: "tldts-core@npm:6.1.86"
checksum: 10/cb5dff9cc15661ac773a2099e98c99a5cb3cebc35909c23cc4261ff7992032c7501995ae995de3574dbbf3431e59c47496534d52f5e96abcb231f0e72144c020
languageName: node
linkType: hard
"tldts@npm:^7.0.5":
version: 7.0.14
resolution: "tldts@npm:7.0.14"
"tldts@npm:^6.1.32":
version: 6.1.86
resolution: "tldts@npm:6.1.86"
dependencies:
tldts-core: "npm:^7.0.14"
tldts-core: "npm:^6.1.86"
bin:
tldts: bin/cli.js
checksum: 10/fbee0768cc35446465c4d2e3c166a7a66b89b033f7b3fc8bfd7e1125eb691d243601ff8efae3f581606db13afd74c172d7fea6b7ce69d1a6acd3d0a1789a3c91
checksum: 10/f7e66824e44479ccdda55ea556af14ce61c4d27708be403e3f90631defde49f82a580e1ca07187cc7e3b349e257a30c2808a22903f3a0548e136ebb609ccc109
languageName: node
linkType: hard
@@ -14325,12 +14277,12 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^6.0.0":
version: 6.0.0
resolution: "tough-cookie@npm:6.0.0"
"tough-cookie@npm:^5.1.1":
version: 5.1.2
resolution: "tough-cookie@npm:5.1.2"
dependencies:
tldts: "npm:^7.0.5"
checksum: 10/1b0592241655912eb972e1c284ccf975af154576b8e9912cad4ed7b4b408a60ccfdad1bc53eef10d376f6a5ef9d84e2f8ea0b46c92263d52de855247ff100e27
tldts: "npm:^6.1.32"
checksum: 10/de430e6e6d34b794137e05b8ac2aa6b74ebbe6cdceb4126f168cf1e76101162a4b2e0e7587c3b70e728bd8654fc39958b2035be7619ee6f08e7257610ba4cd04
languageName: node
linkType: hard
@@ -14343,7 +14295,7 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^5.1.1":
"tr46@npm:^5.1.0":
version: 5.1.1
resolution: "tr46@npm:5.1.1"
dependencies:
@@ -15203,10 +15155,10 @@ __metadata:
languageName: node
linkType: hard
"webidl-conversions@npm:^8.0.0":
version: 8.0.0
resolution: "webidl-conversions@npm:8.0.0"
checksum: 10/8138d1b291c8f311d93de680653b13b04560aa35d83f9606642e746fca39d7dab9cddd9282ade21774115ea332b8b11f008106b82d4a0125e98a49479381aeee
"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
checksum: 10/4c4f65472c010eddbe648c11b977d048dd96956a625f7f8b9d64e1b30c3c1f23ea1acfd654648426ce5c743c2108a5a757c0592f02902cf7367adb7d14e67721
languageName: node
linkType: hard
@@ -15349,13 +15301,13 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:^15.0.0":
version: 15.0.0
resolution: "whatwg-url@npm:15.0.0"
"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.1":
version: 14.2.0
resolution: "whatwg-url@npm:14.2.0"
dependencies:
tr46: "npm:^5.1.1"
webidl-conversions: "npm:^8.0.0"
checksum: 10/236f88f36c17573ae604305bc63f6405a5d86f593baed8ca7814aa88226e1ffa64cc707ba3a86666ff852f4a431f220303ff63c6affcbdcf48c60394066bc7cd
tr46: "npm:^5.1.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10/f0a95b0601c64f417c471536a2d828b4c16fe37c13662483a32f02f183ed0f441616609b0663fb791e524e8cd56d9a86dd7366b1fc5356048ccb09b576495e7c
languageName: node
linkType: hard
@@ -15857,7 +15809,7 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.0, ws@npm:^8.18.2":
"ws@npm:^8.18.0":
version: 8.18.3
resolution: "ws@npm:8.18.3"
peerDependencies: