Compare commits

..

18 Commits

Author SHA1 Message Date
Wendelin
4f31761f06 Add entities 2025-09-26 16:55:44 +02:00
Wendelin
eabe7e5492 WIP target picker popover 2025-09-25 17:09:43 +02:00
Wendelin
b443ebfb0c Merge branch 'dev' of github.com:home-assistant/frontend into target-selector 2025-09-25 11:34:46 +02:00
Wendelin
32e4c23c1a Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-25 09:17:09 +02:00
Wendelin
cf97566669 Fix dialog title 2025-09-18 16:59:37 +02:00
Wendelin
c68d73b1ef Add overview dialog 2025-09-18 11:08:52 +02:00
Wendelin
89ef753309 Fix filtering 2025-09-17 15:18:49 +02:00
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
56 changed files with 3412 additions and 1548 deletions

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.10.3.cjs
yarnPath: .yarn/releases/yarn-4.10.2.cjs

View File

@@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -40,7 +40,8 @@ const playDummyMedia = (viewTitle?: string) => {
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
loadRequestData.media.streamType =
"NONE" as framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -89,7 +90,7 @@ const showMediaPlayer = () => {
const options = new framework.CastReceiverOptions();
options.disableIdleTimeout = true;
options.customNamespaces = {
[CAST_NS]: framework.system.MessageType.JSON,
[CAST_NS]: "json" as framework.system.MessageType.JSON,
};
castContext.addCustomMessageListener(
@@ -97,9 +98,7 @@ castContext.addCustomMessageListener(
// @ts-ignore
(ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
) {
if (playerManager.getPlayerState() !== "IDLE") {
playerManager.stop();
} else {
showLovelaceController();
@@ -113,7 +112,7 @@ castContext.addCustomMessageListener(
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -127,24 +126,23 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
framework.events.EventType.MEDIA_STATUS,
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.playerState === "IDLE" &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
event.mediaStatus?.idleReason !== "INTERRUPTED"
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.3",
"@codemirror/view": "6.38.2",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
@@ -111,7 +111,7 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.13",
"hls.js": "1.6.12",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
@@ -158,10 +158,10 @@
"@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.2.3",
"@rspack/core": "1.5.6",
"@rspack/core": "1.5.5",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-receiver": "6.0.24",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/culori": "4.0.1",
@@ -217,7 +217,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.2",
"typescript-eslint": "8.44.1",
"typescript-eslint": "8.44.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",
@@ -235,5 +235,5 @@
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
"packageManager": "yarn@4.10.3"
"packageManager": "yarn@4.10.2"
}

View File

@@ -1,40 +1,23 @@
import { formatHex, parse } from "culori";
/**
* Expands a 3-digit hex color to a 6-digit hex color.
* @param hex - The hex color to expand.
* @returns The expanded hex color.
* @throws If the hex color is invalid.
*/
export const expandHex = (hex: string): string => {
const color = parse(hex);
if (!color) {
throw new Error(`Invalid hex color: ${hex}`);
hex = hex.replace("#", "");
if (hex.length === 6) return hex;
let result = "";
for (const val of hex) {
result += val + val;
}
const formattedColor = formatHex(color);
if (!formattedColor) {
throw new Error(`Could not format hex color: ${hex}`);
}
return formattedColor.replace("#", "");
return result;
};
/**
* Blends two hex colors. c1 is placed over c2, blend is c1's opacity.
* @param c1 - The first hex color.
* @param c2 - The second hex color.
* @param blend - The blend percentage (0-100).
* @returns The blended hex color.
*/
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
let color = "";
c1 = expandHex(c1);
c2 = expandHex(c2);
let color = "";
for (let i = 0; i <= 5; i += 2) {
const h1 = parseInt(c1.substring(i, i + 2), 16);
const h2 = parseInt(c2.substring(i, i + 2), 16);
const hex = Math.floor(h2 + (h1 - h2) * (blend / 100))
.toString(16)
.padStart(2, "0");
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
while (hex.length < 2) hex = "0" + hex;
color += hex;
}
return `#${color}`;

View File

@@ -1,49 +1,28 @@
import { wcagLuminance, wcagContrast } from "culori";
export const luminosity = (rgb: [number, number, number]): number => {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
const lum: [number, number, number] = [0, 0, 0];
for (let i = 0; i < rgb.length; i++) {
const chan = rgb[i] / 255;
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
}
/**
* Calculates the luminosity of an RGB color.
* @param rgb - The RGB color to calculate the luminosity of.
* @returns The luminosity of the color.
*/
export const luminosity = (rgb: [number, number, number]): number =>
wcagLuminance({
mode: "rgb",
r: rgb[0] / 255,
g: rgb[1] / 255,
b: rgb[2] / 255,
});
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
};
/**
* Calculates the contrast ratio between two RGB colors.
* @param color1 - The first color to calculate the contrast ratio of.
* @param color2 - The second color to calculate the contrast ratio of.
* @returns The contrast ratio between the two colors.
*/
export const rgbContrast = (
color1: [number, number, number],
color2: [number, number, number]
) =>
wcagContrast(
{
mode: "rgb",
r: color1[0] / 255,
g: color1[1] / 255,
b: color1[2] / 255,
},
{
mode: "rgb",
r: color2[0] / 255,
g: color2[1] / 255,
b: color2[2] / 255,
}
);
) => {
const lum1 = luminosity(color1);
const lum2 = luminosity(color2);
if (lum1 > lum2) {
return (lum1 + 0.05) / (lum2 + 0.05);
}
return (lum2 + 0.05) / (lum1 + 0.05);
};
/**
* Calculates the contrast ratio between two RGB colors.
* @param rgb1 - The first color to calculate the contrast ratio of.
* @param rgb2 - The second color to calculate the contrast ratio of.
* @returns The contrast ratio between the two colors.
*/
export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]

View File

@@ -8,10 +8,10 @@ interface AreaContext {
}
export const getAreaContext = (
area: AreaRegistryEntry,
hass: HomeAssistant
hassFloors: HomeAssistant["floors"]
): AreaContext => {
const floorId = area.floor_id;
const floor = floorId ? hass.floors[floorId] : undefined;
const floor = floorId ? hassFloors[floorId] : undefined;
return {
area: area,

View File

@@ -7,7 +7,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-sortable";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
@customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement {

View File

@@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -8,8 +7,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
interface AttributeOption {
value: string;
label: string;

View File

@@ -1,14 +1,16 @@
import { mdiPlus, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { computeRTL } from "../../common/util/compute_rtl";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import {
getEntities,
type EntityComboBoxItem,
} from "../../data/entity_registry";
import { domainToName } from "../../data/integration";
import {
isHelperDomain,
@@ -19,21 +21,11 @@ import type { HomeAssistant } from "../../types";
import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type {
PickerComboBoxItem,
PickerComboBoxSearchFn,
} from "../ha-picker-combo-box";
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
import "../ha-svg-icon";
import "./state-badge";
interface EntityComboBoxItem extends PickerComboBoxItem {
domain_name?: string;
stateObj?: HassEntity;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
@customElement("ha-entity-picker")
@@ -250,7 +242,7 @@ export class HaEntityPicker extends LitElement {
);
private _getItems = () =>
this._getEntities(
getEntities(
this.hass,
this.includeDomains,
this.excludeDomains,
@@ -258,125 +250,10 @@ export class HaEntityPicker extends LitElement {
this.includeDeviceClasses,
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities
this.excludeEntities,
this.value
);
private _getEntities = memoizeOne(
(
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
const isRTL = computeRTL(this.hass);
items = entityIds.map<EntityComboBoxItem>((entityId) => {
const stateObj = hass!.states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return {
id: entityId,
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj,
};
});
if (includeDeviceClasses) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(
item.stateObj.attributes.device_class
))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj && entityFilter!(item.stateObj))
);
}
return items;
}
);
protected render() {
const placeholder =
this.placeholder ??

View File

@@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -9,8 +8,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
interface StateOption {
value: string;
label: string;

View File

@@ -1,5 +1,5 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
@@ -73,18 +73,14 @@ export class HaAnalytics extends LitElement {
.checked=${this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
?disabled=${baseEnabled}
>
</ha-switch>
${baseEnabled
? nothing
: html`<ha-tooltip
.for="switch-${preference}"
placement="right"
>
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
<ha-tooltip .for="switch-${preference}" placement="right">
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>
</span>
</ha-settings-row>
`

View File

@@ -8,21 +8,13 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../data/area_registry";
import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
} from "../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
getFloorAreaLookup,
type FloorRegistryEntry,
} from "../data/floor_registry";
getAreasAndFloors,
type AreaFloorValue,
type FloorComboBoxItem,
} from "../data/area_floor";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box-item";
@@ -30,24 +22,12 @@ import "./ha-floor-icon";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import "./ha-icon-button";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
import "./ha-tree-indicator";
const SEPARATOR = "________";
interface FloorComboBoxItem extends PickerComboBoxItem {
type: "floor" | "area";
floor?: FloorRegistryEntry;
area?: AreaRegistryEntry;
}
interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -154,243 +134,6 @@ export class HaAreaFloorPicker extends LitElement {
`;
};
private _getAreasAndFloors = memoizeOne(
(
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeAreas: this["excludeAreas"],
excludeFloors: this["excludeFloors"]
): FloorComboBoxItem[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
deviceFilter ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) =>
deviceFilter!(device)
);
}
if (entityFilter) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
});
}
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = outputAreas.filter((area) =>
areaIds!.includes(area.area_id)
);
}
if (excludeAreas) {
outputAreas = outputAreas.filter(
(area) => !excludeAreas!.includes(area.area_id)
);
}
if (excludeFloors) {
outputAreas = outputAreas.filter(
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
);
}
const floorAreaLookup = getFloorAreaLookup(outputAreas);
const unassisgnedAreas = Object.values(outputAreas).filter(
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
);
// @ts-ignore
const floorAreaEntries: [
FloorRegistryEntry | undefined,
AreaRegistryEntry[],
][] = Object.entries(floorAreaLookup)
.map(([floorId, floorAreas]) => {
const floor = floors.find((fl) => fl.floor_id === floorId)!;
return [floor, floorAreas] as const;
})
.sort(([floorA], [floorB]) => {
if (floorA.level !== floorB.level) {
return (floorA.level ?? 0) - (floorB.level ?? 0);
}
return stringCompare(floorA.name, floorB.name);
});
const items: FloorComboBoxItem[] = [];
floorAreaEntries.forEach(([floor, floorAreas]) => {
if (floor) {
const floorName = computeFloorName(floor);
const areaSearchLabels = floorAreas
.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return [area.area_id, areaName, ...area.aliases];
})
.flat();
items.push({
id: this._formatValue({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
floor: floor,
search_labels: [
floor.floor_id,
floorName,
...floor.aliases,
...areaSearchLabels,
],
});
}
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: this._formatValue({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
});
items.push(
...unassisgnedAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: this._formatValue({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
return items;
}
);
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
item,
{ index },
@@ -446,11 +189,13 @@ export class HaAreaFloorPicker extends LitElement {
};
private _getItems = () =>
this._getAreasAndFloors(
getAreasAndFloors(
this.hass.states,
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._formatValue,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,

View File

@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
);
const items: DisplayItem[] = areas.map((area) => {
const { floor } = getAreaContext(area, this.hass!);
const { floor } = getAreaContext(area, this.hass.floors);
return {
value: area.area_id,
label: area.name,

View File

@@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
);
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => {
const { floor } = getAreaContext(area, this.hass!);
const { floor } = getAreaContext(area, this.hass.floors);
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
if (!acc[floorId]) {

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

@@ -26,11 +26,6 @@ export class HaFab extends FabBase {
var(--ha-border-radius-pill)
);
}
:host .mdc-fab--extended .mdc-fab__label {
white-space: var(--ha-fab-label-white-space, nowrap);
word-break: var(--ha-fab-label-word-break, normal);
text-align: var(--ha-fab-label-text-align, center);
}
:host .mdc-fab.mdc-fab--extended .ripple {
border-radius: var(
--ha-button-border-radius,

View File

@@ -79,6 +79,7 @@ export class HaGenericPicker extends LitElement {
${!this._opened
? html`
<ha-picker-field
id="picker"
type="button"
compact
aria-label=${ifDefined(this.label)}

View File

@@ -1,10 +1,10 @@
import { Dialog } from "@material/web/dialog/internal/dialog";
import { styles } from "@material/web/dialog/internal/dialog-styles";
import {
type DialogAnimation,
DIALOG_DEFAULT_CLOSE_ANIMATION,
DIALOG_DEFAULT_OPEN_ANIMATION,
} from "@material/web/dialog/internal/animations";
import { Dialog } from "@material/web/dialog/internal/dialog";
import { styles } from "@material/web/dialog/internal/dialog-styles";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
@@ -57,6 +57,9 @@ export class HaMdDialog extends Dialog {
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
@property({ attribute: "flexcontent", type: Boolean, reflect: true })
public flexContent = false;
private _polyfillDialogRegistered = false;
constructor() {
@@ -200,6 +203,10 @@ export class HaMdDialog extends Dialog {
.scrim {
z-index: 10; /* overlay navigation */
}
:host([flexcontent]) .content {
display: flex;
}
`,
];
}

View File

@@ -33,7 +33,7 @@ export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
export const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
item
) => html`
<ha-combo-box-item type="button" compact>

View File

@@ -39,24 +39,22 @@ class HaSegmentedBar extends LitElement {
<slot name="extra"></slot>
</div>
<div class="bar">
${this.segments.map(
(segment, index) => html`
${this.hideTooltip || !segment.label
? nothing
: html`
<ha-tooltip for="segment-${index}" placement="top">
${segment.label}
</ha-tooltip>
`}
<div
id="segment-${index}"
style=${styleMap({
width: `${(segment.value / totalValue) * 100}%`,
backgroundColor: segment.color,
})}
></div>
`
)}
${this.segments.map((segment) => {
const bar = html`<div
style=${styleMap({
width: `${(segment.value / totalValue) * 100}%`,
backgroundColor: segment.color,
})}
></div>`;
return this.hideTooltip && !segment.label
? bar
: html`
<ha-tooltip>
<span slot="content">${segment.label}</span>
${bar}
</ha-tooltip>
`;
})}
</div>
${this.hideLegend
? nothing

View File

@@ -18,8 +18,6 @@ export class HaTabGroupTab extends Tab {
opacity: 0.8;
color: inherit;
--wa-space-l: 16px;
}
:host([active]:not([disabled])) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
import { ContextProvider } from "@lit/context";
import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { labelsContext } from "../../../data/context";
import { subscribeLabelRegistry } from "../../../data/label_registry";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-dialog";
import type { HaMdDialog } from "../../ha-md-dialog";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../ha-target-picker-item-row";
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
@customElement("ha-dialog-target-details")
class DialogTargetDetails
extends SubscribeMixin(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: TargetDetailsDialogParams;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public showDialog(params: TargetDetailsDialogParams): void {
this._params = params;
}
public closeDialog() {
this._dialog?.close();
return true;
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._params = undefined;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title"
>${this.hass.localize(
"ui.components.target-picker.target_details"
)}</span
>
<span slot="subtitle"
>${this.hass.localize(
`ui.components.target-picker.type.${this._params.type}`
)}:
${this._params.title}</span
>
</ha-dialog-header>
<div slot="content">
<ha-target-picker-item-row
.hass=${this.hass}
.type=${this._params.type}
.itemId=${this._params.itemId}
.deviceFilter=${this._params.deviceFilter}
.entityFilter=${this._params.entityFilter}
.includeDomains=${this._params.includeDomains}
.includeDeviceClasses=${this._params.includeDeviceClasses}
expand
></ha-target-picker-item-row>
</div>
</ha-md-dialog>
`;
}
static styles = css`
ha-md-dialog {
min-width: 400px;
max-height: 90%;
--dialog-content-padding: 8px 24px
max(var(--safe-area-inset-bottom, 0px), 32px);
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-target-details": DialogTargetDetails;
}
}

View File

@@ -0,0 +1,100 @@
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-dialog";
import type { HaMdDialog } from "../../ha-md-dialog";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../ha-target-picker-selector";
import type { TargetTypeFloorless } from "../ha-target-picker-selector";
import type { TargetPickerDialogParams } from "./show-dialog-target-picker";
@customElement("ha-dialog-target-picker")
class DialogTargetPicker extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: TargetPickerDialogParams;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog(params: TargetPickerDialogParams): void {
this._params = params;
}
public closeDialog() {
this._dialog?.close();
return true;
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._params = undefined;
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog flexcontent open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">Pick target</span>
</ha-dialog-header>
<div class="content" slot="content">
<ha-target-picker-selector
mode="dialog"
autofocus
.hass=${this.hass}
@filter-types-changed=${this._handleUpdatePickerFilters}
.filterTypes=${this._params.typeFilter || []}
></ha-target-picker-selector>
</div>
</ha-md-dialog>
`;
}
private _handleUpdatePickerFilters(ev: CustomEvent<TargetTypeFloorless[]>) {
if (this._params?.updateTypeFilter) {
this._params.updateTypeFilter(ev.detail);
}
}
static styles = css`
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
--dialog-content-padding: 8px 0 0 0;
}
.content {
display: flex;
flex-direction: column;
flex: 1;
}
ha-target-picker-selector {
flex: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-target-picker": DialogTargetPicker;
}
}

View File

@@ -0,0 +1,28 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
import type { TargetType } from "../ha-target-picker-item-row";
export type NewBackupType = "automatic" | "manual";
export interface TargetDetailsDialogParams {
title: string;
type: TargetType;
itemId: string;
deviceFilter?: HaDevicePickerDeviceFilterFunc;
entityFilter?: HaEntityPickerEntityFilterFunc;
includeDomains?: string[];
includeDeviceClasses?: string[];
}
export const loadTargetDetailsDialog = () => import("./dialog-target-details");
export const showTargetDetailsDialog = (
element: HTMLElement,
params: TargetDetailsDialogParams
) =>
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-target-details",
dialogImport: loadTargetDetailsDialog,
dialogParams: params,
});

View File

@@ -0,0 +1,28 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
import type { TargetTypeFloorless } from "../ha-target-picker-selector";
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
export interface TargetPickerDialogParams {
target: HassServiceTarget;
deviceFilter?: HaDevicePickerDeviceFilterFunc;
entityFilter?: HaEntityPickerEntityFilterFunc;
includeDomains?: string[];
includeDeviceClasses?: string[];
typeFilter?: TargetTypeFloorless[];
updateTypeFilter?: (types: TargetTypeFloorless[]) => void;
selectTarget: (target: HassServiceTarget) => void;
}
export const loadTargetPickerDialog = () => import("./dialog-target-picker");
export const showTargetPickerDialog = (
element: HTMLElement,
params: TargetPickerDialogParams
) =>
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-target-picker",
dialogImport: loadTargetPickerDialog,
dialogParams: params,
});

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 { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import "../ha-expansion-panel";
import "../ha-md-list";
import "./ha-target-picker-item-row";
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,649 @@
import { consume } from "@lit/context";
import {
mdiClose,
mdiDevices,
mdiHome,
mdiLabel,
mdiTextureBox,
} from "@mdi/js";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
extractFromTarget,
type ExtractFromTargetResult,
} from "../../data/target";
import { buttonLinkStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-icon-button";
import "../ha-md-list";
import type { HaMdList } from "../ha-md-list";
import "../ha-md-list-item";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-svg-icon";
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
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 }) public expand = false;
@property({ type: Boolean, attribute: "last" }) public lastItem = false;
@property({ type: Boolean, attribute: "sub-entry", reflect: true })
public subEntry = false;
@property({ type: Boolean, attribute: "hide-context" })
public hideContext = false;
@property({ attribute: false })
public parentEntries?: 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 _iconImg?: string;
@state() private _domainName?: string;
@state() private _entries?: ExtractFromTargetResult;
@state()
@consume({ context: labelsContext, subscribe: true })
_labelRegistry!: LabelRegistryEntry[];
@query("ha-md-list-item") public item?: HaMdListItem;
@query("ha-md-list") public list?: HaMdList;
@query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow;
protected willUpdate(changedProps: PropertyValues) {
if (!this.subEntry && changedProps.has("itemId")) {
this._updateItemData();
}
}
protected render() {
const { name, context, iconPath, fallbackIconPath, stateObject } =
this._itemData(this.type, this.itemId);
const showDevices = ["floor", "area", "label"].includes(this.type);
const showEntities = this.type !== "entity";
const entries = this.parentEntries || this._entries;
// Don't show sub entries that have no entities
if (
this.subEntry &&
this.type !== "entity" &&
(!entries || entries.referenced_entities.length === 0)
) {
return nothing;
}
return html`
<ha-md-list-item type="text">
<div slot="start">
${this.subEntry
? html`
<div class="horizontal-line-wrapper">
<div class="horizontal-line"></div>
</div>
`
: nothing}
${iconPath
? html`<ha-icon .icon=${iconPath}></ha-icon>`
: this._iconImg
? html`<img
alt=${this._domainName || ""}
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: stateObject
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject}
>
</ha-state-icon>
`
: nothing}
</div>
<div slot="headline">${name}</div>
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
: this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
>${this._domainName}</span
>`
: nothing}
${!this.subEntry &&
((entries && (showEntities || showDevices)) || this._domainName)
? html`
<div slot="end" class="summary">
${showEntities && !this.expand
? html`<button class="main link" @click=${this._openDetails}>
${this.hass.localize(
"ui.components.target-picker.entities_count",
{
count: entries?.referenced_entities.length,
}
)}
</button>`
: showEntities
? html`<span class="main">
${this.hass.localize(
"ui.components.target-picker.entities_count",
{
count: entries?.referenced_entities.length,
}
)}
</span>`
: nothing}
${showDevices
? html`<span class="secondary"
>${this.hass.localize(
"ui.components.target-picker.devices_count",
{
count: entries?.referenced_devices.length,
}
)}</span
>`
: nothing}
${this._domainName && !showDevices
? html`<span class="secondary domain"
>${this._domainName}</span
>`
: nothing}
</div>
`
: nothing}
${!this.expand && !this.subEntry
? html`
<ha-icon-button
.path=${mdiClose}
slot="end"
@click=${this._removeItem}
></ha-icon-button>
`
: nothing}
</ha-md-list-item>
${this.expand && entries && entries.referenced_entities
? this._renderEntries()
: nothing}
`;
}
private _renderEntries() {
const entries = this.parentEntries || this._entries;
let nextType: TargetType =
this.type === "floor"
? "area"
: this.type === "area"
? "device"
: "entity";
if (this.type === "label") {
if (entries?.referenced_areas.length) {
nextType = "area";
} else if (entries?.referenced_devices.length) {
nextType = "device";
}
}
const rows1 =
(nextType === "area"
? entries?.referenced_areas
: nextType === "device"
? entries?.referenced_devices
: entries?.referenced_entities) || [];
const 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 =
this.type === "label" && entries
? entries.referenced_entities.filter((entity_id) =>
this.hass.entities[entity_id].labels.includes(this.itemId)
)
: nextType === "device" && entries
? entries.referenced_entities.filter(
(entity_id) =>
this.hass.entities[entity_id].area_id === this.itemId
)
: [];
return html`
<div class="entries-tree">
<div class="line-wrapper">
<div class="line"></div>
</div>
<ha-md-list class="entries">
${rows1.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
.type=${nextType}
.itemId=${itemId}
.parentEntries=${rows1Entries?.[index]}
.hideContext=${this.hideContext || this.type !== "label"}
expand
.lastItem=${rows2.length === 0 && index === rows1.length - 1}
></ha-target-picker-item-row>
`
)}
${rows2.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
type="entity"
.itemId=${itemId}
.hideContext=${this.hideContext || this.type !== "label"}
.lastItem=${index === rows2.length - 1}
></ha-target-picker-item-row>
`
)}
</ha-md-list>
</div>
`;
}
private async _updateItemData() {
if (this.type === "entity") {
this._entries = undefined;
return;
}
try {
const entries = await extractFromTarget(this.hass, {
[`${this.type}_id`]: [this.itemId],
});
const hiddenAreaIds: string[] = [];
if (this.type === "floor" || this.type === "label") {
entries.referenced_areas = entries.referenced_areas.filter(
(area_id) => {
const area = this.hass.areas[area_id];
if (
(this.type === "floor" || area.labels.includes(this.itemId)) &&
areaMeetsFilter(
area,
this.hass.devices,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
return true;
}
hiddenAreaIds.push(area_id);
return false;
}
);
}
const hiddenDeviceIds: string[] = [];
if (
this.type === "floor" ||
this.type === "area" ||
this.type === "label"
) {
entries.referenced_devices = entries.referenced_devices.filter(
(device_id) => {
const device = this.hass.devices[device_id];
if (
!hiddenAreaIds.includes(device.area_id || "") &&
(this.type !== "label" || device.labels.includes(this.itemId)) &&
deviceMeetsFilter(
device,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
return true;
}
hiddenDeviceIds.push(device_id);
return false;
}
);
}
entries.referenced_entities = entries.referenced_entities.filter(
(entity_id) => {
const entity = this.hass.entities[entity_id];
if (hiddenDeviceIds.includes(entity.device_id || "")) {
return false;
}
if (
(this.type === "area" && entity.area_id === this.itemId) ||
(this.type === "label" && entity.labels.includes(this.itemId)) ||
entries.referenced_devices.includes(entity.device_id || "")
) {
return entityRegMeetsFilter(
entity,
this.type === "label",
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
);
}
return false;
}
);
this._entries = entries;
} catch (e) {
// eslint-disable-next-line no-console
console.error("Failed to extract target", e);
}
}
private _itemData = memoizeOne((type: TargetType, item: string) => {
if (type === "floor") {
const floor = this.hass.floors?.[item];
return {
name: floor?.name || item,
iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
};
}
if (type === "area") {
const area = this.hass.areas?.[item];
return {
name: area?.name || item,
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
iconPath: area?.icon,
fallbackIconPath: mdiTextureBox,
};
}
if (type === "device") {
const device = this.hass.devices?.[item];
if (device.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry);
}
return {
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices,
};
}
if (type === "entity") {
this._setDomainName(computeDomain(item));
const stateObject = this.hass.states[item];
const entityName = computeEntityName(
stateObject,
this.hass.entities,
this.hass.devices
);
const { area, device } = getEntityContext(
stateObject,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const context = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
return {
name: entityName || deviceName || item,
context,
stateObject,
};
}
// type label
const label = this._labelRegistry.find((lab) => lab.label_id === item);
return {
name: label?.name || item,
iconPath: label?.icon,
fallbackIconPath: mdiLabel,
};
});
private _setDomainName(domain: string) {
this._domainName = domainToName(this.hass.localize, domain);
}
private _removeItem(ev) {
ev.stopPropagation();
fireEvent(this, "remove-target-item", {
type: this.type,
id: this.itemId,
});
}
private async _getDeviceDomain(configEntryId: string) {
try {
const data = await getConfigEntry(this.hass, configEntryId);
const domain = data.config_entry.domain;
this._iconImg = brandsUrl({
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
this._setDomainName(domain);
} catch {
// failed to load config entry -> ignore
}
}
private _openDetails() {
showTargetDetailsDialog(this, {
title: this._itemData(this.type, this.itemId).name,
type: this.type,
itemId: this.itemId,
deviceFilter: this.deviceFilter,
entityFilter: this.entityFilter,
includeDomains: this.includeDomains,
includeDeviceClasses: this.includeDeviceClasses,
});
}
static styles = [
buttonLinkStyle,
css`
:host {
--md-list-item-top-space: 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;
}
:host([expand]:not([sub-entry])) ha-md-list-item {
border: 2px solid var(--ha-color-border-neutral-loud);
background-color: var(--ha-color-fill-neutral-quiet-resting);
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
state-badge {
color: var(--ha-color-on-neutral-quiet);
}
img {
width: 24px;
height: 24px;
}
ha-icon-button {
--mdc-icon-button-size: 32px;
}
.summary {
display: flex;
flex-direction: column;
align-items: flex-end;
line-height: var(--ha-line-height-condensed);
}
:host([sub-entry]) .summary {
margin-right: 48px;
}
.summary .main {
font-weight: var(--ha-font-weight-medium);
}
.summary .secondary {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
.domain {
font-family: var(--ha-font-family-code);
}
.entries-tree {
display: flex;
position: relative;
}
.entries-tree .line-wrapper {
padding: 20px;
}
.entries-tree .line-wrapper .line {
border-left: 2px dashed var(--divider-color);
height: calc(100% - 28px);
position: absolute;
top: 0;
}
:host([sub-entry]) .entries-tree .line-wrapper .line {
height: calc(100% - 12px);
top: -18px;
}
.entries {
padding: 0;
--md-item-overflow: visible;
}
.horizontal-line-wrapper {
position: relative;
}
.horizontal-line-wrapper .horizontal-line {
position: absolute;
top: 11px;
margin-inline-start: -28px;
width: 29px;
border-top: 2px dashed var(--divider-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-item-row": HaTargetPickerItemRow;
}
}

View File

@@ -0,0 +1,501 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import { mdiCheck, mdiTextureBox } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import {
getAreasAndFloors,
type AreaFloorValue,
type FloorComboBoxItem,
} from "../../data/area_floor";
import {
getEntities,
type EntityComboBoxItem,
} from "../../data/entity_registry";
import { HaFuse } from "../../resources/fuse";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "../entity/state-badge";
import "../ha-button";
import "../ha-combo-box-item";
import "../ha-floor-icon";
import "../ha-md-list";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
import "../ha-svg-icon";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import "../ha-tree-indicator";
import type { TargetType } from "./ha-target-picker-item-row";
const SEPARATOR = "________";
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
@customElement("ha-target-picker-selector")
export class HaTargetPickerSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public filterTypes: TargetTypeFloorless[] =
[];
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
@state() private _searchTerm = "";
@state() private _listScrolled = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
loadVirtualizer();
}
}
protected render() {
return html`
<ha-textfield
.label=${this.hass.localize("ui.common.search")}
@input=${this._searchChanged}
.value=${this._searchTerm}
></ha-textfield>
<div class="filter">${this._renderFilterButtons()}</div>
<lit-virtualizer
scroller
.items=${this._getItems()}
.renderItem=${this._renderRow}
@scroll=${this._onScrollList}
class=${this._listScrolled ? "scrolled" : ""}
>
</lit-virtualizer>
`;
}
private _renderFilterButtons() {
const filter: (TargetTypeFloorless | "separator")[] = [
"entity",
"device",
"area",
"separator",
"label",
];
return filter.map((filterType) => {
if (filterType === "separator") {
return html`<div class="separator"></div>`;
}
const selected = this.filterTypes.includes(filterType);
return html`
<ha-button
@click=${this._toggleFilter}
.type=${filterType}
size="small"
.variant=${selected ? "brand" : "neutral"}
appearance="filled"
>
${selected
? html`<ha-svg-icon slot="start" .path=${mdiCheck}></ha-svg-icon>`
: nothing}
${filterType.charAt(0).toUpperCase() +
filterType.slice(1)}s</ha-button
>
`;
});
}
private _renderRow = (item) => {
if (!item) {
return nothing;
}
if (typeof item === "string") {
if (item === "padding") {
return html`<div class="bottom-padding"></div>`;
}
return html`<div class="title">${item}</div>`;
}
if (item.type === "area" || item.type === "floor") {
return this._areaRowRenderer(item);
}
if ("domain" in item) {
// TODO device row
}
if ("stateObj" in item) {
return this._entityRowRenderer(item);
}
// label or empty
return html`
<ha-combo-box-item type="button" compact>
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: item.icon_path
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path}
></ha-svg-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
</ha-combo-box-item>
`;
};
private _filterAreasAndFloors(items: FloorComboBoxItem[]) {
const index = this._areaFuseIndex(items);
const fuse = new HaFuse(items, { shouldSort: false }, index);
const results = fuse.multiTermsSearch(this._searchTerm);
let filteredItems = items as FloorComboBoxItem[];
if (results) {
filteredItems = results.map((result) => result.item);
}
return filteredItems;
}
private _filterEntities(items: EntityComboBoxItem[]) {
const fuseIndex = this._entityFuseIndex(items);
const fuse = new HaFuse(items, { shouldSort: false }, fuseIndex);
const results = fuse.multiTermsSearch(this._searchTerm);
let filteredItems = items as EntityComboBoxItem[];
if (results) {
filteredItems = results.map((result) => result.item);
}
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex(
(item) => item.stateObj?.entity_id === this._searchTerm
);
if (index === -1) {
return filteredItems;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
}
private _getItems = () => {
const items: (
| string
| FloorComboBoxItem
| EntityComboBoxItem
| PickerComboBoxItem
)[] = [];
if (this.filterTypes.length === 0 || this.filterTypes.includes("entity")) {
let entities = getEntities(this.hass);
if (this._searchTerm) {
entities = this._filterEntities(entities);
}
if (entities.length > 0 && this.filterTypes.length !== 1) {
items.push(
this.hass.localize("ui.components.target-picker.type.entities")
); // title
}
items.push(...entities);
}
if (this.filterTypes.length === 0 || this.filterTypes.includes("area")) {
let areasAndFloors = getAreasAndFloors(
this.hass.states,
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
)
);
if (this._searchTerm) {
areasAndFloors = this._filterAreasAndFloors(areasAndFloors);
}
if (areasAndFloors.length > 0 && this.filterTypes.length !== 1) {
items.push(
this.hass.localize("ui.components.target-picker.type.areas")
); // title
}
items.push(
...areasAndFloors.map((item, index) => {
const nextItem = areasAndFloors[index + 1];
if (
!nextItem ||
(item.type === "area" && nextItem.type === "floor")
) {
return {
...item,
last: true,
};
}
return item;
})
);
}
if (this._searchTerm && items.length === 0) {
items.push({
id: "empty",
primary: this.hass.localize(
"ui.components.target-picker.no_target_found",
{ term: html`<span class="search-term">${this._searchTerm}</span>` }
),
});
} else if (items.length === 0) {
items.push({
id: "empty",
primary: this.hass.localize("ui.components.target-picker.no_targets"),
});
}
if (this.mode === "dialog") {
items.push("padding"); // padding for safe area inset
}
return items;
};
private _areaFuseIndex = memoizeOne((states: FloorComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _entityFuseIndex = memoizeOne((states: EntityComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _searchChanged(ev: Event) {
const textfield = ev.target as HaTextField;
const value = textfield.value.trim();
this._searchTerm = value;
}
private _areaRowRenderer = (item) => {
const rtl = computeRTL(this.hass);
const hasFloor = item.type === "area" && item.area?.floor_id;
return html`
<ha-combo-box-item
type="button"
style=${item.type === "area" && hasFloor
? "--md-list-item-leading-space: 48px;"
: ""}
>
${item.type === "area" && hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.last}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.type === "floor" && item.floor
? html`<ha-floor-icon
slot="start"
.floor=${item.floor}
></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${item.icon_path || mdiTextureBox}
></ha-svg-icon>`}
${item.primary}
</ha-combo-box-item>
`;
};
private get _showEntityId() {
return this.hass.userData?.showEntityIdPicker;
}
private _entityRowRenderer = (item) => {
const showEntityId = this._showEntityId;
return html`
<ha-combo-box-item type="button" compact>
${item.icon_path
? html`
<ha-svg-icon
slot="start"
style="margin: 0 4px"
.path=${item.icon_path}
></ha-svg-icon>
`
: html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${item.stateObj.entity_id}
</span>
`
: nothing}
${item.domain_name && !showEntityId
? html`
<div slot="trailing-supporting-text" class="domain">
${item.domain_name}
</div>
`
: nothing}
</ha-combo-box-item>
`;
};
private _toggleFilter(ev: any) {
const type = ev.target.type as TargetTypeFloorless;
if (!type) {
return;
}
const index = this.filterTypes.indexOf(type);
if (index === -1) {
this.filterTypes = [...this.filterTypes, type];
} else {
this.filterTypes = this.filterTypes.filter((t) => t !== type);
}
// Reset scroll position when filter changes
if (this._virtualizerElement) {
this._virtualizerElement.scrollTop = 0;
}
fireEvent(this, "filter-types-changed", this.filterTypes);
}
@eventOptions({ passive: true })
private _onScrollList(ev) {
const top = ev.target.scrollTop ?? 0;
this._listScrolled = top > 0;
}
static styles = [
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 12px;
flex: 1;
}
ha-textfield {
padding: 0 12px;
}
.filter {
display: flex;
gap: 8px;
flex-wrap: wrap;
padding: 0 12px;
--ha-button-border-radius: var(--ha-border-radius-md);
}
.filter .separator {
height: 32px;
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.title {
width: 100%;
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: 4px 8px;
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
}
:host([mode="dialog"]) .title {
padding: 4px 16px;
}
:host([mode="dialog"]) .filter,
:host([mode="dialog"]) ha-textfield {
padding: 0 16px;
}
ha-combo-box-item {
width: 100%;
}
lit-virtualizer {
flex: 1;
box-shadow: none;
transition: box-shadow 180ms ease-in-out;
}
lit-virtualizer.scrolled {
border-top: 1px solid var(--ha-color-border-neutral-quiet);
}
.bottom-padding {
height: max(var(--safe-area-inset-bottom, 0px), 32px);
width: 100%;
}
.search-term {
color: var(--primary-color);
font-weight: var(--ha-font-weight-medium);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-selector": HaTargetPickerSelector;
}
interface HASSDomEvents {
"filter-types-changed": TargetTypeFloorless[];
}
}

297
src/data/area_floor.ts Normal file
View File

@@ -0,0 +1,297 @@
import memoizeOne from "memoize-one";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { stringCompare } from "../common/string/compare";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
type DeviceRegistryEntry,
} from "./device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
import { getFloorAreaLookup, type FloorRegistryEntry } from "./floor_registry";
export interface FloorComboBoxItem extends PickerComboBoxItem {
type: "floor" | "area";
floor?: FloorRegistryEntry;
area?: AreaRegistryEntry;
}
export interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
memoizeOne(
(
haFloorsMemo: HomeAssistant["floors"],
haAreasMemo: HomeAssistant["areas"],
haDevicesMemo: HomeAssistant["devices"],
haEntitiesMemo: HomeAssistant["entities"],
includeDomainsMemo?: string[],
excludeDomainsMemo?: string[],
includeDeviceClassesMemo?: string[],
deviceFilterMemo?: HaDevicePickerDeviceFilterFunc,
entityFilterMemo?: HaEntityPickerEntityFilterFunc,
excludeAreasMemo?: string[],
excludeFloorsMemo?: string[]
): FloorComboBoxItem[] => {
const floors = Object.values(haFloorsMemo);
const areas = Object.values(haAreasMemo);
const devices = Object.values(haDevicesMemo);
const entities = Object.values(haEntitiesMemo);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
if (
includeDomainsMemo ||
excludeDomainsMemo ||
includeDeviceClassesMemo ||
deviceFilterMemo ||
entityFilterMemo
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
if (includeDomainsMemo) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomainsMemo.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomainsMemo.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomainsMemo) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomainsMemo.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) =>
!excludeDomainsMemo.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClassesMemo) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClassesMemo.includes(
stateObj.attributes.device_class
)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClassesMemo.includes(
stateObj.attributes.device_class
)
);
});
}
if (deviceFilterMemo) {
inputDevices = inputDevices!.filter((device) =>
deviceFilterMemo!(device)
);
}
if (entityFilterMemo) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilterMemo(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilterMemo!(stateObj);
});
}
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = outputAreas.filter((area) =>
areaIds!.includes(area.area_id)
);
}
if (excludeAreasMemo) {
outputAreas = outputAreas.filter(
(area) => !excludeAreasMemo!.includes(area.area_id)
);
}
if (excludeFloorsMemo) {
outputAreas = outputAreas.filter(
(area) =>
!area.floor_id || !excludeFloorsMemo!.includes(area.floor_id)
);
}
const floorAreaLookup = getFloorAreaLookup(outputAreas);
const unassisgnedAreas = Object.values(outputAreas).filter(
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
);
// @ts-ignore
const floorAreaEntries: [
FloorRegistryEntry | undefined,
AreaRegistryEntry[],
][] = Object.entries(floorAreaLookup)
.map(([floorId, floorAreas]) => {
const floor = floors.find((fl) => fl.floor_id === floorId)!;
return [floor, floorAreas] as const;
})
.sort(([floorA], [floorB]) => {
if (floorA.level !== floorB.level) {
return (floorA.level ?? 0) - (floorB.level ?? 0);
}
return stringCompare(floorA.name, floorB.name);
});
const items: FloorComboBoxItem[] = [];
floorAreaEntries.forEach(([floor, floorAreas]) => {
if (floor) {
const floorName = computeFloorName(floor);
const areaSearchLabels = floorAreas
.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return [area.area_id, areaName, ...area.aliases];
})
.flat();
items.push({
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
floor: floor,
search_labels: [
floor.floor_id,
floorName,
...floor.aliases,
...areaSearchLabels,
],
});
}
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
});
items.push(
...unassisgnedAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
return items;
}
)(
haFloors,
haAreas,
haDevices,
haEntities,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors
);

View File

@@ -1,3 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { arrayLiteralIncludes } from "../common/array/literal-includes";
export const UNAVAILABLE = "unavailable";
@@ -10,3 +11,5 @@ export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
export const isOffState = arrayLiteralIncludes(OFF_STATES);
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;

View File

@@ -1,12 +1,16 @@
import type { Connection } from "home-assistant-js-websocket";
import type { Connection, HassEntity } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import memoizeOne from "memoize-one";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { debounce } from "../common/util/debounce";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import { domainToName } from "./integration";
import type { LightColor } from "./light";
import type { RegistryEntry } from "./registry";
@@ -324,3 +328,141 @@ export const getAutomaticEntityIds = (
type: "config/entity_registry/get_automatic_entity_ids",
entity_ids,
});
export interface EntityComboBoxItem extends PickerComboBoxItem {
domain_name?: string;
stateObj?: HassEntity;
}
export const getEntities = (
hass: HomeAssistant,
includeDomains?: string[],
excludeDomains?: string[],
entityFilter?: HaEntityPickerEntityFilterFunc,
includeDeviceClasses?: string[],
includeUnitOfMeasurement?: string[],
includeEntities?: string[],
excludeEntities?: string[],
value?: string
): EntityComboBoxItem[] =>
memoizeOne(
(
states: HomeAssistant["states"],
isRTLMemo: boolean,
includeDomainsMemo?: string[],
excludeDomainsMemo?: string[],
entityFilterMemo?: HaEntityPickerEntityFilterFunc,
includeDeviceClassesMemo?: string[],
includeUnitOfMeasurementMemo?: string[],
includeEntitiesMemo?: string[],
excludeEntitiesMemo?: string[]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(states);
if (includeEntitiesMemo) {
entityIds = entityIds.filter((entityId) =>
includeEntitiesMemo.includes(entityId)
);
}
if (excludeEntitiesMemo) {
entityIds = entityIds.filter(
(entityId) => !excludeEntitiesMemo.includes(entityId)
);
}
if (includeDomainsMemo) {
entityIds = entityIds.filter((eid) =>
includeDomainsMemo.includes(computeDomain(eid))
);
}
if (excludeDomainsMemo) {
entityIds = entityIds.filter(
(eid) => !excludeDomainsMemo.includes(computeDomain(eid))
);
}
items = entityIds.map<EntityComboBoxItem>((entityId) => {
const stateObj = states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = hass.formatEntityName(stateObj, "entity");
const deviceName = hass.formatEntityName(stateObj, "device");
const areaName = hass.formatEntityName(stateObj, "area");
const domainName = domainToName(hass.localize, computeDomain(entityId));
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTLMemo ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return {
id: entityId,
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj,
};
});
if (includeDeviceClassesMemo) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClassesMemo.includes(
item.stateObj.attributes.device_class
))
);
}
if (includeUnitOfMeasurementMemo) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurementMemo.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilterMemo) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === value ||
(item.stateObj && entityFilterMemo!(item.stateObj))
);
}
return items;
}
)(
hass.states,
computeRTL(hass),
includeDomains,
excludeDomains,
entityFilter,
includeDeviceClasses,
includeUnitOfMeasurement,
includeEntities,
excludeEntities
);

View File

@@ -4,7 +4,6 @@ export interface LovelaceBadgeConfig {
type: string;
[key: string]: any;
visibility?: Condition[];
disabled?: boolean;
}
export const ensureBadgeConfig = (

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

@@ -0,0 +1,155 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
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,
includeDomains?: string[],
includeDeviceClasses?: string[],
states?: HomeAssistant["states"],
entityFilter?: HaEntityPickerEntityFilterFunc
): boolean => {
const areaDevices = Object.values(devices).filter(
(device) => device.area_id === area.area_id
);
if (
areaDevices.some((device) =>
deviceMeetsFilter(
device,
entities,
deviceFilter,
includeDomains,
includeDeviceClasses,
states,
entityFilter
)
)
) {
return true;
}
const areaEntities = Object.values(entities).filter(
(entity) => entity.area_id === area.area_id
);
if (
areaEntities.some((entity) =>
entityRegMeetsFilter(
entity,
false,
includeDomains,
includeDeviceClasses,
states,
entityFilter
)
)
) {
return true;
}
return false;
};
export const deviceMeetsFilter = (
device: DeviceRegistryEntry,
entities: HomeAssistant["entities"],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
includeDomains?: string[],
includeDeviceClasses?: string[],
states?: HomeAssistant["states"],
entityFilter?: HaEntityPickerEntityFilterFunc
): boolean => {
const devEntities = Object.values(entities).filter(
(entity) => entity.device_id === device.id
);
if (
!devEntities.some((entity) =>
entityRegMeetsFilter(
entity,
false,
includeDomains,
includeDeviceClasses,
states,
entityFilter
)
)
) {
return false;
}
if (deviceFilter) {
return deviceFilter(device);
}
return true;
};
export const entityRegMeetsFilter = (
entity: EntityRegistryDisplayEntry,
includeSecondary = false,
includeDomains?: string[],
includeDeviceClasses?: string[],
states?: HomeAssistant["states"],
entityFilter?: HaEntityPickerEntityFilterFunc
): boolean => {
if (entity.hidden || (entity.entity_category && !includeSecondary)) {
return false;
}
if (
includeDomains &&
!includeDomains.includes(computeDomain(entity.entity_id))
) {
return false;
}
if (includeDeviceClasses) {
const stateObj = states?.[entity.entity_id];
if (!stateObj) {
return false;
}
if (
!stateObj.attributes.device_class ||
!includeDeviceClasses!.includes(stateObj.attributes.device_class)
) {
return false;
}
}
if (entityFilter) {
const stateObj = states?.[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
}
return true;
};

View File

@@ -48,10 +48,10 @@ class MoreInfoMediaPlayer extends LitElement {
@property({ attribute: false }) public stateObj?: MediaPlayerEntity;
private _formatDuration(duration: number) {
private _formateDuration(duration: number) {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
const seconds = duration % 60;
return formatDurationDigital(this.hass.locale, {
hours,
minutes,
@@ -260,12 +260,12 @@ class MoreInfoMediaPlayer extends LitElement {
const controls = computeMediaControls(stateObj, true);
const coverUrl = stateObj.attributes.entity_picture || "";
const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj);
const position = Math.max(Math.floor(playerObj.currentProgress || 0), 0);
const duration = Math.max(stateObj.attributes.media_duration || 0, 0);
const remaining = Math.max(duration - position, 0);
const remainingFormatted = this._formatDuration(remaining);
const positionFormatted = this._formatDuration(position);
const position = Math.floor(playerObj.currentProgress) || 0;
const duration = stateObj.attributes.media_duration || 0;
const remaining = duration - position;
const durationFormated =
remaining > 0 ? this._formateDuration(remaining) : 0;
const postionFormated = this._formateDuration(position);
const primaryTitle = playerObj.primaryTitle;
const secondaryTitle = playerObj.secondaryTitle;
const turnOn = controls?.find((c) => c.action === "turn_on");
@@ -323,10 +323,11 @@ class MoreInfoMediaPlayer extends LitElement {
@change=${this._handleMediaSeekChanged}
?disabled=${!stateActive(stateObj) ||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
>
<span slot="reference">${positionFormatted}</span>
<span slot="reference">${remainingFormatted}</span>
</ha-slider>
></ha-slider>
<div class="position-info-row">
<span class="position-time">${postionFormated}</span>
<span class="duration-time">${durationFormated}</span>
</div>
</div>
`
: nothing}
@@ -547,8 +548,13 @@ class MoreInfoMediaPlayer extends LitElement {
flex-direction: column;
}
.position-bar ha-slider::part(references) {
.position-info-row {
display: flex;
flex-direction: row;
justify-content: space-between;
color: var(--secondary-text-color);
padding: 0 8px;
font-size: var(--ha-font-size-s);
}
.media-info-row {

View File

@@ -8,7 +8,6 @@ import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea";
import type { HaTextArea } from "../../components/ha-textarea";
import { convertTextToSpeech } from "../../data/tts";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import type { TTSTryDialogParams } from "./show-dialog-tts-try";
@@ -150,24 +149,21 @@ export class TTSTryDialog extends LitElement {
});
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-textarea,
ha-select {
width: 100%;
}
ha-select {
margin-top: 8px;
}
.loading {
height: 36px;
}
`,
];
static styles = css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-textarea,
ha-select {
width: 100%;
}
ha-select {
margin-top: 8px;
}
.loading {
height: 36px;
}
`;
}
declare global {

View File

@@ -213,7 +213,6 @@ class HaConfigEnergy extends LitElement {
this.hass.states[key],
])
),
issues: this._validationResult,
};
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: "application/json" });

View File

@@ -143,9 +143,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
slot="fab"
@click=${this._importExternalThreadCredentials}
extended
.label=${this.hass.localize(
"ui.panel.config.thread.thread_network_send_credentials_ha"
)}
label="Send credentials to Home Assistant"
><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon
></ha-fab>`
: nothing}
@@ -312,9 +310,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
<ha-button
.datasetId=${network.dataset.dataset_id}
@click=${this._setPreferred}
>${this.hass.localize(
"ui.panel.config.thread.thread_network_make_preferred"
)}</ha-button
>Make preferred network</ha-button
>
</div>`
: ""}
@@ -326,9 +322,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
size="small"
.networkDataset=${network.dataset}
@click=${this._sendCredentials}
>${this.hass.localize(
"ui.panel.config.thread.thread_network_send_credentials_phone"
)}</ha-button
>Send credentials to phone</ha-button
>
</div>`
: ""}
@@ -742,12 +736,6 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
display: flex;
justify-content: space-between;
}
ha-fab {
--ha-fab-label-white-space: normal;
--ha-fab-label-word-break: break-word;
--ha-fab-label-text-align: left;
--mdc-icon-size: 24px;
}
`,
];
}

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 {
@@ -16,20 +22,21 @@ 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";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
@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

@@ -161,7 +161,7 @@ export class HuiBadge extends ReactiveElement {
);
}
private _updateVisibility(ignoreConditions?: boolean) {
private _updateVisibility(forceVisible?: boolean) {
if (!this._element || !this.hass) {
return;
}
@@ -171,18 +171,9 @@ export class HuiBadge extends ReactiveElement {
return;
}
if (this.preview) {
this._setElementVisibility(true);
return;
}
if (this.config?.disabled) {
this._setElementVisibility(false);
return;
}
const visible =
ignoreConditions ||
forceVisible ||
this.preview ||
!this.config?.visibility ||
checkConditionsMet(this.config.visibility, this.hass);
this._setElementVisibility(visible);

View File

@@ -1,32 +1,30 @@
import { mdiWaterBoiler } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-select-menu";
import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item";
import "../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../data/entity";
import type {
OperationMode,
WaterHeaterEntity,
} from "../../../data/water_heater";
import {
computeOperationModeIcon,
compareWaterHeaterOperationMode,
computeOperationModeIcon,
} from "../../../data/water_heater";
import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes";
import type {
WaterHeaterOperationModesCardFeatureConfig,
LovelaceCardFeatureContext,
WaterHeaterOperationModesCardFeatureConfig,
} from "./types";
export const supportsWaterHeaterOperationModesCardFeature = (
@@ -54,9 +52,6 @@ class HuiWaterHeaterOperationModeCardFeature
@state() _currentOperationMode?: OperationMode;
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -102,23 +97,8 @@ class HuiWaterHeaterOperationModeCardFeature
}
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (this._haSelect && changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this.hass &&
this.hass.formatEntityAttributeValue !==
oldHass?.formatEntityAttributeValue
) {
this._haSelect.layoutOptions();
}
}
}
private async _valueChanged(ev: CustomEvent) {
const mode =
(ev.detail as any).value ?? ((ev.target as any).value as OperationMode);
const mode = (ev.detail as any).value as OperationMode;
if (mode === this._stateObj!.state) return;
@@ -163,48 +143,9 @@ class HuiWaterHeaterOperationModeCardFeature
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this._stateObj!, mode),
icon: html`
<ha-svg-icon
slot="graphic"
.path=${computeOperationModeIcon(mode as OperationMode)}
></ha-svg-icon>
`,
path: computeOperationModeIcon(mode as OperationMode),
}));
if (this._config.style === "dropdown") {
return html`
<ha-control-select-menu
show-arrow
hide-label
.label=${this.hass.localize("ui.card.water_heater.mode")}
.value=${this._currentOperationMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._valueChanged}
@closed=${stopPropagation}
>
${this._currentOperationMode
? html`
<ha-svg-icon
slot="icon"
.path=${computeOperationModeIcon(this._currentOperationMode)}
></ha-svg-icon>
`
: html`
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
`}
${options.map(
(option) => html`
<ha-list-item .value=${option.value} graphic="icon">
${option.icon}${option.label}
</ha-list-item>
`
)}
</ha-control-select-menu>
`;
}
return html`
<ha-control-select
.options=${options}

View File

@@ -140,7 +140,6 @@ export interface ToggleCardFeatureConfig {
export interface WaterHeaterOperationModesCardFeatureConfig {
type: "water-heater-operation-modes";
style?: "dropdown" | "icons";
operation_modes?: OperationMode[];
}

View File

@@ -4,14 +4,12 @@ import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker";
import type {
HaEntityPicker,
HaEntityPickerEntityFilterFunc,
} from "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-icon-button";
import "../../../components/ha-sortable";
import type { HomeAssistant } from "../../../types";
import type { EntityConfig } from "../entity-rows/types";
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
@customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement {

View File

@@ -22,8 +22,8 @@ import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
import { targetStruct } from "../../../../data/script";
import type { HaEntityPickerEntityFilterFunc } from "../../../../components/entity/ha-entity-picker";
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
const cardConfigStruct = assign(
baseLovelaceCardConfig,

View File

@@ -16,7 +16,6 @@ import type {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
import { compareWaterHeaterOperationMode } from "../../../../data/water_heater";
import type { LocalizeFunc } from "../../../../common/translations/localize";
type WaterHeaterOperationModesCardFeatureData =
WaterHeaterOperationModesCardFeatureConfig & {
@@ -40,27 +39,11 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
private _schema = memoizeOne(
(
localize: LocalizeFunc,
formatEntityState: FormatEntityStateFunc,
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
name: "style",
selector: {
select: {
multiple: false,
mode: "list",
options: ["dropdown", "icons"].map((mode) => ({
value: mode,
label: localize(
`ui.panel.lovelace.editor.features.types.water-heater-operation-modes.style_list.${mode}`
),
})),
},
},
},
{
name: "customize_modes",
selector: {
@@ -102,13 +85,11 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
: undefined;
const data: WaterHeaterOperationModesCardFeatureData = {
style: "icons",
...this._config,
customize_modes: this._config.operation_modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityState,
stateObj,
data.customize_modes
@@ -150,7 +131,6 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
) => {
switch (schema.name) {
case "operation_modes":
case "style":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.water-heater-operation-modes.${schema.name}`

View File

@@ -1,7 +1,6 @@
import { object, string, any, optional, boolean } from "superstruct";
import { object, string, any } from "superstruct";
export const baseLovelaceBadgeConfig = object({
type: string(),
visibility: any(),
disabled: optional(boolean()),
});

View File

@@ -1,4 +1,4 @@
import { object, string, any, optional, boolean } from "superstruct";
import { object, string, any } from "superstruct";
export const baseLovelaceCardConfig = object({
type: string(),
@@ -6,5 +6,4 @@ export const baseLovelaceCardConfig = object({
layout_options: any(),
grid_options: any(),
visibility: any(),
disabled: optional(boolean()),
});

View File

@@ -4,14 +4,13 @@ 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 { HeadingCardConfig, TileCardConfig } from "../../cards/types";
import type { TileCardConfig } from "../../cards/types";
const DEFAULT_LIMIT = 8;
export interface CommonControlSectionStrategyConfig {
type: "common-controls";
title?: string;
icon?: string;
limit?: number;
exclude_entities?: string[];
hide_empty?: boolean;
@@ -32,8 +31,7 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
section.cards?.push({
type: "heading",
heading: config.title,
icon: config.icon,
} satisfies HeadingCardConfig);
});
}
if (!isComponentLoaded(hass, "usage_prediction")) {

View File

@@ -52,6 +52,13 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
--wa-color-surface-default: var(--white-color);
--wa-panel-border-radius: var(--ha-border-radius-3xl);
--wa-panel-border-style: solid;
--wa-panel-border-width: 1px;
--wa-color-surface-border: var(--ha-color-border-neutral-quiet);
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: box-shadow: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
}
`;

View File

@@ -663,20 +663,50 @@
},
"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.",
"add_target": "Add target",
"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}",
"target_details": "Target details",
"no_targets": "No targets",
"no_target_found": "No target found for term {term}",
"selected": {
"entity": "Entities: {count}",
"device": "Devices: {count}",
"area": "Areas: {count}",
"label": "Labels: {count}",
"floor": "Floors: {count}"
},
"type": {
"area": "Area",
"areas": "Areas",
"device": "Device",
"devices": "Devices",
"entity": "Entity",
"entities": "Entities",
"label": "Label",
"labels": "Labels",
"floor": "Floor"
}
},
"subpage-data-table": {
"filters": "Filters",
@@ -5836,10 +5866,7 @@
"change_channel_range": "Channel must be in the range 11 to 26",
"change_channel_text": "Initiating a channel change for your Home Assistant Thread network should be performed with caution. Some Thread devices may not migrate to the new channel automatically and, if the new channel is congested, your Thread devices may become intermittently unavailable. Some devices may need to be manually re-joined to your Thread network before they show in Home Assistant again. This action cannot be reversed (without performing another channel change).",
"thread_network_info": "Thread network information",
"thread_network_delete_credentials": "Delete Thread network credentials",
"thread_network_send_credentials_ha": "Send credentials to Home Assistant",
"thread_network_send_credentials_phone": "Send credentials to phone",
"thread_network_make_preferred": "Make preferred network"
"thread_network_delete_credentials": "Delete Thread network credentials"
},
"ssdp": {
"name": "Name",
@@ -8220,12 +8247,7 @@
"water-heater-operation-modes": {
"label": "Water heater operation modes",
"operation_modes": "Operation modes",
"customize_modes": "Customize operation modes",
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",
"style_list": {
"dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]",
"icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]"
}
"customize_modes": "Customize operation modes"
},
"lawn-mower-commands": {
"label": "Lawn mower commands",

View File

@@ -1,6 +1,5 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import { getAreaContext } from "../../../../src/common/entity/context/get_area_context";
import type { HomeAssistant } from "../../../../src/types";
import { mockArea, mockFloor } from "./context-mock";
describe("getAreaContext", () => {
@@ -9,14 +8,7 @@ describe("getAreaContext", () => {
area_id: "area_1",
});
const hass = {
areas: {
area_1: area,
},
floors: {},
} as unknown as HomeAssistant;
const result = getAreaContext(area, hass);
const result = getAreaContext(area, {});
expect(result).toEqual({
area,
@@ -34,16 +26,9 @@ describe("getAreaContext", () => {
floor_id: "floor_1",
});
const hass = {
areas: {
area_2: area,
},
floors: {
floor_1: floor,
},
} as unknown as HomeAssistant;
const result = getAreaContext(area, hass);
const result = getAreaContext(area, {
floor_1: floor,
});
expect(result).toEqual({
area,

276
yarn.lock
View File

@@ -1284,15 +1284,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.38.3, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.3
resolution: "@codemirror/view@npm:6.38.3"
"@codemirror/view@npm:6.38.2, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.2
resolution: "@codemirror/view@npm:6.38.2"
dependencies:
"@codemirror/state": "npm:^6.5.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/2df41450399cbac0eaf06dba822418dd6926e48344b9255902248075ef040c957dfe97fe842a755e284a2fd4a66dc17b9638385f46ad74e926baac2e797335a2
checksum: 10/300608850a29215d7b47fe8ade183fc2241457a924335bd127e29e1af11da9314369c65ec0da968177086f3529abbcd71a609c1af673ea8951c32a523cab358c
languageName: node
linkType: hard
@@ -4052,92 +4052,92 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-darwin-arm64@npm:1.5.6"
"@rspack/binding-darwin-arm64@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-darwin-arm64@npm:1.5.5"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-darwin-x64@npm:1.5.6"
"@rspack/binding-darwin-x64@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-darwin-x64@npm:1.5.5"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.5.6"
"@rspack/binding-linux-arm64-gnu@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.5.5"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-linux-arm64-musl@npm:1.5.6"
"@rspack/binding-linux-arm64-musl@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-linux-arm64-musl@npm:1.5.5"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-linux-x64-gnu@npm:1.5.6"
"@rspack/binding-linux-x64-gnu@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-linux-x64-gnu@npm:1.5.5"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-linux-x64-musl@npm:1.5.6"
"@rspack/binding-linux-x64-musl@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-linux-x64-musl@npm:1.5.5"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-wasm32-wasi@npm:1.5.6"
"@rspack/binding-wasm32-wasi@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-wasm32-wasi@npm:1.5.5"
dependencies:
"@napi-rs/wasm-runtime": "npm:^1.0.5"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.5.6"
"@rspack/binding-win32-arm64-msvc@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.5.5"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.5.6"
"@rspack/binding-win32-ia32-msvc@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.5.5"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding-win32-x64-msvc@npm:1.5.6"
"@rspack/binding-win32-x64-msvc@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding-win32-x64-msvc@npm:1.5.5"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/binding@npm:1.5.6"
"@rspack/binding@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/binding@npm:1.5.5"
dependencies:
"@rspack/binding-darwin-arm64": "npm:1.5.6"
"@rspack/binding-darwin-x64": "npm:1.5.6"
"@rspack/binding-linux-arm64-gnu": "npm:1.5.6"
"@rspack/binding-linux-arm64-musl": "npm:1.5.6"
"@rspack/binding-linux-x64-gnu": "npm:1.5.6"
"@rspack/binding-linux-x64-musl": "npm:1.5.6"
"@rspack/binding-wasm32-wasi": "npm:1.5.6"
"@rspack/binding-win32-arm64-msvc": "npm:1.5.6"
"@rspack/binding-win32-ia32-msvc": "npm:1.5.6"
"@rspack/binding-win32-x64-msvc": "npm:1.5.6"
"@rspack/binding-darwin-arm64": "npm:1.5.5"
"@rspack/binding-darwin-x64": "npm:1.5.5"
"@rspack/binding-linux-arm64-gnu": "npm:1.5.5"
"@rspack/binding-linux-arm64-musl": "npm:1.5.5"
"@rspack/binding-linux-x64-gnu": "npm:1.5.5"
"@rspack/binding-linux-x64-musl": "npm:1.5.5"
"@rspack/binding-wasm32-wasi": "npm:1.5.5"
"@rspack/binding-win32-arm64-msvc": "npm:1.5.5"
"@rspack/binding-win32-ia32-msvc": "npm:1.5.5"
"@rspack/binding-win32-x64-msvc": "npm:1.5.5"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -4159,23 +4159,23 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/852113a80ff7396257426a6a3a6c6fd47f5743c7304b90de9d348ed0496e5f335bcc0617f857be2d9e836fa610dd7a3952a1a834b756a228c4913acb78a3d3fa
checksum: 10/65b71796a3e8f1bc5a374253aafc128076cf1b02ac0ae8484eff897420152f1863c153dd9195ba84a8d2c4a41ab8a41d590b0645308f6035ab188c9c3d33b214
languageName: node
linkType: hard
"@rspack/core@npm:1.5.6":
version: 1.5.6
resolution: "@rspack/core@npm:1.5.6"
"@rspack/core@npm:1.5.5":
version: 1.5.5
resolution: "@rspack/core@npm:1.5.5"
dependencies:
"@module-federation/runtime-tools": "npm:0.18.0"
"@rspack/binding": "npm:1.5.6"
"@rspack/binding": "npm:1.5.5"
"@rspack/lite-tapable": "npm:1.0.1"
peerDependencies:
"@swc/helpers": ">=0.5.1"
peerDependenciesMeta:
"@swc/helpers":
optional: true
checksum: 10/50814815c63b611c2e9a7724dfa194e8b52e7232fa18637ef17c6de79c36ceb798f1fd7501e869b24a4f4f4ab6c198c5ce43884be81e7421c82a0e4880dc9600
checksum: 10/864e16e3370ee09cbe26a29220a59392f10e61b8ae1e258139c9939c2ecc20c8899e92357e67bdb81603ce102baa46e5bef916de3b39000828d40c54158ab816
languageName: node
linkType: hard
@@ -4491,10 +4491,10 @@ __metadata:
languageName: node
linkType: hard
"@types/chromecast-caf-receiver@npm:6.0.22":
version: 6.0.22
resolution: "@types/chromecast-caf-receiver@npm:6.0.22"
checksum: 10/6c51cb52527776ddfa187a261b88184c98bdd61c129dd8719cba213894d565cf69073734d6473696ffd60a768f6fb5a3fe9932693f43174fbc5e7af201db8a90
"@types/chromecast-caf-receiver@npm:6.0.24":
version: 6.0.24
resolution: "@types/chromecast-caf-receiver@npm:6.0.24"
checksum: 10/1f2b95e8a15dbb36d5328895229d4a5cb255b33e62d46335bd6ed75e16aa9ea6a7d765a64ae120d19b3134fb3e51e9547d2544c7277f7bffe0bf0b3999f026da
languageName: node
linkType: hard
@@ -5000,106 +5000,106 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.44.1"
"@typescript-eslint/eslint-plugin@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.44.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.44.1"
"@typescript-eslint/type-utils": "npm:8.44.1"
"@typescript-eslint/utils": "npm:8.44.1"
"@typescript-eslint/visitor-keys": "npm:8.44.1"
"@typescript-eslint/scope-manager": "npm:8.44.0"
"@typescript-eslint/type-utils": "npm:8.44.0"
"@typescript-eslint/utils": "npm:8.44.0"
"@typescript-eslint/visitor-keys": "npm:8.44.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
"@typescript-eslint/parser": ^8.44.1
"@typescript-eslint/parser": ^8.44.0
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e0f69d1d24bbf63c3f2937f85b49994eae907656b01bc9d3563f096750add1085c4b15953b82b750b0da2b8444850558a8bf0d5bcfb8f0410dfd628f4245dc11
checksum: 10/38d0491d96740d1c9f412b0ee1a8a24ed132433347e52810b030e331b27a846b2c70a8424b5684e7ccd7a7ed231e336dd40a0642f5014409ed28e84561fd0757
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/parser@npm:8.44.1"
"@typescript-eslint/parser@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/parser@npm:8.44.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.44.1"
"@typescript-eslint/types": "npm:8.44.1"
"@typescript-eslint/typescript-estree": "npm:8.44.1"
"@typescript-eslint/visitor-keys": "npm:8.44.1"
"@typescript-eslint/scope-manager": "npm:8.44.0"
"@typescript-eslint/types": "npm:8.44.0"
"@typescript-eslint/typescript-estree": "npm:8.44.0"
"@typescript-eslint/visitor-keys": "npm:8.44.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/ff5048c36d9fde27a03f64f3c4ad4739370fde1d744fa7bd1e08280601bd9adfe64c740789fd2adede54dd212a005c59bf1c06c68d05f57b7028332838ed28f8
checksum: 10/8c7ddabf46a94877e3af9b029c5e65cc99ec2983ee14842eea7e8ebccc0e24f589cdf7d2ca63ece87284ea7a4d9d622a22452ac5c66cb6ebda1b91b438e20979
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/project-service@npm:8.44.1"
"@typescript-eslint/project-service@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/project-service@npm:8.44.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.44.1"
"@typescript-eslint/types": "npm:^8.44.1"
"@typescript-eslint/tsconfig-utils": "npm:^8.44.0"
"@typescript-eslint/types": "npm:^8.44.0"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/4b74d9d1c113b2637b6d65c790bfd2fa15ab1061fe77e68519c3b1939f4b0ee9e15d621ffc946ae2ef457289e830ddea879553868d5c7ff1af4904d7842792e0
checksum: 10/400b4981e6f6bd323592df7803383c0f223256f76b4876c03c2e8fa5e9470ce90a6a39555f759a391ba87bbf79add800eadf42ba06acf0732407a225790722de
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/scope-manager@npm:8.44.1"
"@typescript-eslint/scope-manager@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/scope-manager@npm:8.44.0"
dependencies:
"@typescript-eslint/types": "npm:8.44.1"
"@typescript-eslint/visitor-keys": "npm:8.44.1"
checksum: 10/f731becce1f79b3add939417e31c7ae38c9150d73de5dec4141376cc64e1bb69f8d6b9f2072f8f442995a1e30eab57fd73c1a4b87220e19abb0f210e2c123096
"@typescript-eslint/types": "npm:8.44.0"
"@typescript-eslint/visitor-keys": "npm:8.44.0"
checksum: 10/5dae4a838637df522f0f71d2df238b5fd5045a374cd036e7485d39fc1b20dd1151185463c8fc35441b12c747b795c88b429a5e229fc9449ae7b15d6e7e1b6caf
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.44.1, @typescript-eslint/tsconfig-utils@npm:^8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.44.1"
"@typescript-eslint/tsconfig-utils@npm:8.44.0, @typescript-eslint/tsconfig-utils@npm:^8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.44.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/942d4bb9ec3d0f1f6c7fe0dc0fef2ae83a12b43ff3537fbd74007d0c9b80f166db2e5fa2f422f0b10ade348e330204dc70fc50e235ee66dc13ba488ac1490778
checksum: 10/c8535d481d7cda5c846d5b74b7cde7a98b636a558b45510820840dcb9928575c4f3d26b3c021fd7e47b782c6d9a73e9a8adf29191548406ac4b02e1a1dce928f
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/type-utils@npm:8.44.1"
"@typescript-eslint/type-utils@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/type-utils@npm:8.44.0"
dependencies:
"@typescript-eslint/types": "npm:8.44.1"
"@typescript-eslint/typescript-estree": "npm:8.44.1"
"@typescript-eslint/utils": "npm:8.44.1"
"@typescript-eslint/types": "npm:8.44.0"
"@typescript-eslint/typescript-estree": "npm:8.44.0"
"@typescript-eslint/utils": "npm:8.44.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/696747b2a048c281d8cfe74b3f61b7af2e7fa371e9afa58de6d6b49ad7cfa2577d52ddd66fe8b243d2d039b489b6db07bf18a746b14004456c8405842276aa92
checksum: 10/513c6d371922c60522a44a659ca99d3c9978b61781a27831fb60f522190c387f2c48f4759614483fb904953a0c1391fd0a9e2027e30ab1eebfea49f404f1a560
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.44.1, @typescript-eslint/types@npm:^8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/types@npm:8.44.1"
checksum: 10/acebff929b2c64254c430fff54d8d135c9f47bcc20062fd3e52f64952b0ef973db9582812025f5314940889ae4c4a8798726a477b94fbda31881109687567528
"@typescript-eslint/types@npm:8.44.0, @typescript-eslint/types@npm:^8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/types@npm:8.44.0"
checksum: 10/9e28c95feb0d3b9ae83117a8b3db43db4e9a5d2fcbf8fd0c6e231e0ae604fe894b6725642fe3234733e81e32bec6f03b270d8a27d4258c0ee0b44a8bd8685756
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/typescript-estree@npm:8.44.1"
"@typescript-eslint/typescript-estree@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/typescript-estree@npm:8.44.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.44.1"
"@typescript-eslint/tsconfig-utils": "npm:8.44.1"
"@typescript-eslint/types": "npm:8.44.1"
"@typescript-eslint/visitor-keys": "npm:8.44.1"
"@typescript-eslint/project-service": "npm:8.44.0"
"@typescript-eslint/tsconfig-utils": "npm:8.44.0"
"@typescript-eslint/types": "npm:8.44.0"
"@typescript-eslint/visitor-keys": "npm:8.44.0"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
@@ -5108,32 +5108,32 @@ __metadata:
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/b7b4d177e9339c978a090f1ec23c3f58316845b1cfc4f80a59f481d748b19078ab2cf4fe2d3aa063ad3dc556ea678289e2a9f61e12d7beaeb2bb681599b7481b
checksum: 10/e2e579b15c49e0ef6d2da9d06105ff2c995c785b1bb6cc0a11aa124c1bcc6435ee8897a9d9d6346951a8cc329b8af6410bc982dc5e55272b9af650b11daa536a
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/utils@npm:8.44.1"
"@typescript-eslint/utils@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/utils@npm:8.44.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.44.1"
"@typescript-eslint/types": "npm:8.44.1"
"@typescript-eslint/typescript-estree": "npm:8.44.1"
"@typescript-eslint/scope-manager": "npm:8.44.0"
"@typescript-eslint/types": "npm:8.44.0"
"@typescript-eslint/typescript-estree": "npm:8.44.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/d7757d400a14bd69272da5e32dc61893ec958a9776b2436e2980d7e638164c88edb4b56c5faff6cf8ea61b1fd8a3f6c78ad4f7fc5c4e7d217d960e08039f7c40
checksum: 10/436e21e3d01e2f6f70fea1d3175de6836f72aa52220c6482c871e5b72f845215db46a8e58e5ab83a197326559d075afb61bedabacbdaa96a243a18f43ac5d6cb
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.44.1":
version: 8.44.1
resolution: "@typescript-eslint/visitor-keys@npm:8.44.1"
"@typescript-eslint/visitor-keys@npm:8.44.0":
version: 8.44.0
resolution: "@typescript-eslint/visitor-keys@npm:8.44.0"
dependencies:
"@typescript-eslint/types": "npm:8.44.1"
"@typescript-eslint/types": "npm:8.44.0"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/040f57906265d9ba5ec2230e728eea87bf6af9e0d345017de9e5b05211469457d838435f8b776354b403dad7b2c4527b68863c4ab6750f2668731dd2a3b8f9e8
checksum: 10/09b008b14f46ea14bb5076632745dd527982f231830141a3c789ed0cd3d7bced7340a0ccfb40c640b765de42c267202db83072587144791df4806bd68233596c
languageName: node
linkType: hard
@@ -9355,10 +9355,10 @@ __metadata:
languageName: node
linkType: hard
"hls.js@npm:1.6.13":
version: 1.6.13
resolution: "hls.js@npm:1.6.13"
checksum: 10/4de045fddbeb6edc3859021ff60b268a642aa87e83347a3a764e53299b616d57f36eecef0b91eab240ffde84f75d1ab5f7cc0916a929de265707d0f0b27d1c71
"hls.js@npm:1.6.12":
version: 1.6.12
resolution: "hls.js@npm:1.6.12"
checksum: 10/b0f23fcda44c6a4dc16dc501b3a17829417133079fefb7463a1e3d22ae9da24cb970e9e45165e4223b1e6b3a3d0f253ca680dcc9daf2e27d3d8153390ed3b9be
languageName: node
linkType: hard
@@ -9379,7 +9379,7 @@ __metadata:
"@codemirror/legacy-modes": "npm:6.5.1"
"@codemirror/search": "npm:6.5.11"
"@codemirror/state": "npm:6.5.2"
"@codemirror/view": "npm:6.38.3"
"@codemirror/view": "npm:6.38.2"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.0"
@@ -9435,14 +9435,14 @@ __metadata:
"@octokit/rest": "npm:22.0.0"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.2.3"
"@rspack/core": "npm:1.5.6"
"@rspack/core": "npm:1.5.5"
"@rspack/dev-server": "npm:1.1.4"
"@swc/helpers": "npm:0.5.17"
"@thomasloven/round-slider": "npm:0.6.0"
"@tsparticles/engine": "npm:3.9.1"
"@tsparticles/preset-links": "npm:3.2.0"
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
"@types/chromecast-caf-receiver": "npm:6.0.22"
"@types/chromecast-caf-receiver": "npm:6.0.24"
"@types/chromecast-caf-sender": "npm:1.0.11"
"@types/color-name": "npm:2.0.0"
"@types/culori": "npm:4.0.1"
@@ -9502,7 +9502,7 @@ __metadata:
gulp-json-transform: "npm:0.5.0"
gulp-rename: "npm:2.1.0"
gulp-zopfli-green: "npm:6.0.2"
hls.js: "npm:1.6.13"
hls.js: "npm:1.6.12"
home-assistant-js-websocket: "npm:9.5.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
@@ -9544,7 +9544,7 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.2"
typescript-eslint: "npm:8.44.1"
typescript-eslint: "npm:8.44.0"
ua-parser-js: "npm:2.0.5"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:3.2.4"
@@ -14576,18 +14576,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.44.1":
version: 8.44.1
resolution: "typescript-eslint@npm:8.44.1"
"typescript-eslint@npm:8.44.0":
version: 8.44.0
resolution: "typescript-eslint@npm:8.44.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.44.1"
"@typescript-eslint/parser": "npm:8.44.1"
"@typescript-eslint/typescript-estree": "npm:8.44.1"
"@typescript-eslint/utils": "npm:8.44.1"
"@typescript-eslint/eslint-plugin": "npm:8.44.0"
"@typescript-eslint/parser": "npm:8.44.0"
"@typescript-eslint/typescript-estree": "npm:8.44.0"
"@typescript-eslint/utils": "npm:8.44.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/9bd0601ef67d0fb20b095a722f4286b3b5d27905ff926a9375fc0934746626d72339d74ba40eb9d361af7fda1987294b0533dbed5dddbac79814daf196a301ac
checksum: 10/7c406b064d35f0fcad95c372a73e7cead2452b2f05ddeaef0a512140de9881d402e6a76c997e014e5719de4eac8a1dac64d293d8958e0178108c49b1dc49c5d6
languageName: node
linkType: hard