Compare commits

..

2 Commits

Author SHA1 Message Date
Bruno Pantaleão 959c440844 Remove inline comments 2026-07-16 13:43:33 +02:00
Bruno Pantaleão be440b0dd3 Add hasLoader external bus config for companion app loaders
When a companion app reports hasLoader, keep the frontend launch screen
blank so the app can render its own loading screen.
2026-07-16 13:41:59 +02:00
41 changed files with 451 additions and 1687 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
if: github.event_name != 'push' || github.ref_name != 'master'
environment:
name: Demo Development
url: ${{ steps.deploy.outputs.unique_deploy_url }}
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+4 -4
View File
@@ -102,7 +102,7 @@
"dialog-polyfill": "0.5.6",
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.5.0",
"fuse.js": "7.4.2",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
@@ -146,13 +146,13 @@
"@bundle-stats/plugin-webpack-filter": "4.22.2",
"@eslint/js": "10.0.1",
"@html-eslint/eslint-plugin": "0.64.0",
"@lokalise/node-api": "16.1.0",
"@lokalise/node-api": "16.0.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.61.1",
"@rsdoctor/rspack-plugin": "1.5.18",
"@rspack/core": "2.1.4",
"@rspack/core": "2.1.3",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -211,7 +211,7 @@
"terser-webpack-plugin": "5.6.1",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.64.0",
"typescript-eslint": "8.63.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.10",
"webpack-stats-plugin": "1.1.3",
+3 -52
View File
@@ -1,53 +1,4 @@
// Unanchored regex fragments, shared as the single source of truth for both
// the boolean validators below and the HTML `pattern` attribute (the browser
// anchors a pattern as `^(?:…)$`).
const IPV4 =
"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
const regexp =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const IPV6 =
"(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))";
// CIDR prefix lengths: 0-32 for IPv4, 0-128 for IPv6.
const IPV4_PREFIX = "(?:3[0-2]|[12]?[0-9])";
const IPV6_PREFIX = "(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])";
// IPv4 or IPv6 address.
export const IP_ADDRESS_PATTERN = `${IPV4}|${IPV6}`;
// IPv4/IPv6 address, optionally with a CIDR prefix (network).
export const IP_ADDRESS_OR_NETWORK_PATTERN = `${IPV4}(?:/${IPV4_PREFIX})?|${IPV6}(?:/${IPV6_PREFIX})?`;
const anchored = (pattern: string): RegExp => new RegExp(`^(?:${pattern})$`);
const ipv4Regexp = anchored(IPV4);
const ipv6Regexp = anchored(IPV6);
// IPv4 address, e.g. 192.168.1.10
export const isIPAddress = (input: string): boolean => ipv4Regexp.test(input);
// IPv6 address, e.g. fe80::85d:e82c:9446:7995
export const isIPv6Address = (input: string): boolean => ipv6Regexp.test(input);
// IPv4 or IPv6 address
export const isIPAddressV4OrV6 = (input: string): boolean =>
isIPAddress(input) || isIPv6Address(input);
// IP network in CIDR notation, e.g. 192.168.1.0/24 or fd00::/8
export const isIPNetwork = (input: string): boolean => {
const parts = input.split("/");
if (parts.length !== 2) {
return false;
}
const [address, prefix] = parts;
if (!/^\d{1,3}$/.test(prefix)) {
return false;
}
const prefixLength = Number(prefix);
if (isIPAddress(address)) {
return prefixLength >= 0 && prefixLength <= 32;
}
if (isIPv6Address(address)) {
return prefixLength >= 0 && prefixLength <= 128;
}
return false;
};
export const isIPAddress = (input: string): boolean => regexp.test(input);
+3 -42
View File
@@ -73,40 +73,6 @@ interface ClockDatePartSectionData {
items: PickerComboBoxItem[];
}
/**
* Filters out sections whose group already has a value in `value`, so only
* one non-separator value per group can be selected in the picker. The whole
* group of the item at `excludeIndex` (the one being edited) stays
* selectable, even if that group has more than one value in `value` (e.g. a
* pre-existing config authored outside the picker via YAML).
* The separator group is always kept.
*/
export const getAvailableClockDatePartSections = (
sections: ClockDatePartSectionData[],
value: string[],
excludeIndex?: number
): ClockDatePartSectionData[] => {
const editedItem = excludeIndex != null ? value[excludeIndex] : undefined;
const editedSection = editedItem
? getClockDatePartSection(editedItem as ClockCardDatePart)
: undefined;
const usedSections = new Set<ClockDatePartSection>();
value.forEach((item) => {
const section = getClockDatePartSection(item as ClockCardDatePart);
if (section !== "separator" && section !== editedSection) {
usedSections.add(section);
}
});
return sections.filter(
(sectionData) =>
sectionData.id === "separator" || !usedSections.has(sectionData.id)
);
};
interface ClockDatePartValueItem {
key: string;
item: string;
@@ -137,17 +103,12 @@ export class HaClockDateFormatPicker extends LitElement {
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
@state() private _editIndex?: number;
private _editIndex?: number;
protected render() {
const value = this._value;
const valueItems = this._getValueItems(value);
const sections = this._buildSections();
const pickerSections = getAvailableClockDatePartSections(
sections,
value,
this._editIndex
);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
@@ -156,8 +117,8 @@ export class HaClockDateFormatPicker extends LitElement {
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${this._getPickerValue()}
.sections=${this._getSectionHeaders(pickerSections)}
.getItems=${this._getItems(pickerSections)}
.sections=${this._getSectionHeaders(sections)}
.getItems=${this._getItems(sections)}
@value-changed=${this._pickerValueChanged}
>
<div slot="field" class="container">
-73
View File
@@ -1,73 +0,0 @@
import type {
HaFormBaseSchema,
HaFormCondition,
HaFormDataContainer,
HaFormFieldCondition,
HaFormSchema,
} from "./types";
const isEmpty = (value: unknown): boolean =>
value === undefined || value === null || value === "";
const matchFieldCondition = (
condition: HaFormFieldCondition,
data: HaFormDataContainer | undefined
): boolean => {
const actual = data?.[condition.field];
switch (condition.operator ?? "eq") {
case "eq":
return actual === condition.value;
case "not_eq":
return actual !== condition.value;
case "in":
return (
Array.isArray(condition.value) &&
condition.value.includes(actual as any)
);
case "not_in":
return (
Array.isArray(condition.value) &&
!condition.value.includes(actual as any)
);
case "exists":
return !isEmpty(actual);
case "not_exists":
return isEmpty(actual);
default:
return false;
}
};
export const evaluateCondition = (
condition: HaFormCondition,
data: HaFormDataContainer | undefined
): boolean => {
if ("condition" in condition) {
switch (condition.condition) {
case "and":
return condition.conditions.every((c) => evaluateCondition(c, data));
case "or":
return condition.conditions.some((c) => evaluateCondition(c, data));
case "not":
return !condition.conditions.some((c) => evaluateCondition(c, data));
default:
return false;
}
}
return matchFieldCondition(condition, data);
};
export const isFieldHidden = (
schema: HaFormSchema,
data: HaFormDataContainer | undefined
): boolean => {
const { hidden } = schema as HaFormBaseSchema;
if (!hidden) {
return false;
}
if (hidden === true) {
return true;
}
const conditions = Array.isArray(hidden) ? hidden : [hidden];
return conditions.every((condition) => evaluateCondition(condition, data));
};
+13 -16
View File
@@ -2,7 +2,6 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import { isFieldHidden } from "./conditions";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
@@ -69,21 +68,19 @@ export class HaFormGrid extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
${this.schema.schema
.filter((item) => !isFieldHidden(item, this.data))
.map(
(item) => html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}
${this.schema.schema.map(
(item) => html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}
`;
}
+2 -11
View File
@@ -1,12 +1,11 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-selector/ha-selector";
import { isFieldHidden } from "./conditions";
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
const LOAD_ELEMENTS = {
@@ -99,11 +98,7 @@ export class HaForm extends LitElement implements HaFormElement {
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
const visibleSchema = this.schema.filter(
(item) => !isFieldHidden(item, this.data)
);
visibleSchema.forEach((item, index) => {
this.schema.forEach((item, index) => {
const element = elements[index];
if (!element) {
return;
@@ -169,10 +164,6 @@ export class HaForm extends LitElement implements HaFormElement {
: ""
}
${this.schema.map((item) => {
if (isFieldHidden(item, this.data)) {
return nothing;
}
const error = getError(this.error, item);
const warning = getWarning(this.warning, item);
-33
View File
@@ -22,9 +22,6 @@ export interface HaFormBaseSchema {
default?: HaFormData;
required?: boolean;
disabled?: boolean;
// Field is hidden while the condition holds. Serializable so it can be
// shared with the backend and other renderers.
hidden?: boolean | HaFormCondition | HaFormCondition[];
description?: {
suffix?: string;
// This value will be set initially when form is loaded
@@ -33,36 +30,6 @@ export interface HaFormBaseSchema {
context?: Record<string, string>;
}
export type HaFormConditionOperator =
"eq" | "not_eq" | "in" | "not_in" | "exists" | "not_exists";
export interface HaFormFieldCondition {
field: string;
operator?: HaFormConditionOperator;
value?: HaFormData | readonly HaFormData[];
}
export interface HaFormAndCondition {
condition: "and";
conditions: readonly HaFormCondition[];
}
export interface HaFormOrCondition {
condition: "or";
conditions: readonly HaFormCondition[];
}
export interface HaFormNotCondition {
condition: "not";
conditions: readonly HaFormCondition[];
}
export type HaFormCondition =
| HaFormFieldCondition
| HaFormAndCondition
| HaFormOrCondition
| HaFormNotCondition;
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
flatten?: boolean;
@@ -133,13 +133,7 @@ export class HaAreaSelector extends LitElement {
}
return ensureArray(this.selector.area.entity).some((filter) =>
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
filterSelectorEntities(filter, entity, this._entitySources)
);
};
@@ -147,13 +147,7 @@ export class HaDeviceSelector extends LitElement {
private _filterEntities = (entity: HassEntity): boolean =>
ensureArray(this.selector.device!.entity).some((filter) =>
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
filterSelectorEntities(filter, entity, this._entitySources)
);
}
@@ -2,12 +2,8 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
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 { fireEvent } from "../../common/dom/fire_event";
import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { EntitySelector } from "../../data/selector";
@@ -27,8 +23,6 @@ export class HaEntitySelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
@property() public value?: any;
@property() public label?: string;
@@ -43,38 +37,12 @@ export class HaEntitySelector extends LitElement {
@state() private _createDomains: string[] | undefined;
private _deviceIntegrationLookup = memoizeOne(
(
entitySources: EntitySources,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
configEntries?: ConfigEntry[]
) =>
getDeviceIntegrationLookup(
entitySources,
Object.values(entities),
Object.values(devices),
configEntries
)
);
// Which async data the current filter needs to be evaluated: a top-level or
// device `integration` filter needs entity sources, and a `device.integration`
// filter additionally needs config entries (the device integration lookup is
// built from both).
private _dataNeeds = memoizeOne((selector: EntitySelector) => {
const filters = selector.entity?.filter
? ensureArray(selector.entity.filter)
: [];
return {
entitySources: filters.some(
(f) => f.integration || f.device?.integration
),
configEntries: filters.some((f) => f.device?.integration),
};
});
private _fetchedConfigEntries = false;
private _hasIntegration(selector: EntitySelector) {
return (
selector.entity?.filter &&
ensureArray(selector.entity.filter).some((filter) => filter.integration)
);
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.get("selector") && this.value !== undefined) {
@@ -89,11 +57,7 @@ export class HaEntitySelector extends LitElement {
}
protected render() {
const needs = this._dataNeeds(this.selector);
if (
(needs.entitySources && !this._entitySources) ||
(needs.configEntries && !this._configEntries)
) {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return nothing;
}
@@ -132,37 +96,15 @@ export class HaEntitySelector extends LitElement {
protected updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps);
// The connection changed (e.g. reconnect); refetch config entries.
const oldHass = changedProps.get("hass");
if (oldHass && oldHass.connection !== this.hass.connection) {
this._fetchedConfigEntries = false;
this._configEntries = undefined;
}
const needs = this._dataNeeds(this.selector);
if (needs.entitySources && !this._entitySources) {
if (
changedProps.has("selector") &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
if (needs.configEntries && !this._fetchedConfigEntries) {
this._fetchedConfigEntries = true;
getConfigEntries(this.hass)
.then((entries) => {
this._configEntries = entries;
})
.catch(() => {
// Fall back to no entries so the picker still renders. We keep
// `_fetchedConfigEntries` set so the failed fetch is not retried on
// every re-render; the connection-change handler above retries on
// reconnect.
this._configEntries = [];
});
}
if (changedProps.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
@@ -172,25 +114,8 @@ export class HaEntitySelector extends LitElement {
if (!this.selector?.entity?.filter) {
return true;
}
const deviceIntegrationLookup =
this._entitySources && this._dataNeeds(this.selector).configEntries
? this._deviceIntegrationLookup(
this._entitySources,
this.hass.entities,
this.hass.devices,
this._configEntries
)
: undefined;
return ensureArray(this.selector.entity.filter).some((filter) =>
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices,
deviceIntegrationLookup
)
filterSelectorEntities(filter, entity, this._entitySources)
);
};
}
@@ -133,13 +133,7 @@ export class HaFloorSelector extends LitElement {
}
return ensureArray(this.selector.floor.entity).some((filter) =>
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
filterSelectorEntities(filter, entity, this._entitySources)
);
};
@@ -93,13 +93,7 @@ export class HaTargetSelector extends LitElement {
}
return ensureArray(this.selector.target.entity).some((filter) =>
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
filterSelectorEntities(filter, entity, this._entitySources)
);
};
+1 -10
View File
@@ -28,10 +28,6 @@ export class HaTextSelector extends LitElement {
@query("ha-input, ha-textarea") private _input?: HTMLInputElement;
@query("ha-input-multi") private _inputMulti?: {
reportValidity: () => boolean;
};
public async focus() {
await this.updateComplete;
this._input?.focus();
@@ -39,7 +35,7 @@ export class HaTextSelector extends LitElement {
public reportValidity(): boolean {
if (this.selector.text?.multiple) {
return this._inputMulti?.reportValidity() ?? true;
return true;
}
return this._input?.reportValidity() ?? true;
}
@@ -56,8 +52,6 @@ export class HaTextSelector extends LitElement {
.inputPrefix=${this.selector.text?.prefix}
.helper=${this.helper}
.autocomplete=${this.selector.text?.autocomplete}
.pattern=${this.selector.text?.pattern}
.validationMessage=${this.selector.text?.validation_message}
@value-changed=${this._handleChange}
>
</ha-input-multi>
@@ -86,9 +80,6 @@ export class HaTextSelector extends LitElement {
.hint=${this.helper}
.disabled=${this.disabled}
.type=${this.selector.text?.type}
.pattern=${this.selector.text?.pattern}
.validationMessage=${this.selector.text?.validation_message}
.autoValidate=${this.selector.text?.pattern !== undefined}
@input=${this._handleChange}
@change=${this._handleChange}
.label=${this.label || ""}
+1 -7
View File
@@ -895,13 +895,7 @@ export class HaServiceControl extends LitElement {
}
if (targetEntities.length) {
targetEntities = targetEntities.filter((entity) =>
entityMeetsTargetSelector(
this.hass.states[entity],
targetSelector,
undefined,
this.hass.entities,
this.hass.devices
)
entityMeetsTargetSelector(this.hass.states[entity], targetSelector)
);
}
target = {
+1 -29
View File
@@ -2,13 +2,7 @@ import { consume, type ContextType } from "@lit/context";
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import {
customElement,
property,
query,
queryAll,
state,
} from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { uid } from "../../common/util/uid";
@@ -56,13 +50,6 @@ class HaInputMulti extends LitElement {
@property() public autocomplete?: string;
/** Regular expression each entry is validated against (HTML `pattern`). */
@property() public pattern?: string;
/** Message shown on an entry when it fails `pattern` validation. */
@property({ attribute: "validation-message" })
public validationMessage?: string;
@property({ attribute: "add-label" }) public addLabel?: string;
@property({ attribute: "remove-label" }) public removeLabel?: string;
@@ -83,8 +70,6 @@ class HaInputMulti extends LitElement {
@query("ha-input[data-last]") private _lastInput?: HaInput;
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
// Stable key per row, kept in sync with `value`. Because items are plain
// strings we cannot use a WeakMap (as the object-based sortable lists do),
// so we track keys in a parallel array. Keys stay fixed while a row is
@@ -104,16 +89,6 @@ class HaInputMulti extends LitElement {
}
}
public reportValidity(): boolean {
let valid = true;
this._inputs?.forEach((input) => {
if (!input.reportValidity()) {
valid = false;
}
});
return valid;
}
protected render() {
return html`
<ha-sortable
@@ -134,9 +109,6 @@ class HaInputMulti extends LitElement {
.type=${this.inputType}
.autocomplete=${this.autocomplete}
.disabled=${this.disabled}
.pattern=${this.pattern}
.validationMessage=${this.validationMessage}
.autoValidate=${this.pattern !== undefined}
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
+1 -1
View File
@@ -140,7 +140,7 @@ export interface HassioAddonSetOptionParams {
export const reloadHassioAddons = async (hass: HomeAssistant) => {
await hass.callWS({
type: "supervisor/api",
endpoint: "/store/reload",
endpoint: "/addons/reload",
method: "post",
});
};
-15
View File
@@ -21,21 +21,6 @@ export interface HttpConfigState {
revert_at: string | null;
}
export const HTTP_CONFIG_FIELDS: (keyof HttpConfig)[] = [
"server_port",
"server_host",
"ssl_certificate",
"ssl_key",
"ssl_peer_certificate",
"ssl_profile",
"cors_allowed_origins",
"use_x_forwarded_for",
"trusted_proxies",
"use_x_frame_options",
"ip_ban_enabled",
"login_attempts_threshold",
];
export interface SaveHttpConfigResult {
restart: boolean;
}
+10 -57
View File
@@ -266,10 +266,6 @@ interface EntitySelectorFilter {
unit_of_measurement?: string | readonly string[];
}
interface EntitySelectorEntityFilter extends EntitySelectorFilter {
device?: DeviceSelectorFilter;
}
export interface EntitySelectorExtraOption {
id: string;
primary: string;
@@ -285,7 +281,7 @@ export interface EntitySelector {
multiple?: boolean;
include_entities?: string[];
exclude_entities?: string[];
filter?: EntitySelectorEntityFilter | readonly EntitySelectorEntityFilter[];
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
reorder?: boolean;
extra_options?: EntitySelectorExtraOption[];
} | null;
@@ -535,10 +531,6 @@ export interface StringSelector {
placeholder?: string;
autocomplete?: string;
multiple?: true;
// Regular expression the value must match (HTML `pattern`); with `multiple`
// every entry is validated. `validation_message` is shown when it fails.
pattern?: string;
validation_message?: string;
} | null;
}
@@ -675,9 +667,7 @@ export const expandLabelTarget = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources,
hass.entities,
hass.devices
entitySources
)
) {
newEntities.push(entity.entity_id);
@@ -743,9 +733,7 @@ export const expandAreaTarget = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources,
hass.entities,
hass.devices
entitySources
)
) {
newEntities.push(entity.entity_id);
@@ -768,9 +756,7 @@ export const expandDeviceTarget = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources,
hass.entities,
hass.devices
entitySources
)
) {
newEntities.push(entity.entity_id);
@@ -811,9 +797,7 @@ export const areaMeetsTargetSelector = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources,
hass.entities,
hass.devices
entitySources
)
) {
return true;
@@ -861,22 +845,14 @@ export const deviceMeetsTargetSelector = (
export const entityMeetsTargetSelector = (
entity: HassEntity | undefined,
targetSelector: TargetSelector,
entitySources?: EntitySources,
entities?: HomeAssistant["entities"],
devices?: HomeAssistant["devices"]
entitySources?: EntitySources
): boolean => {
if (!entity) {
return false;
}
if (targetSelector.target?.entity) {
return ensureArray(targetSelector.target!.entity).some((filterEntity) =>
filterSelectorEntities(
filterEntity,
entity,
entitySources,
entities,
devices
)
filterSelectorEntities(filterEntity, entity, entitySources)
);
}
return true;
@@ -915,12 +891,9 @@ export const filterSelectorDevices = (
};
export const filterSelectorEntities = (
filterEntity: EntitySelectorEntityFilter,
filterEntity: EntitySelectorFilter,
entity: HassEntity,
entitySources?: EntitySources,
entityRegistry?: HomeAssistant["entities"],
devices?: HomeAssistant["devices"],
deviceIntegrationLookup?: Record<string, Set<string>>
entitySources?: EntitySources
): boolean => {
const {
domain: filterDomain,
@@ -928,7 +901,6 @@ export const filterSelectorEntities = (
supported_features: filterSupportedFeature,
unit_of_measurement: filterUnitOfMeasurement,
integration: filterIntegration,
device: filterDevice,
} = filterEntity;
if (filterDomain) {
@@ -975,24 +947,6 @@ export const filterSelectorEntities = (
}
}
if (filterDevice) {
if (!entityRegistry || !devices) {
return false;
}
const deviceId = entityRegistry[entity.entity_id]?.device_id;
if (!deviceId) {
return false;
}
const device = devices[deviceId];
if (!device) {
return false;
}
if (!filterSelectorDevices(filterDevice, device, deviceIntegrationLookup)) {
return false;
}
}
if (
filterIntegration &&
entitySources?.[entity.entity_id]?.domain !== filterIntegration
@@ -1062,7 +1016,7 @@ export const handleLegacyDeviceSelector = (
export const computeCreateDomains = (
selector: EntitySelector | TargetSelector
): undefined | string[] => {
let entityFilters: EntitySelectorEntityFilter[] | undefined;
let entityFilters: EntitySelectorFilter[] | undefined;
if ("target" in selector) {
entityFilters = ensureArray(selector.target?.entity);
@@ -1080,7 +1034,6 @@ export const computeCreateDomains = (
!entityFilter.integration &&
!entityFilter.device_class &&
!entityFilter.supported_features &&
!entityFilter.device &&
entityFilter.domain
? ensureArray(entityFilter.domain).filter((domain) =>
isHelperDomain(domain)
@@ -1,27 +1,34 @@
import { mdiArrowRight } from "@mdi/js";
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { formatNumericDuration } from "../../common/datetime/format_duration";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import type { HttpConfig } from "../../data/http";
import {
HTTP_CONFIG_FIELDS,
promoteHttpConfig,
saveHttpConfig,
} from "../../data/http";
import { promoteHttpConfig, saveHttpConfig } from "../../data/http";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { HttpPendingConfigDialogParams } from "./show-dialog-http-pending-config";
const HTTP_FIELDS: (keyof HttpConfig)[] = [
"server_port",
"server_host",
"ssl_certificate",
"ssl_key",
"ssl_peer_certificate",
"ssl_profile",
"cors_allowed_origins",
"use_x_forwarded_for",
"trusted_proxies",
"use_x_frame_options",
"ip_ban_enabled",
"login_attempts_threshold",
];
@customElement("dialog-http-pending-config")
export class DialogHttpPendingConfig
extends LitElement
@@ -50,10 +57,6 @@ export class DialogHttpPendingConfig
this._error = undefined;
this._reverted = false;
this._startCountdown();
// The field labels live in the config panel fragment, which is not loaded
// yet when this dialog pops up on startup. Load it so the changed-field
// names resolve; the dialog re-renders once hass updates.
this.hass.loadFragmentTranslation("config");
}
public closeDialog(): boolean {
@@ -111,44 +114,17 @@ export class DialogHttpPendingConfig
return [];
}
const { stable, pending } = this._params.state;
return HTTP_CONFIG_FIELDS.filter(
return HTTP_FIELDS.filter(
(key) => JSON.stringify(stable[key]) !== JSON.stringify(pending[key])
);
}
private _formatValue(key: keyof HttpConfig, value: unknown): string {
if (value === undefined || value === null || value === "") {
return this.hass.localize("ui.dialogs.http_pending_config.not_set");
}
if (typeof value === "boolean") {
return this.hass.localize(value ? "ui.common.yes" : "ui.common.no");
}
if (Array.isArray(value)) {
return value.length
? value.join(", ")
: this.hass.localize("ui.dialogs.http_pending_config.not_set");
}
if (key === "ssl_profile") {
return (
this.hass.localize(
`ui.panel.config.network.http.ssl_profile_${value}` as any
) || String(value)
);
}
return String(value);
}
protected render() {
if (!this._params) {
return nothing;
}
const changes = this._changedFields;
const { stable, pending } = this._params.state;
const rtl = computeRTL(
this.hass.language,
this.hass.translationMetadata.translations
);
return html`
<ha-dialog
@@ -211,25 +187,9 @@ export class DialogHttpPendingConfig
${changes.map(
(key) => html`
<li>
<span class="field">
${this.hass.localize(
`ui.panel.config.network.http.fields.${key}` as any
)}
</span>
<span class="values">
<span class="old"
>${this._formatValue(key, stable[key])}</span
>
<ha-svg-icon
.path=${mdiArrowRight}
style=${styleMap({
transform: rtl ? "scaleX(-1)" : "",
})}
></ha-svg-icon>
<span class="new"
>${this._formatValue(key, pending![key])}</span
>
</span>
${this.hass.localize(
`ui.panel.config.network.http.fields.${key}` as any
)}
</li>
`
)}
@@ -360,37 +320,15 @@ export class DialogHttpPendingConfig
margin-bottom: var(--ha-space-2);
}
ul {
list-style: none;
margin: 0 0 var(--ha-space-4) 0;
padding: 0;
padding-left: var(--ha-space-6);
padding-inline-start: var(--ha-space-6);
padding-inline-end: initial;
color: var(--secondary-text-color);
}
li {
padding: var(--ha-space-2) 0;
border-bottom: 1px solid var(--divider-color);
}
li:last-child {
border-bottom: none;
}
.field {
display: block;
font-weight: var(--ha-font-weight-medium);
margin-bottom: var(--ha-space-1);
}
.values {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--ha-space-2);
color: var(--secondary-text-color);
word-break: break-word;
}
.values .new {
color: var(--primary-text-color);
}
.values ha-svg-icon {
--mdc-icon-size: 18px;
flex-shrink: 0;
}
ha-alert {
display: block;
margin-top: var(--ha-space-4);
+1 -5
View File
@@ -123,10 +123,6 @@ interface EMOutgoingMessageConnectionStatus extends EMMessage {
payload: { event: string };
}
interface EMOutgoingMessageFrontendLoaded extends EMMessage {
type: "frontend/loaded"; // Fired once the launch screen is removed (connected and essential data loaded)
}
interface EMOutgoingMessageAppConfiguration extends EMMessage {
type: "config_screen/show";
}
@@ -205,7 +201,6 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageBarCodeNotify
| EMOutgoingMessageBarCodeScan
| EMOutgoingMessageConnectionStatus
| EMOutgoingMessageFrontendLoaded
| EMOutgoingMessageExoplayerPlayHLS
| EMOutgoingMessageExoplayerResize
| EMOutgoingMessageExoplayerStop
@@ -371,6 +366,7 @@ export interface ExternalConfig {
appVersion?: string;
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
hasAssistSettings?: boolean; // Shows the "This device" section in voice assistant settings
hasLoader?: boolean; // App renders its own loading screen, so the frontend launch screen stays blank
}
export interface ExternalEntityAddToAction {
+3
View File
@@ -59,6 +59,9 @@
#ha-launch-screen.removing {
opacity: 0;
}
#ha-launch-screen.blank > * {
display: none;
}
#ha-launch-screen svg {
width: 112px;
flex-shrink: 0;
+13 -1
View File
@@ -17,6 +17,8 @@ import QuickBarMixin from "../state/quick-bar-mixin";
import type { HomeAssistant, Route } from "../types";
import { storeState } from "../util/ha-pref-storage";
import {
blankLaunchScreen,
clearLaunchScreenInfoBox,
removeLaunchScreen,
renderLaunchScreenInfoBox,
} from "../util/launch-screen";
@@ -59,6 +61,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
private _httpPendingDialogOpen = false;
private _appHandlesLoader = false;
private _panelUrl: string;
@storage({ key: "ha-version", state: false, subscribe: false })
@@ -120,7 +124,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
this.render = this.renderHass;
this.update = super.update;
removeLaunchScreen();
this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
}
super.update(changedProps);
}
@@ -307,6 +310,11 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
const { auth, conn } = result;
this._checkUpdate(conn);
this.initializeHass(auth, conn);
if (this.hass?.auth.external?.config.hasLoader) {
this._appHandlesLoader = true;
blankLaunchScreen();
this._renderInitInfo(false);
}
} catch (_err: any) {
this._renderInitInfo(true);
}
@@ -366,6 +374,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
}
private _renderInitInfo(error: boolean) {
if (this._appHandlesLoader) {
clearLaunchScreenInfoBox();
return;
}
renderLaunchScreenInfoBox(
html`<ha-init-page
.error=${error}
@@ -3,10 +3,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
IP_ADDRESS_OR_NETWORK_PATTERN,
IP_ADDRESS_PATTERN,
} from "../../../common/string/is_ip_address";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -14,11 +10,7 @@ import "../../../components/ha-card";
import "../../../components/ha-form/ha-form";
import type { HaForm } from "../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../components/ha-form/types";
import {
fetchHttpConfig,
HTTP_CONFIG_FIELDS,
saveHttpConfig,
} from "../../../data/http";
import { fetchHttpConfig, saveHttpConfig } from "../../../data/http";
import type { HttpConfig } from "../../../data/http";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
@@ -32,6 +24,10 @@ const SCHEMA = memoizeOne(
required: true,
selector: { number: { min: 1, max: 65535, mode: "box" } },
},
{
name: "server_host",
selector: { text: { multiple: true } },
},
{
name: "ssl",
type: "expandable",
@@ -85,15 +81,7 @@ const SCHEMA = memoizeOne(
},
{
name: "trusted_proxies",
selector: {
text: {
multiple: true,
pattern: IP_ADDRESS_OR_NETWORK_PATTERN,
validation_message: localize(
"ui.panel.config.network.http.invalid_network"
),
},
},
selector: { text: { multiple: true } },
},
],
},
@@ -120,18 +108,6 @@ const SCHEMA = memoizeOne(
flatten: true,
title: localize("ui.panel.config.network.http.sections.advanced"),
schema: [
{
name: "server_host",
selector: {
text: {
multiple: true,
pattern: IP_ADDRESS_PATTERN,
validation_message: localize(
"ui.panel.config.network.http.invalid_host"
),
},
},
},
{
name: "cors_allowed_origins",
selector: { text: { multiple: true } },
@@ -189,9 +165,6 @@ class HaConfigHttpForm extends LitElement {
const schema = SCHEMA(this.hass.localize);
const portChanged =
!!this._stable && this._config?.server_port !== this._stable.server_port;
return html`
<ha-card
outlined
@@ -201,17 +174,6 @@ class HaConfigHttpForm extends LitElement {
<p class="description">
${this.hass.localize("ui.panel.config.network.http.description")}
</p>
${
portChanged
? html`
<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.network.http.port_warning"
)}
</ha-alert>
`
: nothing
}
${
this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -356,7 +318,15 @@ class HaConfigHttpForm extends LitElement {
) {
return;
}
this._handleSaveError(err);
// voluptuous formats errors as "<message> @ data['<field>']".
// If a field is identified, mark it inline; otherwise show a card-level
// alert.
const fieldMatch = err.message?.match(/\bdata\['([^']+)'\]/);
if (fieldMatch) {
this._fieldErrors = { [fieldMatch[1]]: err.message };
} else {
this._error = err.message;
}
} finally {
this._saving = false;
}
@@ -370,34 +340,6 @@ class HaConfigHttpForm extends LitElement {
target?.scrollIntoView({ behavior: "smooth", block: "center" });
}
private _handleSaveError(err: any): void {
const rawMessage =
(typeof err === "string" ? err : err?.message) ||
this.hass.localize("ui.panel.config.network.http.save_error");
// Voluptuous formats validation errors as
// "<reason> @ data['config']['<field>'][<index>]. Got '<value>'"
// Strip the internal data path for display and pick the deepest known
// field name so it can also be flagged inline.
const message =
rawMessage.replace(/\s*@\s*data(\['[^']*'\]|\[\d+\])+/g, "").trim() ||
rawMessage;
const field = [...rawMessage.matchAll(/\['([^']+)'\]/g)]
.map((match) => match[1])
.reverse()
.find((name) => HTTP_CONFIG_FIELDS.includes(name as keyof HttpConfig)) as
keyof HttpConfig | undefined;
if (field) {
// Show a card-level alert too — the field may sit in a collapsed section.
this._error = `${this.hass.localize(
`ui.panel.config.network.http.fields.${field}` as any
)}: ${message}`;
this._fieldErrors = { [field]: message };
} else {
this._error = message;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
+14 -11
View File
@@ -49,8 +49,8 @@ import { resolveEntityIDs } from "../../data/selector";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
import { csvSafeString, csvDownload } from "../../util/csv";
@customElement("ha-panel-history")
class HaPanelHistory extends LitElement {
@@ -443,8 +443,8 @@ class HaPanelHistory extends LitElement {
return;
}
const csv: string[] = [""]; // headers will be replaced later.
const headers = ["entity_id", "state", "last_changed"];
const csv: string[][] = [[]]; // headers will be replaced later.
const processedDomainAttributes = new Set<string>();
const domainAttributes: Record<string, Record<string, number>> = {
climate: {
@@ -486,7 +486,7 @@ class HaPanelHistory extends LitElement {
if (entity.statistics) {
for (const s of entity.statistics) {
csv.push([entityId, s.state, formatDate(s.last_changed)]);
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
}
}
@@ -503,22 +503,25 @@ class HaPanelHistory extends LitElement {
}
}
csv.push(data);
csv.push(data.join(",") + "\n");
}
}
}
for (const timeline of this._mungedStateHistory.timeline) {
const entityId = timeline.entity_id;
for (const s of timeline.data) {
csv.push([
entityId,
csvSafeString(s.state),
formatDate(s.last_changed),
]);
const safeState = /,|"/.test(s.state)
? `"${s.state.replaceAll('"', '""')}"`
: s.state;
csv.push(`${entityId},${safeState},${formatDate(s.last_changed)}\n`);
}
}
csv[0] = headers;
csvDownload(csv, "history.csv");
csv[0] = headers.join(",") + "\n";
const blob = new Blob(csv, {
type: "text/csv",
});
const url = window.URL.createObjectURL(blob);
fileDownload(url, "history.csv");
}
private _suggestCard() {
-4
View File
@@ -100,10 +100,6 @@ export class HaLogbook extends LitElement {
private _readyListenerAttached = false;
public getEntries(): LogbookEntry[] {
return this._logbookEntries || [];
}
protected render() {
if (!isComponentLoaded(this.hass.config, "logbook")) {
return nothing;
+7 -91
View File
@@ -1,15 +1,9 @@
import {
mdiDotsVertical,
mdiDownload,
mdiFilterRemove,
mdiRefresh,
} from "@mdi/js";
import { mdiFilterRemove, mdiRefresh } from "@mdi/js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fromUnixTime } from "date-fns";
import { storage } from "../../common/decorators/storage";
import { navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
@@ -24,9 +18,6 @@ import {
} from "../../common/url/search-params";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/date-picker/ha-date-range-picker";
import "../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
@@ -36,8 +27,6 @@ import { resolveEntityIDs } from "../../data/selector";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "./ha-logbook";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { csvDownload, csvSafeString } from "../../util/csv";
interface LogbookState {
time: { range: [Date, Date] };
@@ -88,24 +77,12 @@ export class HaPanelLogbook extends LitElement {
.path=${mdiFilterRemove}
.label=${this.hass.localize("ui.common.reset")}
></ha-icon-button>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="refresh">
${this.hass.localize("ui.common.refresh")}
<ha-svg-icon slot="icon" .path=${mdiRefresh}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item value="download">
${this.hass.localize("ui.panel.logbook.download_data")}
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<ha-icon-button
slot="actionItems"
@click=${this._refreshLogbook}
.path=${mdiRefresh}
.label=${this.hass!.localize("ui.common.refresh")}
></ha-icon-button>
<div class="content">
<div class="filters">
@@ -291,67 +268,6 @@ export class HaPanelLogbook extends LitElement {
this.shadowRoot!.querySelector("ha-logbook")?.refresh();
}
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
switch (action) {
case "download":
this._downloadData();
break;
case "refresh":
this._refreshLogbook();
break;
}
}
private _downloadData() {
const data =
this.shadowRoot!.querySelector("ha-logbook")?.getEntries() || [];
if (data.length === 0) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.logbook.download_data_error"),
text: this.hass.localize("ui.panel.logbook.error_no_data"),
warning: true,
});
return;
}
const headers = [
"time",
"entity_id",
"state",
"event_type",
"context_id",
"context_user_id",
"context_event_type",
"context_domain",
"context_service",
"context_entity_id",
"context_state",
"context_source",
];
const csv: string[][] = [headers];
for (const d of data) {
const time = fromUnixTime(d.when).toISOString();
csv.push([
time,
d.entity_id || "",
csvSafeString(d.state),
csvSafeString(d.attributes?.event_type),
d.context_id || "",
d.context_user_id || "",
csvSafeString(d.context_event_type),
d.context_domain || "",
d.context_service || "",
d.context_entity_id || "",
csvSafeString(d.context_state),
d.context_source || "",
]);
}
csvDownload(csv, "activity.csv");
}
static get styles() {
return [
haStyle,
+26 -2
View File
@@ -23,7 +23,6 @@ import type {
LovelaceGridOptions,
LovelaceHeaderFooter,
} from "../types";
import { migrateEntitiesCardConfig } from "./migrate-card-config";
import type { EntitiesCardConfig } from "./types";
import { haStyleScrollbar } from "../../../resources/styles";
@@ -50,7 +49,32 @@ export const computeShowHeaderToggle = <
return !!config.show_header_toggle;
};
export { migrateEntitiesCardConfig };
export const migrateEntitiesCardConfig = (
config: EntitiesCardConfig
): EntitiesCardConfig => {
let changed = false;
const newEntities = config.entities?.map((e) => {
if (typeof e !== "object") {
return e;
}
if (!("format" in e)) {
return e;
}
changed = true;
const { format, ...rest } = e;
return {
...rest,
time_format: (rest as EntityConfig).time_format ?? format,
};
});
if (!changed) {
return config;
}
return {
...config,
entities: newEntities as (LovelaceRowConfig | string)[],
};
};
@customElement("hui-entities-card")
class HuiEntitiesCard extends LitElement implements LovelaceCard {
+26 -2
View File
@@ -31,11 +31,35 @@ import "../components/hui-timestamp-display";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import { migrateGlanceCardConfig } from "./migrate-card-config";
import type { GlanceCardConfig, GlanceConfigEntity } from "./types";
import { TIMESTAMP_STATE_DOMAINS } from "../../../common/const";
export { migrateGlanceCardConfig };
export const migrateGlanceCardConfig = (
config: GlanceCardConfig
): GlanceCardConfig => {
let changed = false;
const newEntities = config.entities?.map((e) => {
if (typeof e !== "object") {
return e;
}
if (!("format" in e)) {
return e;
}
changed = true;
const { format, ...rest } = e;
return {
...rest,
time_format: rest.time_format ?? format,
};
});
if (!changed) {
return config;
}
return {
...config,
entities: newEntities as (GlanceConfigEntity | string)[],
};
};
@customElement("hui-glance-card")
export class HuiGlanceCard extends LitElement implements LovelaceCard {
@@ -1,66 +0,0 @@
import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
import type {
EntitiesCardConfig,
GlanceCardConfig,
GlanceConfigEntity,
} from "./types";
export const migrateEntitiesCardConfig = (
config: EntitiesCardConfig
): EntitiesCardConfig => {
let changed = false;
const newEntities = config.entities?.map((e) => {
if (typeof e !== "object") {
return e;
}
// Custom rows own their config schema and may use `format` with a
// different meaning (e.g. custom:multiple-entity-row), so leave it
// untouched.
if (e.type?.startsWith("custom:")) {
return e;
}
if (!("format" in e)) {
return e;
}
changed = true;
const { format, ...rest } = e;
return {
...rest,
time_format: (rest as EntityConfig).time_format ?? format,
};
});
if (!changed) {
return config;
}
return {
...config,
entities: newEntities as (LovelaceRowConfig | string)[],
};
};
export const migrateGlanceCardConfig = (
config: GlanceCardConfig
): GlanceCardConfig => {
let changed = false;
const newEntities = config.entities?.map((e) => {
if (typeof e !== "object") {
return e;
}
if (!("format" in e)) {
return e;
}
changed = true;
const { format, ...rest } = e;
return {
...rest,
time_format: rest.time_format ?? format,
};
});
if (!changed) {
return config;
}
return {
...config,
entities: newEntities as (GlanceConfigEntity | string)[],
};
};
@@ -54,65 +54,6 @@ const cardConfigStruct = assign(
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { min: 0, step: "any", mode: "box" } },
},
{
name: "show_names",
default: true,
required: false,
selector: { boolean: {} },
},
{
name: "logarithmic_scale",
required: false,
selector: { boolean: {} },
},
{
name: "expand_legend",
required: false,
selector: { boolean: {} },
},
],
},
{
name: "",
type: "grid",
schema: [
{
name: "min_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
{
name: "max_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
],
},
{
name: "fit_y_data",
required: false,
hidden: {
condition: "and",
conditions: [
{ field: "min_y_axis", operator: "not_exists" },
{ field: "max_y_axis", operator: "not_exists" },
],
},
selector: { boolean: {} },
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-history-graph-card-editor")
export class HuiHistoryGraphCardEditor
extends LitElement
@@ -129,6 +70,65 @@ export class HuiHistoryGraphCardEditor
this._config = config;
}
private _schema = memoizeOne(
(showFitOption: boolean) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { min: 0, step: "any", mode: "box" } },
},
{
name: "show_names",
default: true,
required: false,
selector: { boolean: {} },
},
{
name: "logarithmic_scale",
required: false,
selector: { boolean: {} },
},
{
name: "expand_legend",
required: false,
selector: { boolean: {} },
},
],
},
{
name: "",
type: "grid",
schema: [
{
name: "min_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
{
name: "max_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
],
},
...(showFitOption
? [
{
name: "fit_y_data",
required: false,
selector: { boolean: {} },
},
]
: []),
] as const
);
private _subForm = memoizeOne((localize: LocalizeFunc, entityId: string) => ({
schema: [
{ name: "entity", selector: { entity: {} }, required: true },
@@ -176,6 +176,11 @@ export class HuiHistoryGraphCardEditor
`;
}
const schema = this._schema(
this._config!.min_y_axis !== undefined ||
this._config!.max_y_axis !== undefined
);
const configEntities = this._config.entities
? (processEditorEntities(this._config.entities) as GraphEntityConfig[])
: [];
@@ -183,7 +188,7 @@ export class HuiHistoryGraphCardEditor
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -278,7 +283,9 @@ export class HuiHistoryGraphCardEditor
) as HistoryGraphCardConfig;
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "show_names":
case "logarithmic_scale":
@@ -92,6 +92,7 @@ export class HuiTileCardEditor
(
localize: LocalizeFunc,
entityId: string | undefined,
hideState: boolean,
showTimeFormat: boolean
) =>
[
@@ -143,25 +144,31 @@ export class HuiTileCardEditor
},
],
},
{
name: "state_content",
hidden: { field: "hide_state", value: true },
selector: {
ui_state_content: {
allow_context: true,
},
},
context: {
filter_entity: "entity",
},
},
{
name: "time_format",
hidden: !showTimeFormat,
selector: {
ui_time_format: {},
},
},
...(!hideState
? ([
{
name: "state_content",
selector: {
ui_state_content: {
allow_context: true,
},
},
context: {
filter_entity: "entity",
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(showTimeFormat
? ([
{
name: "time_format",
selector: {
ui_time_format: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
{
name: "content_layout",
required: true,
@@ -286,7 +293,12 @@ export class HuiTileCardEditor
this._config.state_content
);
const schema = this._schema(this.hass.localize, entityId, showTimeFormat);
const schema = this._schema(
this.hass.localize,
entityId,
this._config.hide_state ?? false,
showTimeFormat
);
const vertical = this._config.vertical ?? false;
+1 -11
View File
@@ -1486,7 +1486,6 @@
"auto_revert": "Settings will automatically revert in {time}.",
"reverted": "Home Assistant reverted the HTTP server settings.",
"changes_label": "Changed settings:",
"not_set": "Not set",
"confirm": "Confirm",
"revert": "Revert",
"close": "Close",
@@ -8680,10 +8679,6 @@
"description": "Configure how Home Assistant serves its web interface. Saving restarts Home Assistant.",
"save": "Save",
"save_no_changes": "Nothing changed — no restart needed.",
"save_error": "Could not save the HTTP configuration.",
"port_warning": "Clients such as the Home Assistant mobile apps will lose their connection until you update the URL in their settings. If Home Assistant is not confirmed reachable on the new port, the change is rolled back automatically after 5 minutes.",
"invalid_host": "Enter a valid IP address.",
"invalid_network": "Enter a valid IP address or network.",
"save_confirm": {
"title": "Restart required",
"text": "Saving will restart Home Assistant to apply the new HTTP settings.",
@@ -8695,7 +8690,7 @@
"ssl": "SSL/TLS",
"reverse_proxy": "Reverse proxy",
"ip_banning": "IP banning",
"advanced": "More options"
"advanced": "Advanced"
},
"fields": {
"server_port": "Server port",
@@ -11571,11 +11566,6 @@
"add_card": "Add current view as card",
"add_card_error": "Unable to add card",
"error_no_data": "You need to select some data sources first."
},
"logbook": {
"download_data": "[%key:ui::panel::history::download_data%]",
"download_data_error": "[%key:ui::panel::history::download_data_error%]",
"error_no_data": "No activity for the selected target in the selected time range."
}
},
"tips": {
-15
View File
@@ -1,15 +0,0 @@
import { fileDownload } from "./file_download";
export function csvSafeString(s: string | undefined): string {
if (!s) return "";
return /,|"/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
}
export function csvDownload(data: string[][], filename: string) {
const csv = data.map((row) => row.join(",").concat("\n"));
const blob = new Blob(csv, {
type: "text/csv",
});
const url = window.URL.createObjectURL(blob);
fileDownload(url, filename);
}
+13 -1
View File
@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { render } from "lit";
import { nothing, render } from "lit";
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
import { withViewTransition } from "../common/util/view-transition";
@@ -27,9 +27,21 @@ export const removeLaunchScreen = () => {
});
};
export const blankLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen");
launchScreenElement?.classList.add("blank");
};
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
const infoBoxElement = document.getElementById("ha-launch-screen-info-box");
if (infoBoxElement) {
render(content, infoBoxElement);
}
};
export const clearLaunchScreenInfoBox = () => {
const infoBoxElement = document.getElementById("ha-launch-screen-info-box");
if (infoBoxElement) {
render(nothing, infoBoxElement);
}
};
-115
View File
@@ -1,115 +0,0 @@
import { describe, expect, it } from "vitest";
import {
IP_ADDRESS_OR_NETWORK_PATTERN,
IP_ADDRESS_PATTERN,
isIPAddress,
isIPAddressV4OrV6,
isIPNetwork,
isIPv6Address,
} from "../../../src/common/string/is_ip_address";
describe("isIPAddress (IPv4)", () => {
it("accepts valid IPv4 addresses", () => {
expect(isIPAddress("192.168.1.10")).toBe(true);
expect(isIPAddress("0.0.0.0")).toBe(true);
expect(isIPAddress("255.255.255.255")).toBe(true);
});
it("rejects invalid IPv4 addresses", () => {
expect(isIPAddress("256.1.1.1")).toBe(false);
expect(isIPAddress("192.168.1")).toBe(false);
expect(isIPAddress("192.168.1.10/24")).toBe(false);
expect(isIPAddress("fe80::1")).toBe(false);
expect(isIPAddress("")).toBe(false);
});
});
describe("isIPv6Address", () => {
it("accepts valid IPv6 addresses", () => {
expect(isIPv6Address("fe80::85d:e82c:9446:7995")).toBe(true);
expect(isIPv6Address("::1")).toBe(true);
expect(isIPv6Address("1050:0000:0000:0000:0005:0600:300c:326b")).toBe(true);
expect(isIPv6Address("::ffff:192.168.1.1")).toBe(true);
});
it("rejects invalid IPv6 addresses", () => {
expect(isIPv6Address("192.168.1.10")).toBe(false);
expect(isIPv6Address("fe80::85d::7995")).toBe(false);
expect(isIPv6Address("gggg::1")).toBe(false);
expect(isIPv6Address("")).toBe(false);
});
});
describe("isIPAddressV4OrV6", () => {
it("accepts both families", () => {
expect(isIPAddressV4OrV6("192.168.1.10")).toBe(true);
expect(isIPAddressV4OrV6("fe80::1")).toBe(true);
});
it("rejects networks and garbage", () => {
expect(isIPAddressV4OrV6("192.168.1.0/24")).toBe(false);
expect(isIPAddressV4OrV6("not-an-ip")).toBe(false);
});
});
describe("isIPNetwork (CIDR)", () => {
it("accepts valid networks", () => {
expect(isIPNetwork("192.168.1.0/24")).toBe(true);
expect(isIPNetwork("10.0.0.0/8")).toBe(true);
expect(isIPNetwork("172.16.0.0/12")).toBe(true);
expect(isIPNetwork("0.0.0.0/0")).toBe(true);
expect(isIPNetwork("fd00::/8")).toBe(true);
expect(isIPNetwork("fe80::/128")).toBe(true);
});
it("rejects invalid networks", () => {
// Prefix out of range — the reported production error.
expect(isIPNetwork("172.30.33.0/24444444")).toBe(false);
expect(isIPNetwork("192.168.1.0/33")).toBe(false);
expect(isIPNetwork("fd00::/129")).toBe(false);
expect(isIPNetwork("192.168.1.0")).toBe(false);
expect(isIPNetwork("192.168.1.0/24/24")).toBe(false);
expect(isIPNetwork("not-a-network/24")).toBe(false);
});
});
// The HTML `pattern` attribute is anchored by the browser as `^(?:…)$`.
const matchesPattern = (pattern: string, value: string): boolean =>
new RegExp(`^(?:${pattern})$`).test(value);
describe("IP_ADDRESS_PATTERN", () => {
it("accepts IPv4 and IPv6 addresses", () => {
expect(matchesPattern(IP_ADDRESS_PATTERN, "192.168.1.10")).toBe(true);
expect(matchesPattern(IP_ADDRESS_PATTERN, "fe80::1")).toBe(true);
});
it("rejects networks and garbage", () => {
expect(matchesPattern(IP_ADDRESS_PATTERN, "192.168.1.0/24")).toBe(false);
expect(matchesPattern(IP_ADDRESS_PATTERN, "not-an-ip")).toBe(false);
expect(matchesPattern(IP_ADDRESS_PATTERN, "256.1.1.1")).toBe(false);
});
});
describe("IP_ADDRESS_OR_NETWORK_PATTERN", () => {
it("accepts addresses and networks", () => {
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.10")).toBe(
true
);
expect(
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.0/24")
).toBe(true);
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "fd00::/8")).toBe(
true
);
});
it("rejects out-of-range prefixes and garbage", () => {
// The reported production error.
expect(
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "172.30.33.0/24444444")
).toBe(false);
expect(
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.0/33")
).toBe(false);
expect(
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "1.1.1.1/233444")
).toBe(false);
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "not-an-ip")).toBe(
false
);
});
});
@@ -1,110 +0,0 @@
import { describe, it, expect } from "vitest";
import { getAvailableClockDatePartSections } from "../../src/components/ha-clock-date-format-picker";
type TestSection = "weekday" | "day" | "month" | "year" | "separator";
const section = (id: TestSection, itemIds: string[]) => ({
id,
title: id,
items: itemIds.map((itemId) => ({ id: itemId, primary: itemId })),
});
const ALL_SECTIONS = [
section("day", ["day-numeric", "day-2-digit"]),
section("month", ["month-numeric", "month-short", "month-long"]),
section("year", ["year-numeric", "year-2-digit"]),
section("weekday", ["weekday-short", "weekday-long"]),
section("separator", [
"separator-dash",
"separator-slash",
"separator-dot",
"separator-new-line",
]),
];
describe("getAvailableClockDatePartSections", () => {
it("returns every section when no value is set", () => {
const result = getAvailableClockDatePartSections(ALL_SECTIONS, []);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"day",
"month",
"year",
"weekday",
"separator",
]);
});
it("hides a group's section once a value from that group is used", () => {
const result = getAvailableClockDatePartSections(ALL_SECTIONS, [
"day-numeric",
]);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"month",
"year",
"weekday",
"separator",
]);
});
it("keeps the edited item's own group visible via excludeIndex", () => {
const value = ["day-numeric", "month-short"];
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 0);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"day",
"year",
"weekday",
"separator",
]);
});
it("keeps the edited item's group visible even when a duplicate of that group exists elsewhere", () => {
const value = ["day-numeric", "day-2-digit", "month-long"];
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 0);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"day",
"year",
"weekday",
"separator",
]);
});
it("does not crash and keeps normal filtering when excludeIndex is out of range", () => {
const value = ["day-numeric", "month-long"];
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 5);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"year",
"weekday",
"separator",
]);
});
it("never hides the separator section, even with multiple separators used", () => {
const value = [
"separator-dash",
"separator-slash",
"separator-dot",
"separator-new-line",
];
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"day",
"month",
"year",
"weekday",
"separator",
]);
});
it("treats an unrecognized token as a separator and does not hide any group", () => {
const result = getAvailableClockDatePartSections(ALL_SECTIONS, [
"not-a-real-part",
]);
expect(result.map((sectionData) => sectionData.id)).toEqual([
"day",
"month",
"year",
"weekday",
"separator",
]);
});
});
@@ -1,134 +0,0 @@
import { describe, expect, it } from "vitest";
import { isFieldHidden } from "../../../src/components/ha-form/conditions";
import type { HaFormSchema } from "../../../src/components/ha-form/types";
const field = (hidden: HaFormSchema["hidden"]): HaFormSchema =>
({ name: "field", selector: { text: {} }, hidden }) as HaFormSchema;
describe("isFieldHidden", () => {
it("shows a field without a hidden condition", () => {
expect(isFieldHidden(field(undefined), { a: 1 })).toBe(false);
});
it("honors a boolean hidden", () => {
expect(isFieldHidden(field(true), {})).toBe(true);
expect(isFieldHidden(field(false), {})).toBe(false);
});
describe("operators", () => {
it("eq (default) matches equal values", () => {
expect(isFieldHidden(field({ field: "a", value: 1 }), { a: 1 })).toBe(
true
);
expect(isFieldHidden(field({ field: "a", value: 1 }), { a: 2 })).toBe(
false
);
});
it("not_eq matches different values", () => {
const schema = field({ field: "a", operator: "not_eq", value: 1 });
expect(isFieldHidden(schema, { a: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1 })).toBe(false);
});
it("in matches membership", () => {
const schema = field({ field: "a", operator: "in", value: ["x", "y"] });
expect(isFieldHidden(schema, { a: "y" })).toBe(true);
expect(isFieldHidden(schema, { a: "z" })).toBe(false);
});
it("not_in matches non-membership", () => {
const schema = field({
field: "a",
operator: "not_in",
value: ["x", "y"],
});
expect(isFieldHidden(schema, { a: "z" })).toBe(true);
expect(isFieldHidden(schema, { a: "x" })).toBe(false);
});
it("exists matches a defined non-empty value", () => {
const schema = field({ field: "a", operator: "exists" });
expect(isFieldHidden(schema, { a: "x" })).toBe(true);
expect(isFieldHidden(schema, { a: "" })).toBe(false);
expect(isFieldHidden(schema, {})).toBe(false);
});
it("not_exists matches a missing or empty value", () => {
const schema = field({ field: "a", operator: "not_exists" });
expect(isFieldHidden(schema, {})).toBe(true);
expect(isFieldHidden(schema, { a: null } as any)).toBe(true);
expect(isFieldHidden(schema, { a: "x" })).toBe(false);
});
});
describe("combinators", () => {
it("and requires every condition", () => {
const schema = field({
condition: "and",
conditions: [
{ field: "a", value: 1 },
{ field: "b", value: 2 },
],
});
expect(isFieldHidden(schema, { a: 1, b: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1, b: 9 })).toBe(false);
});
it("or requires any condition", () => {
const schema = field({
condition: "or",
conditions: [
{ field: "a", value: 1 },
{ field: "b", value: 2 },
],
});
expect(isFieldHidden(schema, { a: 9, b: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 9, b: 9 })).toBe(false);
});
it("not negates its conditions", () => {
const schema = field({
condition: "not",
conditions: [{ field: "a", value: 1 }],
});
expect(isFieldHidden(schema, { a: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1 })).toBe(false);
});
it("nests combinators", () => {
const schema = field({
condition: "and",
conditions: [
{ field: "a", value: 1 },
{
condition: "or",
conditions: [
{ field: "b", value: 2 },
{ field: "c", value: 3 },
],
},
],
});
expect(isFieldHidden(schema, { a: 1, b: 9, c: 3 })).toBe(true);
expect(isFieldHidden(schema, { a: 1, b: 9, c: 9 })).toBe(false);
expect(isFieldHidden(schema, { a: 9, b: 2, c: 3 })).toBe(false);
});
});
it("treats an array of conditions as AND", () => {
const schema = field([
{ field: "a", value: 1 },
{ field: "b", value: 2 },
]);
expect(isFieldHidden(schema, { a: 1, b: 2 })).toBe(true);
expect(isFieldHidden(schema, { a: 1, b: 9 })).toBe(false);
});
it("handles missing data", () => {
expect(isFieldHidden(field({ field: "a", value: 1 }), undefined)).toBe(
false
);
});
});
-134
View File
@@ -1,134 +0,0 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { describe, expect, it } from "vitest";
import type { DeviceRegistryEntry } from "../../src/data/device/device_registry";
import { filterSelectorEntities } from "../../src/data/selector";
import type { HomeAssistant } from "../../src/types";
const entity = {
entity_id: "light.living_room",
state: "on",
attributes: {},
} as HassEntity;
const entityRegistry = {
"light.living_room": { device_id: "device_1" },
} as unknown as HomeAssistant["entities"];
const devices = {
device_1: {
id: "device_1",
manufacturer: "Signify",
model: "Hue Bulb",
model_id: "LCT015",
} as DeviceRegistryEntry,
} as unknown as HomeAssistant["devices"];
describe("filterSelectorEntities device filter", () => {
it("matches when the nested device manufacturer matches", () => {
expect(
filterSelectorEntities(
{ device: { manufacturer: "Signify" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(true);
});
it("does not match when the nested device manufacturer differs", () => {
expect(
filterSelectorEntities(
{ device: { manufacturer: "Sonos" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(false);
});
it("matches when model and model_id both match", () => {
expect(
filterSelectorEntities(
{ device: { model: "Hue Bulb", model_id: "LCT015" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(true);
});
it("does not match when one of model or model_id differs", () => {
expect(
filterSelectorEntities(
{ device: { model: "Hue Bulb", model_id: "OTHER" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(false);
});
it("matches the device integration via the lookup", () => {
expect(
filterSelectorEntities(
{ device: { integration: "hue" } },
entity,
undefined,
entityRegistry,
devices,
{ device_1: new Set(["hue"]) }
)
).toBe(true);
});
it("does not match a device integration that is absent from the lookup", () => {
expect(
filterSelectorEntities(
{ device: { integration: "zha" } },
entity,
undefined,
entityRegistry,
devices,
{ device_1: new Set(["hue"]) }
)
).toBe(false);
});
it("does not match when the entity has no underlying device", () => {
expect(
filterSelectorEntities(
{ device: { manufacturer: "Signify" } },
entity,
undefined,
{} as HomeAssistant["entities"],
devices
)
).toBe(false);
});
it("combines device conditions with other entity conditions (AND)", () => {
expect(
filterSelectorEntities(
{ domain: "light", device: { manufacturer: "Signify" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(true);
expect(
filterSelectorEntities(
{ domain: "switch", device: { manufacturer: "Signify" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(false);
});
});
@@ -1,127 +0,0 @@
import { describe, expect, it } from "vitest";
import {
migrateEntitiesCardConfig,
migrateGlanceCardConfig,
} from "../../../../src/panels/lovelace/cards/migrate-card-config";
import type {
EntitiesCardConfig,
GlanceCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
describe("migrateEntitiesCardConfig", () => {
it("migrates the legacy `format` option to `time_format` on native rows", () => {
const config = {
type: "entities",
entities: [{ entity: "sensor.last_changed", format: "relative" }],
} as unknown as EntitiesCardConfig;
const result = migrateEntitiesCardConfig(config);
expect(result.entities).toEqual([
{ entity: "sensor.last_changed", time_format: "relative" },
]);
// `format` is dropped after migration
expect(result.entities[0]).not.toHaveProperty("format");
});
it("leaves custom rows untouched", () => {
const customRow = {
type: "custom:multiple-entity-row",
entity: "sensor.power",
format: "precision1",
};
const config = {
type: "entities",
entities: [customRow],
} as unknown as EntitiesCardConfig;
const result = migrateEntitiesCardConfig(config);
// Nothing changed, so the same config reference is returned and the
// custom row keeps its own `format` semantics.
expect(result).toBe(config);
expect(result.entities[0]).toEqual(customRow);
});
it("returns the same config reference when there is nothing to migrate", () => {
const config = {
type: "entities",
entities: [{ entity: "sensor.temperature" }, { entity: "light.kitchen" }],
} as EntitiesCardConfig;
expect(migrateEntitiesCardConfig(config)).toBe(config);
});
it("passes string rows through untouched", () => {
const config = {
type: "entities",
entities: ["sensor.temperature", { entity: "sensor.x", format: "time" }],
} as EntitiesCardConfig;
const result = migrateEntitiesCardConfig(config);
expect(result.entities[0]).toBe("sensor.temperature");
expect(result.entities[1]).toEqual({
entity: "sensor.x",
time_format: "time",
});
});
it("keeps a pre-existing `time_format` over the legacy `format`", () => {
const config = {
type: "entities",
entities: [
{ entity: "sensor.x", format: "relative", time_format: "datetime" },
],
} as unknown as EntitiesCardConfig;
const result = migrateEntitiesCardConfig(config);
expect(result.entities[0]).toEqual({
entity: "sensor.x",
time_format: "datetime",
});
});
});
describe("migrateGlanceCardConfig", () => {
it("migrates the legacy `format` option to `time_format`", () => {
const config = {
type: "glance",
entities: [{ entity: "sensor.last_changed", format: "relative" }],
} as unknown as GlanceCardConfig;
const result = migrateGlanceCardConfig(config);
expect(result.entities).toEqual([
{ entity: "sensor.last_changed", time_format: "relative" },
]);
expect(result.entities[0]).not.toHaveProperty("format");
});
it("returns the same config reference when there is nothing to migrate", () => {
const config = {
type: "glance",
entities: ["sensor.temperature", { entity: "light.kitchen" }],
} as GlanceCardConfig;
expect(migrateGlanceCardConfig(config)).toBe(config);
});
it("keeps a pre-existing `time_format` over the legacy `format`", () => {
const config = {
type: "glance",
entities: [
{ entity: "sensor.x", format: "relative", time_format: "datetime" },
],
} as unknown as GlanceCardConfig;
const result = migrateGlanceCardConfig(config);
expect(result.entities[0]).toEqual({
entity: "sensor.x",
time_format: "datetime",
});
});
});
+151 -151
View File
@@ -2566,13 +2566,13 @@ __metadata:
languageName: node
linkType: hard
"@emnapi/core@npm:1.11.2":
version: 1.11.2
resolution: "@emnapi/core@npm:1.11.2"
"@emnapi/core@npm:1.11.1":
version: 1.11.1
resolution: "@emnapi/core@npm:1.11.1"
dependencies:
"@emnapi/wasi-threads": "npm:1.2.2"
tslib: "npm:^2.4.0"
checksum: 10/b3e9693f79ca1d8bb90cb8a950150c7738f54eca0ae1849d3d5103a87ce4e388c2669fb253cc123d3449ad2ae12583435455dbdc0270b1e9efb0cfd502c952b4
checksum: 10/9aba37e0c11a75ef8372fd0a9c6e5396f4e8c1ebdd6fee737414787610a9dc1cd9bf188f525153561ca9363896e1135dd240f1ce28f3470dba3ad7e683e6db1a
languageName: node
linkType: hard
@@ -2585,12 +2585,12 @@ __metadata:
languageName: node
linkType: hard
"@emnapi/runtime@npm:1.11.2":
version: 1.11.2
resolution: "@emnapi/runtime@npm:1.11.2"
"@emnapi/runtime@npm:1.11.1":
version: 1.11.1
resolution: "@emnapi/runtime@npm:1.11.1"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/280a219d3bf302615dabaaa248223b0e5fe9c49bacf0987dd958ca37ab425b62cf05287053cb188e711681418aaa5e447eaed6b62a0848c0a1303b2707864600
checksum: 10/8f7c622a49314df4d07952110e108e83b0fe129a8ddb9ef1e0ae372d754616169d5b0dd47a0d354a0fea2612abe42cedb582d15916936d1320c6c468acc804cc
languageName: node
linkType: hard
@@ -3434,10 +3434,10 @@ __metadata:
languageName: node
linkType: hard
"@lokalise/node-api@npm:16.1.0":
version: 16.1.0
resolution: "@lokalise/node-api@npm:16.1.0"
checksum: 10/f77f9e7d25af18be950f15750863061d00382f0abd2cfab1461b4354ba3d7ecc514e30811bc656e9cf382632d205c099d5d3d974b487dba75ff00dc9c1e193e0
"@lokalise/node-api@npm:16.0.0":
version: 16.0.0
resolution: "@lokalise/node-api@npm:16.0.0"
checksum: 10/88075629f30feb19537f8c40dabf7a85cc0539445322c216686f7e5a68ba6735a083d7ce1eee806ce31606d28ee6250ca39b185a783ce5ec263e1e0e71c3a701
languageName: node
linkType: hard
@@ -4735,110 +4735,110 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-darwin-arm64@npm:2.1.4"
"@rspack/binding-darwin-arm64@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-darwin-arm64@npm:2.1.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-darwin-x64@npm:2.1.4"
"@rspack/binding-darwin-x64@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-darwin-x64@npm:2.1.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.4"
"@rspack/binding-linux-arm64-gnu@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.3"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.4"
"@rspack/binding-linux-arm64-musl@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.3"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-gnu@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.4"
"@rspack/binding-linux-riscv64-gnu@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.3"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-musl@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.4"
"@rspack/binding-linux-riscv64-musl@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.3"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.4"
"@rspack/binding-linux-x64-gnu@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.3"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.4"
"@rspack/binding-linux-x64-musl@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.3"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.4"
"@rspack/binding-wasm32-wasi@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.3"
dependencies:
"@emnapi/core": "npm:1.11.2"
"@emnapi/runtime": "npm:1.11.2"
"@emnapi/core": "npm:1.11.1"
"@emnapi/runtime": "npm:1.11.1"
"@napi-rs/wasm-runtime": "npm:1.1.6"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.4"
"@rspack/binding-win32-arm64-msvc@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.4"
"@rspack/binding-win32-ia32-msvc@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.3"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.4"
"@rspack/binding-win32-x64-msvc@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/binding@npm:2.1.4"
"@rspack/binding@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/binding@npm:2.1.3"
dependencies:
"@rspack/binding-darwin-arm64": "npm:2.1.4"
"@rspack/binding-darwin-x64": "npm:2.1.4"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.4"
"@rspack/binding-linux-arm64-musl": "npm:2.1.4"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.4"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.4"
"@rspack/binding-linux-x64-gnu": "npm:2.1.4"
"@rspack/binding-linux-x64-musl": "npm:2.1.4"
"@rspack/binding-wasm32-wasi": "npm:2.1.4"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.4"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.4"
"@rspack/binding-win32-x64-msvc": "npm:2.1.4"
"@rspack/binding-darwin-arm64": "npm:2.1.3"
"@rspack/binding-darwin-x64": "npm:2.1.3"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.3"
"@rspack/binding-linux-arm64-musl": "npm:2.1.3"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.3"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.3"
"@rspack/binding-linux-x64-gnu": "npm:2.1.3"
"@rspack/binding-linux-x64-musl": "npm:2.1.3"
"@rspack/binding-wasm32-wasi": "npm:2.1.3"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.3"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.3"
"@rspack/binding-win32-x64-msvc": "npm:2.1.3"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -4864,15 +4864,15 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/425bf152dba708992ce16114ce6bc8dfa424071694e85201317518dce1899eeae873a2adbd23b214bb8d223f7290e000fd78ad9bf262c633e12d3d4ff73bbdcd
checksum: 10/7e499489ac0d882ba6573854d8bed7db8fd2ce8c95274a28527b2e8a8692c3c75e34c892cff9385082b3eafe199ececeb0878ab9314adcd54c5ed0db916e410b
languageName: node
linkType: hard
"@rspack/core@npm:2.1.4":
version: 2.1.4
resolution: "@rspack/core@npm:2.1.4"
"@rspack/core@npm:2.1.3":
version: 2.1.3
resolution: "@rspack/core@npm:2.1.3"
dependencies:
"@rspack/binding": "npm:2.1.4"
"@rspack/binding": "npm:2.1.3"
peerDependencies:
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
"@swc/helpers": ^0.5.23
@@ -4881,7 +4881,7 @@ __metadata:
optional: true
"@swc/helpers":
optional: true
checksum: 10/3c7aa9e8dbe8b132b51fc017a6c2c76ddf346cf663cad2a8912ba61469d80468acd045503d405bf962bb5ec44f44036512dd6d6233ea6d1419979e5c17e1dbd8
checksum: 10/24579403136732ba0710e83851c4ac557f3cab78bb80a58999b02c8a11cb6f0d64fd71010b4fb02824b124aa4da80af544f17e31b59b1a231993124c637cadf7
languageName: node
linkType: hard
@@ -5741,105 +5741,105 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.64.0"
"@typescript-eslint/eslint-plugin@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.63.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
"@typescript-eslint/scope-manager": "npm:8.64.0"
"@typescript-eslint/type-utils": "npm:8.64.0"
"@typescript-eslint/utils": "npm:8.64.0"
"@typescript-eslint/visitor-keys": "npm:8.64.0"
"@typescript-eslint/scope-manager": "npm:8.63.0"
"@typescript-eslint/type-utils": "npm:8.63.0"
"@typescript-eslint/utils": "npm:8.63.0"
"@typescript-eslint/visitor-keys": "npm:8.63.0"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
"@typescript-eslint/parser": ^8.64.0
"@typescript-eslint/parser": ^8.63.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/ec7cbcb44968386a4b9aff9272ce6b75bdcc7f398db5f2d07a21854baf0364ca33c74268c0e19d21386c6b87b9358b141df7bef3d26e4e4e7a9eb5f8f394dc75
checksum: 10/ab60da4c4a66e8b882b6d585457c6cd352492917940d43b255021f71fc057d327fe8e46856316b312a5c5b78b1245acb129869a7c60a7c2fa65e7822ef58c7c6
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/parser@npm:8.64.0"
"@typescript-eslint/parser@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/parser@npm:8.63.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.64.0"
"@typescript-eslint/types": "npm:8.64.0"
"@typescript-eslint/typescript-estree": "npm:8.64.0"
"@typescript-eslint/visitor-keys": "npm:8.64.0"
"@typescript-eslint/scope-manager": "npm:8.63.0"
"@typescript-eslint/types": "npm:8.63.0"
"@typescript-eslint/typescript-estree": "npm:8.63.0"
"@typescript-eslint/visitor-keys": "npm:8.63.0"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/7239b16a6ca6bb1764ad04bc1eb46997336541d049fcffb4966ef404fbb02324b2b33aed50c2b976359ec8d3c97b90668cfd6aa9177228b4b799152f01a3904a
checksum: 10/40ee2a1c703898b9c6df0731e5584ce0bf63d8c13fb464a952500dae156b68b41b522b2cb4755a2f0cb7e0d529b0888e3ba783e3eeb1236c062a3a7ca227f634
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/project-service@npm:8.64.0"
"@typescript-eslint/project-service@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/project-service@npm:8.63.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.64.0"
"@typescript-eslint/types": "npm:^8.64.0"
"@typescript-eslint/tsconfig-utils": "npm:^8.63.0"
"@typescript-eslint/types": "npm:^8.63.0"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/511b9a8f1dcb32c99003cab9791309056ac6e889fe46227600deb743e869a63b3e78d7d2d3ef1bf65b2d5e574b73add3040fbef4c0f94aed587368aaea149559
checksum: 10/16b07d0e95abfbe56eccc7c67185caf43e9059740c82a09a0f0f4393539f89a19f8c1b996d396f9476401af55bd804897fd19c988712db3e0f36562d2805c223
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/scope-manager@npm:8.64.0"
"@typescript-eslint/scope-manager@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/scope-manager@npm:8.63.0"
dependencies:
"@typescript-eslint/types": "npm:8.64.0"
"@typescript-eslint/visitor-keys": "npm:8.64.0"
checksum: 10/3c72c4915cee19d632ddc7491c3a4668dd04aa8bcb112fde8ac10aea2cc0364aaa8439d9939d0c62a7b4159fc22f4ced7cba3a77df4422b21d76497068f08076
"@typescript-eslint/types": "npm:8.63.0"
"@typescript-eslint/visitor-keys": "npm:8.63.0"
checksum: 10/6b0183e85d79e287660274d39fed1fc8321ae03ae40545acaad6718fd2524063d302f3348c04576d038a25f16f8c9ca4e8780a0ad754f1a494a00027e867deda
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.64.0, @typescript-eslint/tsconfig-utils@npm:^8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.64.0"
"@typescript-eslint/tsconfig-utils@npm:8.63.0, @typescript-eslint/tsconfig-utils@npm:^8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.63.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/905469c5067a9a2d18d065848c6497081ab787b22a9d7c06499f4bb593b7d67f9fb17e7861a114c46626555853cc52d4542db5311c8dc7cd138e45461a73e589
checksum: 10/7c23671159f8593b676d53bc52b4a3bd4c3ad62abe2ad99b379b70298abf3b784cdcf18ca2b10f2aa62a9f8e0232e6efb87364940a3bd4a32126a357f39ac3c8
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/type-utils@npm:8.64.0"
"@typescript-eslint/type-utils@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/type-utils@npm:8.63.0"
dependencies:
"@typescript-eslint/types": "npm:8.64.0"
"@typescript-eslint/typescript-estree": "npm:8.64.0"
"@typescript-eslint/utils": "npm:8.64.0"
"@typescript-eslint/types": "npm:8.63.0"
"@typescript-eslint/typescript-estree": "npm:8.63.0"
"@typescript-eslint/utils": "npm:8.63.0"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/7ab1fd8c0d292c0155cf137d316b1618681eca0fe4cf446a05d909de41311ec6bb64fed82513e822063a962508d5b3e615fb0155a4a565d6b9c6cb81d06a5cd2
checksum: 10/8a1001edb129aec55c4ef8567f5f33af4b0ee92e89463f45e7d8a006872e438db1a3cbe2364769d205124d9d143018c920e165868788a0e7aee15466ed6d38f0
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.64.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/types@npm:8.64.0"
checksum: 10/b8951c00ce9b9702f3201f017354774ea5f39c30c2b6f815cb50d91f53f61c325e30f548329daf731d5169d752095cf102da54b754fb202bcfb619faa56fa9f4
"@typescript-eslint/types@npm:8.63.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/types@npm:8.63.0"
checksum: 10/f72eb114970cae0da8e53f68cd8dca1b51ac410f4438141a2851655fa07aacb3090e24a2ae53462cf0970e4d5b52cabf9693aa6f07abdff5c210874806427e5b
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/typescript-estree@npm:8.64.0"
"@typescript-eslint/typescript-estree@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/typescript-estree@npm:8.63.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.64.0"
"@typescript-eslint/tsconfig-utils": "npm:8.64.0"
"@typescript-eslint/types": "npm:8.64.0"
"@typescript-eslint/visitor-keys": "npm:8.64.0"
"@typescript-eslint/project-service": "npm:8.63.0"
"@typescript-eslint/tsconfig-utils": "npm:8.63.0"
"@typescript-eslint/types": "npm:8.63.0"
"@typescript-eslint/visitor-keys": "npm:8.63.0"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
semver: "npm:^7.7.3"
@@ -5847,32 +5847,32 @@ __metadata:
ts-api-utils: "npm:^2.5.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/833101d25e820d1d0e317dfc9beca985b3c87e5458b20ed002c86c2d425667aa506f756f1759b54f724033effb529266f836f912c460ee662f347fb43dded6ed
checksum: 10/793e0227698f4d8b325922816f27a4e737760aae255874c2021c5e2179499fd036b9e87bf0346ead6360444fb7ab4815bb558c94a17d8481632351bbdb2780bf
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/utils@npm:8.64.0"
"@typescript-eslint/utils@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/utils@npm:8.63.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
"@typescript-eslint/scope-manager": "npm:8.64.0"
"@typescript-eslint/types": "npm:8.64.0"
"@typescript-eslint/typescript-estree": "npm:8.64.0"
"@typescript-eslint/scope-manager": "npm:8.63.0"
"@typescript-eslint/types": "npm:8.63.0"
"@typescript-eslint/typescript-estree": "npm:8.63.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/ff7e5374bd6ef60d7df723e6440468e3a5d4879d1d9876e322904319a1f1d2f98d858fc9b9169dbbd7ee0786a07102a058f13e2b6c16d1bf9aae9eb836a258a3
checksum: 10/017bd1998822902b5dde5af9e031908aeb8861565b947aa217d5641bac4758fb831c52629b600bb58a990fcfef1ca58eaf5b44595575bb71a43761c2f40a7f56
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.64.0":
version: 8.64.0
resolution: "@typescript-eslint/visitor-keys@npm:8.64.0"
"@typescript-eslint/visitor-keys@npm:8.63.0":
version: 8.63.0
resolution: "@typescript-eslint/visitor-keys@npm:8.63.0"
dependencies:
"@typescript-eslint/types": "npm:8.64.0"
"@typescript-eslint/types": "npm:8.63.0"
eslint-visitor-keys: "npm:^5.0.0"
checksum: 10/d149be4ac0e67c51097cdb7335d8e2d29b9dc0de173961c5cc550e938a00a3af28b1f8ecd7ad832fcfe489d1b11e36fdd394b6ea92115a7558123562e31a704f
checksum: 10/9edcff478ba98f81f843b0c02874f3ef58c15b1001b2cf74d878416340c460a7c6e37442b4bee3ef226f0029131d620ee05a9ee3d19ec694ffa07fcf9a7a03ca
languageName: node
linkType: hard
@@ -9470,10 +9470,10 @@ __metadata:
languageName: node
linkType: hard
"fuse.js@npm:7.5.0":
version: 7.5.0
resolution: "fuse.js@npm:7.5.0"
checksum: 10/44dafab623a1144971ccbb380d6874ef6c51a73c85474016b0d47d6ad732d819cdb070f056149cd968b5304e7c0975f1890aaf98f3c4def2e9c21aebe4d62e23
"fuse.js@npm:7.4.2":
version: 7.4.2
resolution: "fuse.js@npm:7.4.2"
checksum: 10/1605bb929331056f9215a8f0a19b2d22615d9195e17794f41254bdeeb77b0145ed0ad6b6842227b329783a4ab53586d795924e7a4a3fb664295f365ef6e2405c
languageName: node
linkType: hard
@@ -9971,7 +9971,7 @@ __metadata:
"@lit/context": "npm:1.1.6"
"@lit/reactive-element": "npm:2.1.2"
"@lit/task": "npm:1.0.3"
"@lokalise/node-api": "npm:16.1.0"
"@lokalise/node-api": "npm:16.0.0"
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch"
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
"@material/web": "npm:2.4.1"
@@ -9983,7 +9983,7 @@ __metadata:
"@playwright/test": "npm:1.61.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.5.18"
"@rspack/core": "npm:2.1.4"
"@rspack/core": "npm:2.1.3"
"@rspack/dev-server": "npm:2.1.0"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
@@ -10036,7 +10036,7 @@ __metadata:
eslint-plugin-wc: "npm:3.1.0"
fancy-log: "npm:2.0.0"
fs-extra: "npm:11.3.6"
fuse.js: "npm:7.5.0"
fuse.js: "npm:7.4.2"
generate-license-file: "npm:4.2.1"
glob: "npm:13.0.6"
globals: "npm:17.7.0"
@@ -10089,7 +10089,7 @@ __metadata:
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:6.0.3"
typescript-eslint: "npm:8.64.0"
typescript-eslint: "npm:8.63.0"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.1.10"
webpack-stats-plugin: "npm:1.1.3"
@@ -15088,18 +15088,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.64.0":
version: 8.64.0
resolution: "typescript-eslint@npm:8.64.0"
"typescript-eslint@npm:8.63.0":
version: 8.63.0
resolution: "typescript-eslint@npm:8.63.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.64.0"
"@typescript-eslint/parser": "npm:8.64.0"
"@typescript-eslint/typescript-estree": "npm:8.64.0"
"@typescript-eslint/utils": "npm:8.64.0"
"@typescript-eslint/eslint-plugin": "npm:8.63.0"
"@typescript-eslint/parser": "npm:8.63.0"
"@typescript-eslint/typescript-estree": "npm:8.63.0"
"@typescript-eslint/utils": "npm:8.63.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/7812e25506003c2a5b395a51ca3c31929028485d191049e6a813e65829e79fcebb07251ba42c5d75af8d15233a5bbb44dd37138345787779eabe07b88fcf75ec
checksum: 10/ab0f3324cbf6ab910903977b9364138c67ccad35675962702bfbe052231440439dcdc50de0c01a1eb1ccecde796a4e117858b10166d9d1153b3b2d82c73a92ba
languageName: node
linkType: hard