Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen
30ae2a1053 Show the protocols on the top level of the config section 2025-12-08 21:29:28 -05:00
403 changed files with 3158 additions and 4212 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
node_modules/.cache/prettier
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7

View File

@@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
github-token: ${{ github.token }}
process-only: "issues, prs"

View File

@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: translations
path: translations.tar.gz

1
.gitignore vendored
View File

@@ -56,5 +56,4 @@ test/coverage/
# AI tooling
.claude
.cursor

2
.nvmrc
View File

@@ -1 +1 @@
24.12.0
22.21.1

View File

@@ -1,4 +1,4 @@
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockDeviceRegistry = (

View File

@@ -1,4 +1,4 @@
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (

View File

@@ -1,4 +1,4 @@
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockLabelRegistry = (

View File

@@ -11,11 +11,11 @@ import { computeInitialHaFormData } from "../../../../src/components/ha-form/com
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {

View File

@@ -13,9 +13,9 @@ import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label_registry";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";

View File

@@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { IntegrationManifest } from "../../../../src/data/integration";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
import type { EntityRegistryEntry } from "../../../../src/data/entity_registry";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../../../src/panels/config/integrations/ha-config-flow-card";
import type {

View File

@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.39.4",
"@codemirror/view": "6.38.8",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-ha.2",
"@home-assistant/webawesome": "3.0.0-ha.1",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -135,7 +135,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.7",
"ua-parser-js": "2.0.6",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -157,8 +157,8 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.15",
"@rspack/core": "1.6.7",
"@rsdoctor/rspack-plugin": "1.3.12",
"@rspack/core": "1.6.6",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
@@ -201,7 +201,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.3.0",
"jsdom": "27.2.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lit-analyzer": "2.0.3",
@@ -214,10 +214,10 @@
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.16",
"terser-webpack-plugin": "5.3.15",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.49.0",
"typescript-eslint": "8.48.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.15",
"webpack-stats-plugin": "1.1.3",
@@ -238,6 +238,6 @@
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.12.0"
"node": "22.21.1"
}
}

View File

@@ -3,8 +3,8 @@ import {
DOMAIN_ATTRIBUTES_FORMATERS,
DOMAIN_ATTRIBUTES_UNITS,
TEMPERATURE_ATTRIBUTES,
} from "../../data/entity/entity_attributes";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
} from "../../data/entity_attributes";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FrontendLocaleData } from "../../data/translation";
import type { WeatherEntity } from "../../data/weather";
import { getWeatherUnit } from "../../data/weather";

View File

@@ -1,12 +1,12 @@
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../../data/entity/entity_registry";
} from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import { getDuplicates } from "../string/get_duplicates";
import { computeStateName } from "./compute_state_name";
import { getDuplicates } from "../string/get_duplicates";
export const computeDeviceName = (
device: DeviceRegistryEntry

View File

@@ -2,7 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../../data/entity/entity_registry";
} from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import { computeDeviceName } from "./compute_device_name";
import { computeStateName } from "./compute_state_name";

View File

@@ -1,12 +1,12 @@
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FrontendLocaleData } from "../../data/translation";
import { TimeZone } from "../../data/translation";
import type { HomeAssistant } from "../../types";
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
import { formatTime } from "../datetime/format_time";
import {
formatNumber,

View File

@@ -1,5 +1,5 @@
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";

View File

@@ -1,11 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../../data/entity/entity_registry";
} from "../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";

View File

@@ -1,14 +1,14 @@
import type { ConfigEntry } from "../../data/config_entries";
import { deleteConfigEntry } from "../../data/config_entries";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import { removeEntityRegistryEntry } from "../../data/entity/entity_registry";
import { HELPERS_CRUD } from "../../data/helpers_crud";
import type { HomeAssistant } from "../../types";
import type { IntegrationManifest } from "../../data/integration";
import { computeDomain } from "./compute_domain";
import { HELPERS_CRUD } from "../../data/helpers_crud";
import type { Helper } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import type { HomeAssistant } from "../../types";
import { isComponentLoaded } from "../config/is_component_loaded";
import { computeDomain } from "./compute_domain";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import type { ConfigEntry } from "../../data/config_entries";
import { deleteConfigEntry } from "../../data/config_entries";
export const isDeletableEntity = (
hass: HomeAssistant,

View File

@@ -1,9 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { stringCompare } from "../string/compare";
import { computeDomain } from "./compute_domain";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import { computeDomain } from "./compute_domain";
import { stringCompare } from "../string/compare";
export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { computeStateDomain } from "./compute_state_domain";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
import type { HomeAssistant } from "../../types";
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
if (!states.length) {

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
import { computeDomain } from "./compute_domain";
export function stateActive(stateObj: HassEntity, state?: string): boolean {

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity/entity";
import { UNAVAILABLE } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import { DOMAINS_WITH_CARD } from "../const";
import { canToggleState } from "./can_toggle_state";

View File

@@ -1,6 +1,6 @@
/** Return a color representing a state. */
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity/entity";
import { UNAVAILABLE } from "../../data/entity";
import type { GroupEntity } from "../../data/group";
import { computeGroupDomain } from "../../data/group";
import { computeCssVariable } from "../../resources/css-variables";

View File

@@ -17,36 +17,25 @@ export interface NavigateOptions {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
/**
* Ensures all dialogs are closed before navigation.
* Returns true if navigation can proceed, false if a dialog refused to close.
*/
const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
export const navigate = async (
path: string,
options?: NavigateOptions,
timestamp = Date.now()
) => {
const { history } = mainWindow;
if (!history.state?.dialog || Date.now() - timestamp >= DIALOG_WAIT_TIMEOUT) {
return true;
}
const closed = await closeAllDialogs();
if (!closed) {
// eslint-disable-next-line no-console
console.warn("Navigation blocked, because dialog refused to close");
return false;
}
// wait for history state to be updated after dialog closed
await new Promise<void>((resolve) => {
setTimeout(resolve);
});
return ensureDialogsClosed(timestamp);
};
export const navigate = async (path: string, options?: NavigateOptions) => {
const canProceed = await ensureDialogsClosed(Date.now());
if (!canProceed) {
return false;
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
const closed = await closeAllDialogs();
if (!closed) {
// eslint-disable-next-line no-console
console.warn("Navigation blocked, because dialog refused to close");
return false;
}
return new Promise<boolean>((resolve) => {
// need to wait for history state to be updated in case a dialog was closed
setTimeout(() => {
navigate(path, options, timestamp).then(resolve);
});
});
}
const replace = options?.replace || false;
@@ -79,14 +68,10 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
* Navigate back in history, with fallback to a default path if no history exists.
* This prevents a user from getting stuck when they navigate directly to a page with no history.
*/
export const goBack = async (fallbackPath?: string): Promise<void> => {
const canProceed = await ensureDialogsClosed(Date.now());
if (!canProceed) {
return;
}
export const goBack = (fallbackPath?: string) => {
const { history } = mainWindow;
// Check if we have history to go back to
const { history } = mainWindow;
if (history.length > 1) {
history.back();
return;

View File

@@ -2,7 +2,7 @@ import type {
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FrontendLocaleData } from "../../data/translation";
import { NumberFormat } from "../../data/translation";
import { round } from "./round";

View File

@@ -1,45 +1,30 @@
/**
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
* Executes a callback within a View Transition if supported, otherwise runs it directly.
*
* @param callback - Synchronous function to execute. The callback will be passed a boolean indicating whether the view transition is available.
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
* @returns Promise that resolves when the transition completes (or immediately if not supported)
*
* @example
* ```typescript
* // Synchronous callback
* withViewTransition(() => {
* this.large = !this.large;
* });
*
* // Async callback
* await withViewTransition(async () => {
* await this.updateData();
* });
* ```
*/
export const withViewTransition = (
callback: (viewTransitionAvailable: boolean) => void
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
): Promise<void> => {
if (!document.startViewTransition) {
callback(false);
return Promise.resolve();
if (document.startViewTransition) {
return document.startViewTransition(() => callback(true)).finished;
}
let callbackInvoked = false;
try {
// View Transitions require DOM updates to happen synchronously within
// the callback. Execute the callback immediately (synchronously).
const transition = document.startViewTransition(() => {
callbackInvoked = true;
callback(true);
});
return transition.finished;
} catch (err) {
// eslint-disable-next-line no-console
console.warn(
"View transition failed, falling back to direct execution.",
err
);
// Make sure the callback is invoked exactly once.
if (!callbackInvoked) {
callback(false);
return Promise.resolve();
}
return Promise.reject(err);
}
// Fallback: Execute callback directly without transition
const result = callback(false);
return result instanceof Promise ? result : Promise.resolve();
};

View File

@@ -1,207 +0,0 @@
import type { EChartsType } from "echarts/core";
import type { SunburstSeriesOption } from "echarts/types/dist/echarts";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { filterXSS } from "../../common/util/xss";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
export interface SunburstNode {
id: string;
name?: string;
value: number;
itemStyle?: {
color?: string;
};
children?: SunburstNode[];
}
@customElement("ha-sunburst-chart")
export class HaSunburstChart extends LitElement {
public hass!: HomeAssistant;
@property({ attribute: false }) public data?: SunburstNode;
@property({ type: String, attribute: false }) public valueFormatter?: (
value: number
) => string;
public chart?: EChartsType;
constructor() {
super();
if (!SunburstChart) {
import("echarts/lib/chart/sunburst/install").then((module) => {
SunburstChart = module;
this.requestUpdate();
});
}
}
render() {
if (!SunburstChart || !this.data) {
return nothing;
}
const options = {
tooltip: {
trigger: "item",
formatter: this._renderTooltip,
appendTo: document.body,
},
} as ECOption;
return html`<ha-chart-base
.data=${this._createData(this.data)}
.options=${options}
height="100%"
.extraComponents=${[SunburstChart]}
></ha-chart-base>`;
}
private _renderTooltip = (params: CallbackDataParams) => {
const data = params.data as { name: string; value: number };
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
};
private _createData = memoizeOne(
(data: SunburstNode): SunburstSeriesOption => {
const computedStyles = getComputedStyle(this);
// Transform to echarts format (uses 'name' instead of 'id')
const transformNode = (
node: SunburstNode,
index: number,
depth: number,
parentColor?: string
) => {
const result = {
...node,
name: node.name || node.id,
};
if (depth > 0 && !node.itemStyle?.color) {
// Don't assign color to root node
result.itemStyle = {
color: parentColor ?? getGraphColorByIndex(index, computedStyles),
};
}
if (node.children && node.children.length > 0) {
result.children = node.children.map((child, i) =>
transformNode(child, i, depth + 1, result.itemStyle?.color)
);
}
return result;
};
const transformedData = transformNode(data, 0, 0);
return {
type: "sunburst",
data: transformedData.children || [transformedData],
radius: [0, "90%"],
sort: undefined, // Keep original order
label: {
show: false,
align: "center",
rotate: "radial",
minAngle: 15,
hideOverlap: true,
},
emphasis: {
focus: "ancestor",
label: {
show: false,
},
},
itemStyle: {
borderRadius: 2,
},
levels: this._generateLevels(this._getMaxDepth(data)),
} as SunburstSeriesOption;
}
);
private _getMaxDepth(node: SunburstNode, currentDepth = 0): number {
if (!node.children || node.children.length === 0) {
return currentDepth;
}
return Math.max(
...node.children.map((child) =>
this._getMaxDepth(child, currentDepth + 1)
)
);
}
private _generateLevels(depth: number): SunburstSeriesOption["levels"] {
const levels: SunburstSeriesOption["levels"] = [];
// Root level (center) - transparent, small fixed size
const rootRadius = 15;
const outerRadius = 95;
const availableRadius = outerRadius - rootRadius;
levels.push({
r0: "0%",
r: `${rootRadius}%`,
itemStyle: {
color: "transparent",
},
});
if (depth === 0) {
return levels;
}
// Distribute remaining radius among data levels using weighted distribution
// First level gets most space, each subsequent level gets progressively smaller
const weights = Array.from({ length: depth }, (_, i) => depth - i);
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
let currentRadius = rootRadius;
for (let i = 0; i < depth; i++) {
const levelRadius = (weights[i] / totalWeight) * availableRadius;
const r0 = currentRadius;
const r = currentRadius + levelRadius;
currentRadius = r;
levels.push({
r0: `${r0}%`,
r: `${r}%`,
// Show labels only on first level
...(i === 0 ? { label: { show: true } } : {}),
});
}
return levels;
}
static styles = css`
:host {
display: block;
flex: 1;
}
ha-chart-base {
width: 100%;
height: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-sunburst-chart": HaSunburstChart;
}
}

View File

@@ -184,11 +184,17 @@ export class StatisticsChart extends LitElement {
}
private _datasetHidden(ev: CustomEvent) {
if (!this._legendData) {
return;
}
this._hiddenStats.add(ev.detail.id);
this.requestUpdate("_hiddenStats");
}
private _datasetUnhidden(ev: CustomEvent) {
if (!this._legendData) {
return;
}
this._hiddenStats.delete(ev.detail.id);
this.requestUpdate("_hiddenStats");
}
@@ -515,9 +521,7 @@ export class StatisticsChart extends LitElement {
`ui.components.statistics_charts.statistic_types.${type}`
),
symbol: "none",
// minmax sampling operates independently per series, breaking stacking alignment
// https://github.com/apache/echarts/issues/11879
sampling: band && drawBands ? "lttb" : "minmax",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: 1.5,
@@ -535,17 +539,10 @@ export class StatisticsChart extends LitElement {
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
series.stackStrategy = "all";
if (this._hiddenStats.has(`${statistic_id}-${bandBottom}`)) {
// changing the stackOrder forces echarts to render the stacked series that are not hidden #28472
series.stackOrder = "seriesDesc";
(series as LineSeriesOption).areaStyle = undefined;
} else {
series.stackOrder = "seriesAsc";
if (drawBands && type === bandTop) {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
}
if (drawBands && type === bandTop) {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
}
}
if (!this.hideLegend) {
@@ -589,8 +586,7 @@ export class StatisticsChart extends LitElement {
} else if (
type === bandTop &&
this.chartType === "line" &&
drawBands &&
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
drawBands
) {
const top = stat[bandTop] || 0;
val.push(Math.abs(top - (stat[bandBottom] || 0)));

View File

@@ -3,11 +3,11 @@ import { getGraphColorByIndex } from "../../common/color/colors";
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
import { labBrighten } from "../../common/color/lab";
import { computeDomain } from "../../common/entity/compute_domain";
import { stateColorProperties } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { computeCssValue } from "../../resources/css-variables";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import { stateColorProperties } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { computeCssValue } from "../../resources/css-variables";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {

View File

@@ -6,7 +6,7 @@ import { computeCssColor } from "../../common/color/compute-color";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { stringCompare } from "../../common/string/compare";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import "../chips/ha-chip-set";
import "../ha-dropdown";
import "../ha-dropdown-item";

View File

@@ -16,10 +16,8 @@ import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
@@ -28,6 +26,8 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import type { LocalizeFunc } from "../../common/translations/localize";
import { nextRender } from "../../common/util/render-status";
export interface RowClickedEvent {
id: string;

View File

@@ -1,9 +1,9 @@
import { expose } from "comlink";
import Fuse, { type FuseOptionKey } from "fuse.js";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import { multiTermSearch } from "../../resources/fuseMultiTerm";
import { HaFuse } from "../../resources/fuse";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -11,10 +11,9 @@ import type {
SortingDirection,
} from "./ha-data-table";
const getSearchKeys = memoizeOne(
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
const fuseIndex = memoizeOne(
(data: DataTableRowData[], columns: SortableColumnContainer) => {
const searchKeys = new Set<string>();
Object.entries(columns).forEach(([key, column]) => {
if (column.filterable) {
searchKeys.add(
@@ -24,15 +23,10 @@ const getSearchKeys = memoizeOne(
);
}
});
return Array.from(searchKeys);
return Fuse.createIndex([...searchKeys], data);
}
);
const fuseIndex = memoizeOne(
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
Fuse.createIndex(keys, data)
);
const filterData = (
data: DataTableRowData[],
columns: SortableColumnContainer,
@@ -44,13 +38,21 @@ const filterData = (
return data;
}
const keys = getSearchKeys(columns);
const index = fuseIndex(data, columns);
const index = fuseIndex(data, keys);
const fuse = new HaFuse(
data,
{ shouldSort: false, minMatchCharLength: 1 },
index
);
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
threshold: 0.2, // reduce fuzzy matches in data tables
});
const searchResults = fuse.multiTermsSearch(filter);
if (searchResults) {
return searchResults.map((result) => result.item);
}
return data;
};
const sortData = (

View File

@@ -1,9 +1,9 @@
import { customElement } from "lit/decorators";
import type { DeviceAction } from "../../data/device/device_automation";
import type { DeviceAction } from "../../data/device_automation";
import {
fetchDeviceActions,
localizeDeviceAutomationAction,
} from "../../data/device/device_automation";
} from "../../data/device_automation";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker")

View File

@@ -2,17 +2,17 @@ import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fullEntitiesContext } from "../../data/context";
import type { DeviceAutomation } from "../../data/device/device_automation";
import type { DeviceAutomation } from "../../data/device_automation";
import {
deviceAutomationsEqual,
sortDeviceAutomations,
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
} from "../../data/device_automation";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import "../ha-md-select";
import "../ha-md-select-option";
import "../ha-md-select";
import { stopPropagation } from "../../common/dom/stop_propagation";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";

View File

@@ -1,9 +1,9 @@
import { customElement } from "lit/decorators";
import type { DeviceCondition } from "../../data/device/device_automation";
import type { DeviceCondition } from "../../data/device_automation";
import {
fetchDeviceConditions,
localizeDeviceAutomationCondition,
} from "../../data/device/device_automation";
} from "../../data/device_automation";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-condition-picker")

View File

@@ -9,11 +9,10 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
import {
deviceComboBoxKeys,
getDevices,
type DevicePickerItem,
} from "../../data/device/device_picker";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
type DeviceRegistryEntry,
} from "../../data/device_registry";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
@@ -217,10 +216,6 @@ export class HaDevicePicker extends LitElement {
.getItems=${this._getItems}
.hideClearIcon=${this.hideClearIcon}
.valueRenderer=${valueRenderer}
.searchKeys=${deviceComboBoxKeys}
.unknownItemText=${this.hass.localize(
"ui.components.device-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -1,9 +1,9 @@
import { customElement } from "lit/decorators";
import type { DeviceTrigger } from "../../data/device/device_automation";
import type { DeviceTrigger } from "../../data/device_automation";
import {
fetchDeviceTriggers,
localizeDeviceAutomationTrigger,
} from "../../data/device/device_automation";
} from "../../data/device_automation";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker")

View File

@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-sortable";
import "./ha-entity-picker";

View File

@@ -7,12 +7,11 @@ import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { computeRTL } from "../../common/util/compute_rtl";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import {
entityComboBoxKeys,
getEntities,
type EntityComboBoxItem,
} from "../../data/entity/entity_picker";
} from "../../data/entity_registry";
import { domainToName } from "../../data/integration";
import {
isHelperDomain,
@@ -228,7 +227,7 @@ export class HaEntityPicker extends LitElement {
if (!createDomains?.length) {
return [];
}
this.hass.loadFragmentTranslation("config");
return createDomains.map((domain) => {
const primary = localize(
"ui.components.entity.entity-picker.create_helper",
@@ -236,7 +235,7 @@ export class HaEntityPicker extends LitElement {
domain: isHelperDomain(domain)
? localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
) || domain
)
: domainToName(localize, domain),
}
);
@@ -289,14 +288,10 @@ export class HaEntityPicker extends LitElement {
.hideClearIcon=${this.hideClearIcon}
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys}
@value-changed=${this._valueChanged}
.addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined}
.unknownItemText=${this.hass.localize(
"ui.components.entity.entity-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;

View File

@@ -6,11 +6,7 @@ import { customElement, property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
UNAVAILABLE,
UNKNOWN,
isUnavailableState,
} from "../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN, isUnavailableState } from "../../data/entity";
import { forwardHaptic } from "../../data/haptics";
import type { HomeAssistant } from "../../types";
import "../ha-formfield";

View File

@@ -14,12 +14,8 @@ import {
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import {
isUnavailableState,
UNAVAILABLE,
UNKNOWN,
} from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
import "../ha-label-badge";

View File

@@ -38,21 +38,9 @@ type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticComboBoxItem extends PickerComboBoxItem {
statistic_id?: string;
stateObj?: HassEntity;
domainName?: string;
type?: StatisticItemType;
}
const SEARCH_KEYS = [
{ name: "label", weight: 10 },
{ name: "search_labels.entityName", weight: 10 },
{ name: "search_labels.friendlyName", weight: 9 },
{ name: "search_labels.deviceName", weight: 8 },
{ name: "search_labels.areaName", weight: 6 },
{ name: "search_labels.domainName", weight: 4 },
{ name: "statisticId", weight: 3 },
{ name: "id", weight: 2 },
];
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -245,6 +233,7 @@ export class HaStatisticPicker extends LitElement {
),
type,
sorting_label: [sortingPrefix, label].join("_"),
search_labels: [label, id],
icon_path: mdiShape,
});
} else if (type === "external") {
@@ -257,7 +246,7 @@ export class HaStatisticPicker extends LitElement {
secondary: domainName,
type,
sorting_label: [sortingPrefix, label].join("_"),
search_labels: { label, domainName },
search_labels: [label, domainName, id],
icon_path: mdiChartLine,
});
}
@@ -291,12 +280,13 @@ export class HaStatisticPicker extends LitElement {
stateObj: stateObj,
type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
areaName: areaName || null,
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
},
id,
].filter(Boolean) as string[],
});
});
@@ -371,13 +361,13 @@ export class HaStatisticPicker extends LitElement {
stateObj: stateObj,
type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
areaName: areaName || null,
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
statisticId,
},
].filter(Boolean) as string[],
};
}
@@ -404,7 +394,7 @@ export class HaStatisticPicker extends LitElement {
secondary: domainName,
type: "external",
sorting_label: [sortingPrefix, label].join("_"),
search_labels: { label, domainName, statisticId },
search_labels: [label, domainName, statisticId],
icon_path: mdiChartLine,
};
}
@@ -419,7 +409,7 @@ export class HaStatisticPicker extends LitElement {
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
type: "no_state",
sorting_label: [sortingPrefix, label].join("_"),
search_labels: { label, statisticId },
search_labels: [label, statisticId],
icon_path: mdiShape,
};
}
@@ -485,10 +475,6 @@ export class HaStatisticPicker extends LitElement {
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.helper=${this.helper}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.statistic-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -0,0 +1,270 @@
import { mdiTextureBox } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
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 { computeFloorName } from "../common/entity/compute_floor_name";
import { computeRTL } from "../common/util/compute_rtl";
import {
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";
import "./ha-floor-icon";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import "./ha-icon-button";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
import "./ha-tree-indicator";
const SEPARATOR = "________";
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property({ attribute: false }) public value?: AreaFloorValue;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* List of areas to be excluded.
* @type {Array}
* @attr exclude-areas
*/
@property({ type: Array, attribute: "exclude-areas" })
public excludeAreas?: string[];
/**
* List of floors to be excluded.
* @type {Array}
* @attr exclude-floors
*/
@property({ type: Array, attribute: "exclude-floors" })
public excludeFloors?: string[];
@property({ attribute: false })
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ attribute: false })
public entityFilter?: (entity: HassEntity) => boolean;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
public async open() {
await this.updateComplete;
await this._picker?.open();
}
private _valueRenderer: PickerValueRenderer = (value: string) => {
const item = this._parseValue(value);
const area = item.type === "area" && this.hass.areas[value];
if (area) {
const areaName = computeAreaName(area);
return html`
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<slot name="headline">${areaName}</slot>
`;
}
const floor = item.type === "floor" && this.hass.floors[value];
if (floor) {
const floorName = computeFloorName(floor);
return html`
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
<span slot="headline">${floorName}</span>
`;
}
return html`
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
};
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
item,
{ index },
combobox
) => {
const nextItem = combobox.filteredItems?.[index + 1];
const isLastArea =
!nextItem ||
nextItem.type === "floor" ||
(nextItem.type === "area" && !nextItem.area?.floor_id);
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=${isLastArea}
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 _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
private _getItems = () =>
this._getAreasAndFloorsMemoized(
this.hass.states,
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._formatValue,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeAreas,
this.excludeFloors
);
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
);
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
const [type, id] = value.split(SEPARATOR);
return { id, type: type as "floor" | "area" };
});
protected render(): TemplateResult {
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const value = this.value ? this._formatValue(this.value) : undefined;
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this.hass.localize(
"ui.components.area-picker.no_match"
)}
.placeholder=${placeholder}
.value=${value}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.rowRenderer=${this._rowRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value) {
this._setValue(undefined);
return;
}
const selected = this._parseValue(value);
this._setValue(selected);
}
private _setValue(value?: AreaFloorValue) {
this.value = value;
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-area-floor-picker": HaAreaFloorPicker;
}
}

View File

@@ -13,9 +13,9 @@ import { createAreaRegistryEntry } from "../data/area_registry";
import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
} from "../data/device/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
} from "../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
import type { HomeAssistant, ValueChangedEvent } from "../types";
@@ -30,12 +30,6 @@ import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___";
const SEARCH_KEYS = [
{ name: "search_labels.areaName", weight: 10 },
{ name: "search_labels.aliases", weight: 8 },
{ name: "search_labels.floorName", weight: 6 },
{ name: "search_labels.id", weight: 3 },
];
@customElement("ha-area-picker")
export class HaAreaPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -51,9 +45,6 @@ export class HaAreaPicker extends LitElement {
@property({ type: Boolean, attribute: "no-add" })
public noAdd = false;
@property({ type: Boolean, attribute: "show-label" })
public showLabel = false;
/**
* Show only areas with entities from specific domains.
* @type {Array}
@@ -299,12 +290,13 @@ export class HaAreaPicker extends LitElement {
secondary: floorName,
icon: area.icon || undefined,
icon_path: area.icon ? undefined : mdiTextureBox,
search_labels: {
areaName: areaName || null,
floorName: floorName || null,
id: area.area_id,
aliases: area.aliases.join(" "),
},
sorting_label: areaName,
search_labels: [
areaName,
floorName,
area.area_id,
...area.aliases,
].filter((v): v is string => Boolean(v)),
};
});
@@ -368,16 +360,8 @@ export class HaAreaPicker extends LitElement {
protected render(): TemplateResult {
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const valueRenderer = this._computeValueRenderer(this.hass.areas);
let showLabel = this.showLabel;
if (this.value) {
const area = this.hass.areas[this.value];
if (area) {
const { floor } = getAreaContext(area, this.hass.floors);
showLabel = !floor && this.showLabel;
}
}
const valueRenderer = this._computeValueRenderer(this.hass.areas);
return html`
<ha-generic-picker
@@ -390,16 +374,11 @@ export class HaAreaPicker extends LitElement {
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${placeholder}
.showLabel=${showLabel}
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.area-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -659,7 +659,7 @@ export class HaAssistChat extends LitElement {
--markdown-table-border-color: var(--divider-color);
--markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color);
--markdown-list-indent: 1.15em;
--markdown-list-indent: 1rem;
&:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;

View File

@@ -3,15 +3,15 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import {
STATE_ATTRIBUTES,
STATE_ATTRIBUTES_DOMAIN_CLASS,
} from "../data/entity/entity_attributes";
} from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-attribute-value";
import "./ha-expansion-panel";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@customElement("ha-attributes")
class HaAttributes extends LitElement {

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { ClimateEntity } from "../data/climate";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState, OFF } from "../data/entity/entity";
import { isUnavailableState, OFF } from "../data/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")

View File

@@ -20,17 +20,6 @@ export class HaComboBoxItem extends HaMdListItem {
[slot="start"] {
--state-icon-color: var(--secondary-text-color);
}
[slot="overline"] {
/* mimicing a floating label of mdc-select */
line-height: 1.15rem;
font-size: calc(var(--mdc-typography-subtitle1-font-size, 1rem) * 0.75);
font-weight: var(--mdc-typography-subtitle1-font-weight, 400);
font-family: var(
--mdc-typography-subtitle1-font-family,
var(--mdc-typography-font-family)
);
color: var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6));
}
[slot="headline"] {
line-height: var(--ha-line-height-normal);
font-size: var(--ha-font-size-m);

View File

@@ -9,14 +9,14 @@ import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
import type { Agent } from "../data/conversation";
import { listAgents } from "../data/conversation";
import { getExtendedEntityRegistryEntry } from "../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
const NONE = "__NONE_OPTION__";

View File

@@ -1,9 +1,6 @@
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon";
import { css, type CSSResultGroup, html } from "lit";
import { css, type CSSResultGroup } from "lit";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
/**
* Home Assistant dropdown item component
@@ -17,16 +14,6 @@ import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
*/
@customElement("ha-dropdown-item")
export class HaDropdownItem extends DropdownItem {
protected renderCheckboxIcon() {
return html`
<ha-svg-icon
id="check"
part="checkmark"
.path=${this.checked ? mdiCheckboxMarked : mdiCheckboxBlankOutline}
></ha-svg-icon>
`;
}
static get styles(): CSSResultGroup {
return [
DropdownItem.styles,
@@ -35,10 +22,6 @@ export class HaDropdownItem extends DropdownItem {
min-height: var(--ha-space-10);
}
#check {
visibility: visible;
}
#icon ::slotted(*) {
color: var(--ha-color-on-neutral-normal);
}

View File

@@ -10,8 +10,8 @@ import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import { subscribeLabelRegistry } from "../data/label/label_registry";
import type { LabelRegistryEntry } from "../data/label_registry";
import { subscribeLabelRegistry } from "../data/label_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";

View File

@@ -12,9 +12,9 @@ import { updateAreaRegistryEntry } from "../data/area_registry";
import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
} from "../data/device/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
} from "../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
createFloorRegistryEntry,
getFloorAreaLookup,
@@ -35,12 +35,6 @@ import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___";
const SEARCH_KEYS = [
{ name: "search_labels.floorName", weight: 10 },
{ name: "search_labels.aliases", weight: 8 },
{ name: "search_labels.floor_id", weight: 3 },
];
interface FloorComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
}
@@ -291,11 +285,10 @@ export class HaFloorPicker extends LitElement {
id: floor.floor_id,
primary: floorName,
floor: floor,
search_labels: {
floorName,
floor_id: floor.floor_id,
aliases: floor.aliases.join(" "),
},
sorting_label: floor.level?.toString() || "zzzzz",
search_labels: [floorName, floor.floor_id, ...floor.aliases].filter(
(v): v is string => Boolean(v)
),
};
});
@@ -400,10 +393,6 @@ export class HaFloorPicker extends LitElement {
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.rowRenderer=${this._rowRenderer}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.floor-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -4,10 +4,8 @@ import { mdiPlaylistPlus } from "@mdi/js";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet";
import "./ha-button";
@@ -48,12 +46,8 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "show-label", type: Boolean })
public showLabel = false;
/** To prevent lags, getItems needs to be memoized */
@property({ attribute: false })
public getItems!: (
public getItems?: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
@@ -70,9 +64,6 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ attribute: false })
public searchKeys?: FuseWeightedKey[];
@property({ attribute: false })
public notFoundLabel?: string | ((search: string) => string);
@@ -116,8 +107,6 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -167,13 +156,10 @@ export class HaGenericPicker extends LitElement {
type="button"
class=${this._opened ? "opened" : ""}
compact
.unknown=${this._unknownValue(this.value, this.getItems())}
.unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.label)}
@click=${this.open}
@clear=${this._clear}
.placeholder=${this.placeholder}
.showLabel=${this.showLabel}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
@@ -243,23 +229,10 @@ export class HaGenericPicker extends LitElement {
.sections=${this.sections}
.sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys}
></ha-picker-combo-box>
`;
}
private _unknownValue = memoizeOne(
(value?: string, items?: (PickerComboBoxItem | string)[]) => {
if (value === undefined || value === null || value === "" || !items) {
return false;
}
return !items.some(
(item) => typeof item !== "string" && item.id === value
);
}
);
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
@@ -371,10 +344,7 @@ export class HaGenericPicker extends LitElement {
wa-popover::part(body) {
width: max(var(--body-width), 250px);
max-width: var(
--ha-generic-picker-max-width,
max(var(--body-width), 250px)
);
max-width: max(var(--body-width), 250px);
max-height: 500px;
height: 70vh;
overflow: hidden;

View File

@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { isUnavailableState, OFF } from "../data/entity/entity";
import { isUnavailableState, OFF } from "../data/entity";
import type { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";

View File

@@ -11,12 +11,12 @@ import {
} from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { getLabels, labelComboBoxKeys } from "../data/label/label_picker";
import type { LabelRegistryEntry } from "../data/label_registry";
import {
createLabelRegistryEntry,
getLabels,
subscribeLabelRegistry,
type LabelRegistryEntry,
} from "../data/label/label_registry";
} from "../data/label_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
@@ -237,7 +237,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.searchKeys=${labelComboBoxKeys}
@value-changed=${this._valueChanged}
>
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>

View File

@@ -8,11 +8,11 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import type { LabelRegistryEntry } from "../data/label_registry";
import {
subscribeLabelRegistry,
updateLabelRegistryEntry,
} from "../data/label/label_registry";
} from "../data/label_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
import type { HomeAssistant, ValueChangedEvent } from "../types";

View File

@@ -40,12 +40,14 @@ export const getLanguageOptions = (
return {
id: lang,
primary,
search_labels: [primary],
};
});
} else if (locale) {
options = languages.map((lang) => ({
id: lang,
primary: formatLanguageCode(lang, locale),
search_labels: [formatLanguageCode(lang, locale)],
}));
}
@@ -132,7 +134,6 @@ export class HaLanguagePicker extends LitElement {
.placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
show-label
.value=${value}
.valueRenderer=${this._valueRenderer}
.disabled=${this.disabled}

View File

@@ -74,6 +74,9 @@ export class HaMarkdown extends LitElement {
background-color: var(--markdown-image-background-color);
border-radius: var(--markdown-image-border-radius);
max-width: 100%;
height: auto;
width: auto;
transition: height 0.2s ease-in-out;
}
p:first-child > img:first-child {
vertical-align: top;
@@ -81,7 +84,8 @@ export class HaMarkdown extends LitElement {
p:first-child > img:last-child {
vertical-align: top;
}
ha-markdown-element > :is(ol, ul) {
:host > ul,
:host > ol {
padding-inline-start: var(--markdown-list-indent, revert);
}
li {
@@ -132,18 +136,6 @@ export class HaMarkdown extends LitElement {
border-bottom: none;
margin: var(--ha-space-4) 0;
}
table[role="presentation"] {
--markdown-table-border-collapse: separate;
--markdown-table-border-width: attr(border, 0);
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th {
vertical-align: attr(align, center);
}
td {
vertical-align: attr(align, left);
}
}
table {
border-collapse: var(--markdown-table-border-collapse, collapse);
}
@@ -151,15 +143,14 @@ export class HaMarkdown extends LitElement {
overflow: auto;
}
th {
text-align: var(--markdown-table-text-align, start);
text-align: start;
}
td,
th {
border-width: var(--markdown-table-border-width, 1px);
border-style: var(--markdown-table-border-style, solid);
border-color: var(--markdown-table-border-color, var(--divider-color));
padding-inline: var(--markdown-table-padding-inline, 0.5em);
padding-block: var(--markdown-table-padding-block, 0.25em);
padding: 0.25em 0.5em;
}
blockquote {
border-left: 4px solid var(--divider-color);

View File

@@ -15,10 +15,7 @@ import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import { HaFuse } from "../resources/fuse";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
@@ -29,26 +26,11 @@ import "./ha-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
{
name: "secondary",
weight: 7,
},
{
name: "id",
weight: 3,
},
];
export interface PickerComboBoxItem {
id: string;
primary: string;
secondary?: string;
search_labels?: Record<string, string | null>;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
icon?: string;
@@ -95,13 +77,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property() public value?: string;
@property({ attribute: false })
public searchKeys?: FuseWeightedKey[];
@state() private _listScrolled = false;
@property({ attribute: false })
public getItems!: (
public getItems?: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
@@ -154,8 +133,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@state() private _sectionTitle?: string;
@state() private _valuePinned = true;
private _allItems: (PickerComboBoxItem | string)[] = [];
private _selectedItemIndex = -1;
@@ -217,15 +194,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
.layout=${this.value && this._valuePinned
? {
pin: {
index: this._getInitialSelectedIndex(),
block: "center",
},
}
: undefined}
@unpinned=${this._handleUnpinned}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
@@ -276,42 +244,24 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
}
@eventOptions({ passive: true })
private _handleUnpinned() {
this._valuePinned = false;
}
private _getAdditionalItems = (searchString?: string) =>
this.getAdditionalItems?.(searchString) || [];
private _getItems = () => {
let items = [...this.getItems(this._search, this.selectedSection)];
let items = [
...(this.getItems
? this.getItems(this._search, this.selectedSection)
: []),
];
if (!this.sections?.length) {
items = items.sort((entityA, entityB) => {
const sortLabelA =
typeof entityA === "string" ? entityA : entityA.sorting_label;
const sortLabelB =
typeof entityB === "string" ? entityB : entityB.sorting_label;
if (!sortLabelA || !sortLabelB) {
return 0;
}
if (!sortLabelB) {
return -1;
}
if (!sortLabelA) {
return 1;
}
return caseInsensitiveStringCompare(
sortLabelA,
sortLabelB,
items = items.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
(entityA as PickerComboBoxItem).sorting_label!,
(entityB as PickerComboBoxItem).sorting_label!,
this.hass?.locale.language ?? navigator.language
);
});
)
);
}
if (!items.length) {
@@ -329,9 +279,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
};
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
if (!item) {
return nothing;
}
if (item === "padding") {
return html`<div class="bottom-padding"></div>`;
}
@@ -392,9 +339,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
fireEvent(this, "value-changed", { value: newValue });
};
private _fuseIndex = memoizeOne(
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged = (ev: Event) => {
@@ -410,26 +356,34 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return;
}
const index = this._fuseIndex(
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
const fuse = new HaFuse(
this._allItems as PickerComboBoxItem[],
this.searchKeys
{
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
);
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
this._allItems as PickerComboBoxItem[],
searchString,
this.searchKeys || DEFAULT_SEARCH_KEYS,
(item) => item.id,
index
) as (PickerComboBoxItem | string)[];
const results = fuse.multiTermsSearch(searchString);
let filteredItems = [...this._allItems];
if (!filteredItems.length) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
if (results) {
const items: (PickerComboBoxItem | string)[] = results.map(
(result) => result.item
);
if (!items.length) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
}
const additionalItems = this._getAdditionalItems();
items.push(...additionalItems);
filteredItems = items;
}
const additionalItems = this._getAdditionalItems();
filteredItems.push(...additionalItems);
if (this.searchFn) {
filteredItems = this.searchFn(
searchString,
@@ -636,25 +590,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item?.id;
private _getInitialSelectedIndex() {
if (!this._virtualizerElement || !this.value) {
return 0;
}
const index = this._virtualizerElement.items.findIndex(
(item) =>
typeof item !== "string" &&
(item as PickerComboBoxItem).id === this.value
);
if (index === -1) {
return 0;
}
return index;
}
typeof item === "string" ? item : item.id;
static get styles() {
return [

View File

@@ -1,4 +1,3 @@
import { consume } from "@lit/context";
import { mdiClose, mdiMenuDown } from "@mdi/js";
import {
css,
@@ -8,10 +7,8 @@ import {
type CSSResultGroup,
type TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { localizeContext } from "../data/context";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import type { HaComboBoxItem } from "./ha-combo-box-item";
import "./ha-icon-button";
@@ -36,25 +33,14 @@ export class HaPickerField extends LitElement {
@property() public placeholder?: string;
@property({ type: Boolean, reflect: true }) public unknown = false;
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "show-label", type: Boolean })
public showLabel = false;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
public async focus() {
await this.updateComplete;
await this.item?.focus();
@@ -63,27 +49,18 @@ export class HaPickerField extends LitElement {
protected render() {
const showClearIcon =
!!this.value && !this.required && !this.disabled && !this.hideClearIcon;
const placeholder = this.showLabel
? html`<span slot="overline">${this.placeholder}</span>`
: nothing;
return html`
<ha-combo-box-item .disabled=${this.disabled} type="button" compact>
${this.value
? this.valueRenderer
? html`${placeholder}${this.valueRenderer(this.value)}`
: html`${placeholder}<span slot="headline">${this.value}</span>`
? this.valueRenderer(this.value)
: html`<slot name="headline">${this.value}</slot>`
: html`
<span slot="headline" class="placeholder">
${this.placeholder}
</span>
`}
${this.unknown
? html`<div slot="supporting-text" class="unknown">
${this.unknownItemText ||
this.localize("ui.components.combo-box.unknown_item")}
</div>`
: nothing}
${showClearIcon
? html`
<ha-icon-button
@@ -165,10 +142,6 @@ export class HaPickerField extends LitElement {
background-color: var(--mdc-theme-primary);
}
:host([unknown]) ha-combo-box-item {
background-color: var(--ha-color-fill-warning-quiet-resting);
}
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
@@ -183,10 +156,6 @@ export class HaPickerField extends LitElement {
color: var(--secondary-text-color);
padding: 0 8px;
}
.unknown {
color: var(--ha-color-on-warning-normal);
}
`,
];
}

View File

@@ -7,7 +7,7 @@ import { fullEntitiesContext } from "../../data/context";
import {
subscribeEntityRegistry,
type EntityRegistryEntry,
} from "../../data/entity/entity_registry";
} from "../../data/entity_registry";
import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector";

View File

@@ -4,14 +4,14 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { AreaSelector } from "../../data/selector";
import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { AreaSelector } from "../../data/selector";
import {
filterSelectorDevices,
filterSelectorEntities,

View File

@@ -5,13 +5,13 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import {
filterSelectorDevices,
filterSelectorEntities,

View File

@@ -4,12 +4,12 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { EntitySelector } from "../../data/selector";
import {
computeCreateDomains,
filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker";

View File

@@ -4,14 +4,14 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { FloorSelector } from "../../data/selector";
import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { FloorSelector } from "../../data/selector";
import {
filterSelectorDevices,
filterSelectorEntities,

View File

@@ -7,15 +7,15 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { TargetSelector } from "../../data/selector";
import {
computeCreateDomains,
filterSelectorDevices,
filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-target-picker";

View File

@@ -21,13 +21,6 @@ interface ServiceComboBoxItem extends PickerComboBoxItem {
service_id?: string;
}
const SEARCH_KEYS = [
{ name: "search_labels.name", weight: 10 },
{ name: "search_labels.description", weight: 8 },
{ name: "search_labels.domainName", weight: 6 },
{ name: "search_labels.serviceId", weight: 3 },
];
@customElement("ha-service-picker")
class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -148,10 +141,6 @@ class HaServicePicker extends LitElement {
this.hass.localize,
this.hass.services
)}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.service-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
@@ -205,7 +194,9 @@ class HaServicePicker extends LitElement {
secondary: description,
domain_name: domainName,
service_id: serviceId,
search_labels: { serviceId, domainName, name, description },
search_labels: [serviceId, domainName, name, description].filter(
Boolean
),
sorting_label: serviceId,
});
}

View File

@@ -13,30 +13,19 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import {
areaFloorComboBoxKeys,
getAreasAndFloors,
type AreaFloorValue,
type FloorComboBoxItem,
} from "../data/area_floor_picker";
} from "../data/area_floor";
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
import { labelsContext } from "../data/context";
import {
deviceComboBoxKeys,
getDevices,
type DevicePickerItem,
} from "../data/device/device_picker";
import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
import {
entityComboBoxKeys,
getEntities,
type EntityComboBoxItem,
} from "../data/entity/entity_picker";
import { getDevices, type DevicePickerItem } from "../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
import { domainToName } from "../data/integration";
import { getLabels, labelComboBoxKeys } from "../data/label/label_picker";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
@@ -48,11 +37,7 @@ import {
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { isHelperDomain } from "../panels/config/helpers/const";
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
import {
multiTermSearch,
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import { HaFuse } from "../resources/fuse";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -128,16 +113,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _fuseIndexes = {
area: memoizeOne((states: FloorComboBoxItem[]) =>
this._createFuseIndex(states, areaFloorComboBoxKeys)
this._createFuseIndex(states)
),
entity: memoizeOne((states: EntityComboBoxItem[]) =>
this._createFuseIndex(states, entityComboBoxKeys)
this._createFuseIndex(states)
),
device: memoizeOne((states: DevicePickerItem[]) =>
this._createFuseIndex(states, deviceComboBoxKeys)
this._createFuseIndex(states)
),
label: memoizeOne((states: PickerComboBoxItem[]) =>
this._createFuseIndex(states, labelComboBoxKeys)
this._createFuseIndex(states)
),
};
@@ -149,8 +134,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
}
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
Fuse.createIndex(keys, states);
private _createFuseIndex = (states) =>
Fuse.createIndex(["search_labels"], states);
protected render() {
if (this.addOnTop) {
@@ -743,14 +728,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
: undefined,
undefined,
`entity${SEPARATOR}`
).sort(this._sortBySortingLabel);
);
if (searchTerm) {
entityItems = this._filterGroup(
"entity",
entityItems,
searchTerm,
entityComboBoxKeys
(item: EntityComboBoxItem) =>
item.stateObj?.entity_id === searchTerm
) as EntityComboBoxItem[];
}
@@ -776,15 +762,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
: undefined,
undefined,
`device${SEPARATOR}`
).sort(this._sortBySortingLabel);
);
if (searchTerm) {
deviceItems = this._filterGroup(
"device",
deviceItems,
searchTerm,
deviceComboBoxKeys
);
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
}
if (!filterType && deviceItems.length) {
@@ -818,9 +799,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
areasAndFloors = this._filterGroup(
"area",
areasAndFloors,
searchTerm,
areaFloorComboBoxKeys,
false
searchTerm
) as FloorComboBoxItem[];
}
@@ -862,15 +841,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
entityFilter,
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
`label${SEPARATOR}`
).sort(this._sortBySortingLabel);
);
if (searchTerm) {
labels = this._filterGroup(
"label",
labels,
searchTerm,
labelComboBoxKeys
);
labels = this._filterGroup("label", labels, searchTerm);
}
if (!filterType && labels.length) {
@@ -889,24 +863,40 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type: TargetType,
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
searchTerm: string,
weightedKeys: FuseWeightedKey[],
sort = true
checkExact?: (
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
) => boolean
) {
const fuseIndex = this._fuseIndexes[type](items);
const fuse = new HaFuse(
items,
{
shouldSort: false,
minMatchCharLength: Math.min(searchTerm.length, 2),
},
fuseIndex
);
if (sort) {
return multiTermSortedSearch(
items,
searchTerm,
weightedKeys,
(item) => item.id,
fuseIndex
);
const results = fuse.multiTermsSearch(searchTerm);
let filteredItems = items;
if (results) {
filteredItems = results.map((result) => result.item);
}
return multiTermSearch(items, searchTerm, weightedKeys, fuseIndex, {
ignoreLocation: true,
});
if (!checkExact) {
return filteredItems;
}
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex((item) => checkExact(item));
if (index === -1) {
return filteredItems;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
}
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
@@ -962,7 +952,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
let hasFloor = false;
let rtl = false;
let showEntityId = false;
if (type === "area" || type === "floor") {
item.id = item[type]?.[`${type}_id`];
rtl = computeRTL(this.hass);
hasFloor =
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
@@ -1068,13 +1061,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
term: html`<b>${search}</b>`,
});
private _sortBySortingLabel = (entityA, entityB) =>
caseInsensitiveStringCompare(
(entityA as PickerComboBoxItem).sorting_label!,
(entityB as PickerComboBoxItem).sorting_label!,
this.hass?.locale.language ?? navigator.language
);
static get styles(): CSSResultGroup {
return css`
.add-target-wrapper {

View File

@@ -13,7 +13,6 @@ export class HaToast extends Snackbar {
}
.mdc-snackbar {
z-index: 10;
margin: 8px;
right: calc(8px + var(--safe-area-inset-right));
bottom: calc(8px + var(--safe-area-inset-bottom));

View File

@@ -68,8 +68,8 @@ class HaEntityMarker extends LitElement {
text-align: center;
align-items: center;
box-sizing: border-box;
width: var(--ha-marker-size, 48px);
height: var(--ha-marker-size, 48px);
width: 48px;
height: 48px;
font-size: var(--ha-marker-font-size, var(--ha-font-size-xl));
border-radius: var(--ha-marker-border-radius, 50%);
border: 1px solid var(--ha-marker-color, var(--primary-color));

View File

@@ -560,11 +560,10 @@ export class HaMap extends ReactiveElement {
radius,
});
const markerIconSize = this._getMarkerSize(computedStyles) / 2;
const marker = new DecoratedMarker([latitude, longitude], circle, {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [markerIconSize, markerIconSize],
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
@@ -619,11 +618,10 @@ export class HaMap extends ReactiveElement {
}
// create marker with the icon
const markerSize = this._getMarkerSize(computedStyles);
const marker = new DecoratedMarker([latitude, longitude], undefined, {
icon: Leaflet.divIcon({
html: entityMarker,
iconSize: [markerSize, markerSize],
iconSize: [48, 48],
className: "",
}),
title: title,
@@ -659,13 +657,6 @@ export class HaMap extends ReactiveElement {
this._mapZones.forEach((marker) => map.addLayer(marker));
}
private _getMarkerSize(computedStyles: CSSStyleDeclaration): number {
const markerSizeVarValue =
computedStyles.getPropertyValue("--ha-marker-size");
const parsed = parseFloat(markerSizeVarValue);
return Number.isNaN(parsed) ? 48 : parsed;
}
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
this._resizeObserver = new ResizeObserver(() => {
@@ -748,19 +739,14 @@ export class HaMap extends ReactiveElement {
text-align: center;
}
ha-icon {
--mdc-icon-size: calc(var(--ha-marker-size, 48px) / 2);
}
.marker-cluster div {
background-clip: padding-box;
background-color: var(--primary-color);
border: 3px solid rgba(var(--rgb-primary-color), 0.2);
width: calc(var(--ha-marker-size, 48px) * 0.667);
height: calc(var(--ha-marker-size, 48px) * 0.667);
border-radius: 50%;
width: 32px;
height: 32px;
border-radius: var(--ha-border-radius-2xl);
text-align: center;
align-content: center;
color: var(--text-primary-color);
font-size: var(--ha-font-size-m);
}

View File

@@ -7,7 +7,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { supportsFeature } from "../../common/entity/supports-feature";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { extractApiErrorMessage } from "../../data/hassio/common";
import {
type MediaPlayerEntity,

View File

@@ -17,7 +17,7 @@ import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { slugify } from "../../common/string/slugify";
import { debounce } from "../../common/util/debounce";
import { isUnavailableState } from "../../data/entity/entity";
import { isUnavailableState } from "../../data/entity";
import type {
MediaPickedEvent,
MediaPlayerBrowseAction,

View File

@@ -1,5 +1,5 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity/entity";
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
import type { TargetType } from "../../../data/target";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";

View File

@@ -1,6 +1,6 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { TargetType, TargetTypeFloorless } from "../../data/target";
import type { HomeAssistant } from "../../types";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";

View File

@@ -23,11 +23,11 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area_registry";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,

View File

@@ -23,7 +23,7 @@ import { slugify } from "../../common/string/slugify";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { TargetType } from "../../data/target";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";

View File

@@ -17,7 +17,6 @@ export class HaTraceBlueprintConfig extends LitElement {
return html`
<ha-code-editor
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>

View File

@@ -17,7 +17,6 @@ export class HaTraceConfig extends LitElement {
return html`
<ha-code-editor
.value=${dump(this.trace.config).trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>

View File

@@ -1,21 +1,14 @@
import { consume } from "@lit/context";
import { dump } from "js-yaml";
import { consume } from "@lit/context";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import "../ha-code-editor";
import "../ha-icon-button";
import "./hat-logbook-note";
import type { LogbookEntry } from "../../data/logbook";
import { describeAction } from "../../data/script_i18n";
import type {
ActionTraceStep,
ChooseActionTraceStep,
@@ -23,12 +16,19 @@ import type {
} from "../../data/trace";
import { getDataFromPath } from "../../data/trace";
import "../../panels/logbook/ha-logbook-renderer";
import type { HomeAssistant } from "../../types";
import "../ha-code-editor";
import "../ha-icon-button";
import "./hat-logbook-note";
import type { NodeInfo } from "./hat-script-graph";
import { traceTabStyles } from "./trace-tab-styles";
import type { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { describeAction } from "../../data/script_i18n";
const TRACE_PATH_TABS = [
"step_config",
@@ -278,7 +278,6 @@ export class HaTracePathDetails extends LitElement {
return config
? html`<ha-code-editor
.value=${dump(config).trimEnd()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>`

View File

@@ -20,14 +20,14 @@ import {
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { LogbookEntry } from "../../data/logbook";
import type {
ChooseAction,
IfAction,
Option,
IfAction,
ParallelAction,
RepeatAction,
SequenceAction,

View File

@@ -17,12 +17,6 @@ interface UserComboBoxItem extends PickerComboBoxItem {
user?: User;
}
const SEARCH_KEYS = [
{ name: "primary", weight: 10 },
{ name: "search_labels.username", weight: 6 },
{ name: "id", weight: 3 },
];
@customElement("ha-user-picker")
class HaUserPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -115,7 +109,9 @@ class HaUserPicker extends LitElement {
id: user.id,
primary: user.name,
domain_name: user.name,
search_labels: { username: user.username },
search_labels: [user.name, user.id, user.username].filter(
Boolean
) as string[],
sorting_label: user.name,
user,
}));
@@ -138,10 +134,6 @@ class HaUserPicker extends LitElement {
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.rowRenderer=${this._rowRenderer}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.user-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -13,7 +13,7 @@ import type {
import { supportsFeature } from "../common/entity/supports-feature";
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
import type { HomeAssistant } from "../types";
import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
import { getExtendedEntityRegistryEntry } from "./entity_registry";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";

View File

@@ -4,16 +4,15 @@ import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
type DeviceRegistryEntry,
} from "./device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";
import type { EntityRegistryDisplayEntry } from "./entity/entity_registry";
} from "./device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
import type { FloorRegistryEntry } from "./floor_registry";
export interface FloorComboBoxItem extends PickerComboBoxItem {
@@ -27,8 +26,7 @@ export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem {
id: undefined;
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
@@ -100,29 +98,6 @@ export const getAreasAndFloors = (
excludeFloors
) as FloorComboBoxItem[];
export const areaFloorComboBoxKeys: FuseWeightedKey[] = [
{
name: "search_labels.name",
weight: 10,
},
{
name: "search_labels.aliases",
weight: 8,
},
{
name: "search_labels.floorName",
weight: 6,
},
{
name: "search_labels.relatedAreas",
weight: 4,
},
{
name: "search_labels.id",
weight: 3,
},
];
const getAreasAndFloorsItems = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -329,12 +304,12 @@ const getAreasAndFloorsItems = (
primary: floorName,
floor: floor,
icon: floor.icon || undefined,
search_labels: {
id: floor.floor_id,
name: floorName || null,
aliases: floor.aliases.join(", ") || null,
relatedAreas: areaSearchLabels.join(" ") || null,
},
search_labels: [
floor.floor_id,
floorName,
...floor.aliases,
...areaSearchLabels,
],
};
items.push(floorItem);
@@ -347,12 +322,11 @@ const getAreasAndFloorsItems = (
primary: areaName || area.area_id,
area: area,
icon: area.icon || undefined,
search_labels: {
id: area.area_id,
name: areaName || null,
aliases: area.aliases.join(", ") || null,
floorName: floorName || null,
},
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
};
});
@@ -365,24 +339,19 @@ const getAreasAndFloorsItems = (
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(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_id,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: {
id: area.area_id,
name: areaName || null,
aliases: area.aliases.join(", ") || null,
},
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && unassignedAreaItems.length) {
items.push({
id: undefined,
areas: unassignedAreaItems,
} as UnassignedAreasFloorComboBoxItem);
} else {

View File

@@ -1,9 +1,9 @@
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device/device_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity/entity_registry";
} from "./entity_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { supportsFeature } from "../common/entity/supports-feature";
import { UNAVAILABLE } from "./entity";
export const enum AssistSatelliteEntityFeature {
ANNOUNCE = 1,

View File

@@ -12,10 +12,7 @@ import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
import type { ConditionDescription } from "./condition";
import { CONDITION_BUILDING_BLOCKS } from "./condition";
import type {
DeviceCondition,
DeviceTrigger,
} from "./device/device_automation";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, Field, MODES } from "./script";
import { migrateAutomationAction } from "./script";
import type { TriggerDescription } from "./trigger";

View File

@@ -26,15 +26,12 @@ import type {
Trigger,
} from "./automation";
import { getConditionDomain, getConditionObjectId } from "./condition";
import type {
DeviceCondition,
DeviceTrigger,
} from "./device/device_automation";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import {
localizeDeviceAutomationCondition,
localizeDeviceAutomationTrigger,
} from "./device/device_automation";
import type { EntityRegistryEntry } from "./entity/entity_registry";
} from "./device_automation";
import type { EntityRegistryEntry } from "./entity_registry";
import type { FrontendLocaleData } from "./translation";
import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger";

View File

@@ -2,7 +2,7 @@ import { getColorByIndex } from "../common/color/colors";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types";
import { isUnavailableState } from "./entity/entity";
import { isUnavailableState } from "./entity";
export interface Calendar {
entity_id: string;

View File

@@ -1,8 +1,8 @@
import { createContext } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import type { LabelRegistryEntry } from "./label/label_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import type { LabelRegistryEntry } from "./label_registry";
export const connectionContext =
createContext<HomeAssistant["connection"]>("connection");

View File

@@ -5,7 +5,7 @@ import type {
import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { UNAVAILABLE } from "./entity";
export const enum CoverEntityFeature {
OPEN = 1,

View File

@@ -1,182 +0,0 @@
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
import type { HomeAssistant } from "../../types";
import type { ConfigEntry } from "../config_entries";
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
import { domainToName } from "../integration";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
} from "./device_registry";
export interface DevicePickerItem extends PickerComboBoxItem {
domain?: string;
domain_name?: string;
}
export const deviceComboBoxKeys: FuseWeightedKey[] = [
{
name: "search_labels.deviceName",
weight: 10,
},
{
name: "search_labels.areaName",
weight: 8,
},
{
name: "search_labels.domainName",
weight: 4,
},
{
name: "search_labels.domain",
weight: 4,
},
];
export const getDevices = (
hass: HomeAssistant,
configEntryLookup: Record<string, ConfigEntry>,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[],
value?: string,
idPrefix = ""
): DevicePickerItem[] => {
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
}
let inputDevices = devices.filter(
(device) => device.id === value || !device.disabled_by
);
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDevices) {
inputDevices = inputDevices.filter(
(device) => !excludeDevices!.includes(device.id)
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
if (entityFilter) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return devEntities.some((entity) => {
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
}
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
// We always want to include the device of the current value
device.id === value || deviceFilter!(device)
);
}
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
const deviceName = computeDeviceNameDisplay(
device,
hass,
deviceEntityLookup[device.id]
);
const { area } = getDeviceContext(device, hass);
const areaName = area ? computeAreaName(area) : undefined;
const configEntry = device.primary_config_entry
? configEntryLookup?.[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
const primary =
deviceName || hass.localize("ui.components.device-picker.unnamed_device");
return {
id: `${idPrefix}${device.id}`,
label: "",
primary,
secondary: areaName,
domain: configEntry?.domain,
domain_name: domainName,
search_labels: {
deviceName,
areaName: areaName || null,
domain: domain || null,
domainName: domainName || null,
},
sorting_label: [primary, areaName, domainName].filter(Boolean).join("_"),
};
});
return outputDevices;
};

View File

@@ -1,169 +0,0 @@
import { computeStateName } from "../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { HomeAssistant } from "../../types";
import type { ConfigEntry } from "../config_entries";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../entity/entity_registry";
import type { EntitySources } from "../entity/entity_sources";
import type { RegistryEntry } from "../registry";
export {
fetchDeviceRegistry,
subscribeDeviceRegistry,
} from "../ws-device_registry";
export interface DeviceRegistryEntry extends RegistryEntry {
id: string;
config_entries: string[];
config_entries_subentries: Record<string, (string | null)[]>;
connections: [string, string][];
identifiers: [string, string][];
manufacturer: string | null;
model: string | null;
model_id: string | null;
name: string | null;
labels: string[];
sw_version: string | null;
hw_version: string | null;
serial_number: string | null;
via_device_id: string | null;
area_id: string | null;
name_by_user: string | null;
entry_type: "service" | null;
disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null;
primary_config_entry: string | null;
}
export type DeviceEntityDisplayLookup = Record<
string,
EntityRegistryDisplayEntry[]
>;
export type DeviceEntityLookup<
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
| EntityRegistryEntry
| EntityRegistryDisplayEntry,
> = Record<string, T[]>;
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
name_by_user?: string | null;
disabled_by?: string | null;
labels?: string[];
}
export const fallbackDeviceName = (
hass: HomeAssistant,
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hass.states[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
};
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
deviceId: string,
updates: Partial<DeviceRegistryEntryMutableParams>
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/update",
device_id: deviceId,
...updates,
});
export const removeConfigEntryFromDevice = (
hass: HomeAssistant,
deviceId: string,
configEntryId: string
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/remove_config_entry",
device_id: deviceId,
config_entry_id: configEntryId,
});
export const sortDeviceRegistryByName = (
entries: DeviceRegistryEntry[],
language: string
) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
return deviceEntityLookup;
};
export const getDeviceEntityDisplayLookup = (
entities: EntityRegistryDisplayEntry[]
): DeviceEntityDisplayLookup => {
const deviceEntityLookup: DeviceEntityDisplayLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
return deviceEntityLookup;
};
export const getDeviceIntegrationLookup = (
entitySources: EntitySources,
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
devices?: DeviceRegistryEntry[],
configEntries?: ConfigEntry[]
): Record<string, Set<string>> => {
const deviceIntegrations: Record<string, Set<string>> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain || entity.device_id === null) {
continue;
}
deviceIntegrations[entity.device_id!] =
deviceIntegrations[entity.device_id!] || new Set<string>();
deviceIntegrations[entity.device_id!].add(source.domain);
}
// Lookup devices that have no entities
if (devices && configEntries) {
for (const device of devices) {
for (const config_entry_id of device.config_entries) {
const entry = configEntries.find((e) => e.entry_id === config_entry_id);
if (entry?.domain) {
deviceIntegrations[device.id] =
deviceIntegrations[device.id] || new Set<string>();
deviceIntegrations[device.id].add(entry.domain);
}
}
}
}
return deviceIntegrations;
};

View File

@@ -1,14 +1,14 @@
import { computeStateName } from "../../common/entity/compute_state_name";
import type { HaFormSchema } from "../../components/ha-form/types";
import type { HomeAssistant } from "../../types";
import type { BaseTrigger } from "../automation";
import { migrateAutomationTrigger } from "../automation";
import type { EntityRegistryEntry } from "../entity/entity_registry";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HaFormSchema } from "../components/ha-form/types";
import type { HomeAssistant } from "../types";
import type { BaseTrigger } from "./automation";
import { migrateAutomationTrigger } from "./automation";
import type { EntityRegistryEntry } from "./entity_registry";
import {
computeEntityRegistryName,
entityRegistryByEntityId,
entityRegistryById,
} from "../entity/entity_registry";
} from "./entity_registry";
export interface DeviceAutomation {
alias?: string;

322
src/data/device_registry.ts Normal file
View File

@@ -0,0 +1,322 @@
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { getDeviceContext } from "../common/entity/context/get_device_context";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { ConfigEntry } from "./config_entries";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { EntitySources } from "./entity_sources";
import { domainToName } from "./integration";
import type { RegistryEntry } from "./registry";
export {
fetchDeviceRegistry,
subscribeDeviceRegistry,
} from "./ws-device_registry";
export interface DeviceRegistryEntry extends RegistryEntry {
id: string;
config_entries: string[];
config_entries_subentries: Record<string, (string | null)[]>;
connections: [string, string][];
identifiers: [string, string][];
manufacturer: string | null;
model: string | null;
model_id: string | null;
name: string | null;
labels: string[];
sw_version: string | null;
hw_version: string | null;
serial_number: string | null;
via_device_id: string | null;
area_id: string | null;
name_by_user: string | null;
entry_type: "service" | null;
disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null;
primary_config_entry: string | null;
}
export type DeviceEntityDisplayLookup = Record<
string,
EntityRegistryDisplayEntry[]
>;
export type DeviceEntityLookup<
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
| EntityRegistryEntry
| EntityRegistryDisplayEntry,
> = Record<string, T[]>;
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
name_by_user?: string | null;
disabled_by?: string | null;
labels?: string[];
}
export const fallbackDeviceName = (
hass: HomeAssistant,
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hass.states[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
};
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
deviceId: string,
updates: Partial<DeviceRegistryEntryMutableParams>
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/update",
device_id: deviceId,
...updates,
});
export const removeConfigEntryFromDevice = (
hass: HomeAssistant,
deviceId: string,
configEntryId: string
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/remove_config_entry",
device_id: deviceId,
config_entry_id: configEntryId,
});
export const sortDeviceRegistryByName = (
entries: DeviceRegistryEntry[],
language: string
) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
return deviceEntityLookup;
};
export const getDeviceEntityDisplayLookup = (
entities: EntityRegistryDisplayEntry[]
): DeviceEntityDisplayLookup => {
const deviceEntityLookup: DeviceEntityDisplayLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
return deviceEntityLookup;
};
export const getDeviceIntegrationLookup = (
entitySources: EntitySources,
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
devices?: DeviceRegistryEntry[],
configEntries?: ConfigEntry[]
): Record<string, Set<string>> => {
const deviceIntegrations: Record<string, Set<string>> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain || entity.device_id === null) {
continue;
}
deviceIntegrations[entity.device_id!] =
deviceIntegrations[entity.device_id!] || new Set<string>();
deviceIntegrations[entity.device_id!].add(source.domain);
}
// Lookup devices that have no entities
if (devices && configEntries) {
for (const device of devices) {
for (const config_entry_id of device.config_entries) {
const entry = configEntries.find((e) => e.entry_id === config_entry_id);
if (entry?.domain) {
deviceIntegrations[device.id] =
deviceIntegrations[device.id] || new Set<string>();
deviceIntegrations[device.id].add(entry.domain);
}
}
}
}
return deviceIntegrations;
};
export interface DevicePickerItem extends PickerComboBoxItem {
domain?: string;
domain_name?: string;
}
export const getDevices = (
hass: HomeAssistant,
configEntryLookup: Record<string, ConfigEntry>,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[],
value?: string,
idPrefix = ""
): DevicePickerItem[] => {
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
}
let inputDevices = devices.filter(
(device) => device.id === value || !device.disabled_by
);
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDevices) {
inputDevices = inputDevices.filter(
(device) => !excludeDevices!.includes(device.id)
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
if (entityFilter) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return devEntities.some((entity) => {
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
}
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
// We always want to include the device of the current value
device.id === value || deviceFilter!(device)
);
}
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
const deviceName = computeDeviceNameDisplay(
device,
hass,
deviceEntityLookup[device.id]
);
const { area } = getDeviceContext(device, hass);
const areaName = area ? computeAreaName(area) : undefined;
const configEntry = device.primary_config_entry
? configEntryLookup?.[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
return {
id: `${idPrefix}${device.id}`,
label: "",
primary:
deviceName ||
hass.localize("ui.components.device-picker.unnamed_device"),
secondary: areaName,
domain: configEntry?.domain,
domain_name: domainName,
search_labels: [deviceName, areaName, domain, domainName].filter(
Boolean
) as string[],
sorting_label: deviceName || "zzz",
};
});
return outputDevices;
};

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import { arrayLiteralIncludes } from "../common/array/literal-includes";
export const UNAVAILABLE = "unavailable";
export const UNKNOWN = "unknown";

View File

@@ -1,155 +0,0 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
import type { HomeAssistant } from "../../types";
import { domainToName } from "../integration";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
export interface EntityComboBoxItem extends PickerComboBoxItem {
domain_name?: string;
stateObj?: HassEntity;
}
export const entityComboBoxKeys: FuseWeightedKey[] = [
{
name: "search_labels.entityName",
weight: 10,
},
{
name: "search_labels.friendlyName",
weight: 8,
},
{
name: "search_labels.deviceName",
weight: 7,
},
{
name: "search_labels.areaName",
weight: 6,
},
{
name: "search_labels.domainName",
weight: 6,
},
{
name: "search_labels.entityId",
weight: 3,
},
];
export const getEntities = (
hass: HomeAssistant,
includeDomains?: string[],
excludeDomains?: string[],
entityFilter?: HaEntityPickerEntityFilterFunc,
includeDeviceClasses?: string[],
includeUnitOfMeasurement?: string[],
includeEntities?: string[],
excludeEntities?: string[],
value?: string,
idPrefix = ""
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
items = entityIds.map<EntityComboBoxItem>((entityId) => {
const stateObj = hass.states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const domainName = domainToName(hass.localize, computeDomain(entityId));
const isRTL = computeRTL(hass);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return {
id: `${idPrefix}${entityId}`,
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [primary, secondary].filter(Boolean).join("_"),
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
areaName: areaName || null,
domainName: domainName || null,
friendlyName: friendlyName || null,
entityId: entityId,
},
stateObj: stateObj,
};
});
if (includeDeviceClasses) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(item.stateObj.attributes.device_class))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === value || (item.stateObj && entityFilter!(item.stateObj))
);
}
return items;
};

View File

@@ -1,5 +1,5 @@
import { formatDurationDigital } from "../../common/datetime/format_duration";
import type { FrontendLocaleData } from "../translation";
import { formatDurationDigital } from "../common/datetime/format_duration";
import type { FrontendLocaleData } from "./translation";
// These attributes are hidden from the more-info window for all entities.
export const STATE_ATTRIBUTES = [

Some files were not shown because too many files have changed in this diff Show More