diff --git a/build-scripts/webpack.cjs b/build-scripts/webpack.cjs
index e1047cadd4..b08d3b2eeb 100644
--- a/build-scripts/webpack.cjs
+++ b/build-scripts/webpack.cjs
@@ -161,6 +161,7 @@ const createWebpackConfig = ({
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
+ "lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js",
diff --git a/gallery/src/pages/automation/describe-action.ts b/gallery/src/pages/automation/describe-action.ts
index 7deccf6927..3340429dc4 100644
--- a/gallery/src/pages/automation/describe-action.ts
+++ b/gallery/src/pages/automation/describe-action.ts
@@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${this._action
- ? describeAction(this.hass, [], this._action)
+ ? describeAction(this.hass, [], [], [], this._action)
: ""}
html`
-
${describeAction(this.hass, [], conf as any)}
+
${describeAction(this.hass, [], [], [], conf as any)}
${dump(conf)}
`
diff --git a/pyproject.toml b/pyproject.toml
index 92bef3e682..71b4ce5600 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
-version = "20240403.1"
+version = "20240404.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
diff --git a/src/components/ha-filter-integrations.ts b/src/components/ha-filter-integrations.ts
index cab9726f44..748cb87eeb 100644
--- a/src/components/ha-filter-integrations.ts
+++ b/src/components/ha-filter-integrations.ts
@@ -1,4 +1,3 @@
-import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -56,11 +55,7 @@ export class HaFilterIntegrations extends LitElement {
@value-changed=${this._handleSearchChange}
>
-
+
${repeat(
this._integrations(this._manifests, this._filter, this.value),
(i) => i.domain,
@@ -92,7 +87,7 @@ export class HaFilterIntegrations extends LitElement {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height =
- `${this.clientHeight - 49}px`;
+ `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
}, 300);
}
}
@@ -131,34 +126,21 @@ export class HaFilterIntegrations extends LitElement {
)
);
- private async _integrationsSelected(
- ev: CustomEvent>>
- ) {
- const integrations = this._integrations(
- this._manifests!,
- this._filter,
- this.value
- );
-
- if (!ev.detail.index.size) {
- fireEvent(this, "data-table-filter-changed", {
- value: [],
- items: undefined,
- });
- this.value = [];
+ private _handleItemClick(ev) {
+ const listItem = ev.target.closest("ha-check-list-item");
+ const value = listItem?.value;
+ if (!value) {
return;
}
-
- const value: string[] = [];
-
- for (const index of ev.detail.index) {
- const domain = integrations[index].domain;
- value.push(domain);
+ if (this.value?.includes(value)) {
+ this.value = this.value?.filter((val) => val !== value);
+ } else {
+ this.value = [...(this.value || []), value];
}
- this.value = value;
+ listItem.selected = this.value?.includes(value);
fireEvent(this, "data-table-filter-changed", {
- value,
+ value: this.value,
items: undefined,
});
}
diff --git a/src/components/ha-outlined-field.ts b/src/components/ha-outlined-field.ts
new file mode 100644
index 0000000000..c0df46cf29
--- /dev/null
+++ b/src/components/ha-outlined-field.ts
@@ -0,0 +1,40 @@
+import { MdOutlinedField } from "@material/web/field/outlined-field";
+import "element-internals-polyfill";
+import { css } from "lit";
+import { customElement } from "lit/decorators";
+import { literal } from "lit/static-html";
+
+@customElement("ha-outlined-field")
+export class HaOutlinedField extends MdOutlinedField {
+ protected readonly fieldTag = literal`ha-outlined-field`;
+
+ static override styles = [
+ ...super.styles,
+ css`
+ .container::before {
+ display: block;
+ content: "";
+ position: absolute;
+ inset: 0;
+ background-color: var(--ha-outlined-field-container-color, transparent);
+ opacity: var(--ha-outlined-field-container-opacity, 1);
+ border-start-start-radius: var(--_container-shape-start-start);
+ border-start-end-radius: var(--_container-shape-start-end);
+ border-end-start-radius: var(--_container-shape-end-start);
+ border-end-end-radius: var(--_container-shape-end-end);
+ }
+ .with-start .start {
+ margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
+ }
+ .with-end .end {
+ margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
+ }
+ `,
+ ];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-outlined-field": HaOutlinedField;
+ }
+}
diff --git a/src/components/ha-outlined-text-field.ts b/src/components/ha-outlined-text-field.ts
index 02ed120d6e..d1645b5dd7 100644
--- a/src/components/ha-outlined-text-field.ts
+++ b/src/components/ha-outlined-text-field.ts
@@ -2,9 +2,13 @@ import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field
import "element-internals-polyfill";
import { css } from "lit";
import { customElement } from "lit/decorators";
+import { literal } from "lit/static-html";
+import "./ha-outlined-field";
@customElement("ha-outlined-text-field")
export class HaOutlinedTextField extends MdOutlinedTextField {
+ protected readonly fieldTag = literal`ha-outlined-field`;
+
static override styles = [
...super.styles,
css`
@@ -25,12 +29,10 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
--md-outlined-field-container-shape-end-end: 10px;
--md-outlined-field-container-shape-end-start: 10px;
--md-outlined-field-focus-outline-width: 1px;
+ --ha-outlined-field-start-margin: -4px;
+ --ha-outlined-field-end-margin: -4px;
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
}
- md-outlined-field {
- background: var(--ha-outlined-text-field-container-color, transparent);
- opacity: var(--ha-outlined-text-field-container-opacity, 1);
- }
.input {
font-family: Roboto, sans-serif;
}
diff --git a/src/components/search-input-outlined.ts b/src/components/search-input-outlined.ts
index f0ce00c673..693be1feac 100644
--- a/src/components/search-input-outlined.ts
+++ b/src/components/search-input-outlined.ts
@@ -97,7 +97,7 @@ class SearchInputOutlined extends LitElement {
ha-outlined-text-field {
display: block;
width: 100%;
- --ha-outlined-text-field-container-color: var(--card-background-color);
+ --ha-outlined-field-container-color: var(--card-background-color);
}
ha-svg-icon,
ha-icon-button {
diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts
index 78eb1ec8e1..eab5998d1d 100644
--- a/src/components/trace/hat-trace-timeline.ts
+++ b/src/components/trace/hat-trace-timeline.ts
@@ -1,3 +1,4 @@
+import { consume } from "@lit-labs/context";
import {
mdiAlertCircle,
mdiCircle,
@@ -6,14 +7,13 @@ import {
mdiProgressWrench,
mdiRecordCircleOutline,
} from "@mdi/js";
-import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
- css,
CSSResultGroup,
- html,
LitElement,
PropertyValues,
TemplateResult,
+ css,
+ html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -23,27 +23,31 @@ import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
- EntityRegistryEntry,
- subscribeEntityRegistry,
-} from "../../data/entity_registry";
+ floorsContext,
+ fullEntitiesContext,
+ labelsContext,
+} from "../../data/context";
+import { EntityRegistryEntry } from "../../data/entity_registry";
+import { FloorRegistryEntry } from "../../data/floor_registry";
+import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
ChooseAction,
ChooseActionChoice,
- getActionType,
IfAction,
ParallelAction,
RepeatAction,
+ getActionType,
} from "../../data/script";
import { describeAction } from "../../data/script_i18n";
import {
ActionTraceStep,
AutomationTraceExtended,
ChooseActionTraceStep,
- getDataFromPath,
IfActionTraceStep,
- isTriggerPath,
TriggerTraceStep,
+ getDataFromPath,
+ isTriggerPath,
} from "../../data/trace";
import { HomeAssistant } from "../../types";
import "./ha-timeline";
@@ -200,6 +204,8 @@ class ActionRenderer {
constructor(
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
+ private labelReg: LabelRegistryEntry[],
+ private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -310,7 +316,14 @@ class ActionRenderer {
this._renderEntry(
path,
- describeAction(this.hass, this.entityReg, data, actionType),
+ describeAction(
+ this.hass,
+ this.entityReg,
+ this.labelReg,
+ this.floorReg,
+ data,
+ actionType
+ ),
undefined,
data.enabled === false
);
@@ -475,7 +488,13 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
- describeAction(this.hass, this.entityReg, repeatConfig);
+ describeAction(
+ this.hass,
+ this.entityReg,
+ this.labelReg,
+ this.floorReg,
+ repeatConfig
+ );
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -631,15 +650,17 @@ export class HaAutomationTracer extends LitElement {
@property({ type: Boolean }) public allowPick = false;
- @state() private _entityReg: EntityRegistryEntry[] = [];
+ @state()
+ @consume({ context: fullEntitiesContext, subscribe: true })
+ _entityReg!: EntityRegistryEntry[];
- public hassSubscribe(): UnsubscribeFunc[] {
- return [
- subscribeEntityRegistry(this.hass.connection!, (entities) => {
- this._entityReg = entities;
- }),
- ];
- }
+ @state()
+ @consume({ context: labelsContext, subscribe: true })
+ _labelReg!: LabelRegistryEntry[];
+
+ @state()
+ @consume({ context: floorsContext, subscribe: true })
+ _floorReg!: FloorRegistryEntry[];
protected render() {
if (!this.trace) {
@@ -657,6 +678,8 @@ export class HaAutomationTracer extends LitElement {
const actionRenderer = new ActionRenderer(
this.hass,
this._entityReg,
+ this._labelReg,
+ this._floorReg,
entries,
this.trace,
logbookRenderer,
diff --git a/src/data/context.ts b/src/data/context.ts
index b5d914522c..75ebe5ae3d 100644
--- a/src/data/context.ts
+++ b/src/data/context.ts
@@ -2,6 +2,8 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
+import { FloorRegistryEntry } from "./floor_registry";
+import { LabelRegistryEntry } from "./label_registry";
export const connectionContext =
createContext("connection");
@@ -25,3 +27,7 @@ export const panelsContext = createContext("panels");
export const fullEntitiesContext =
createContext("extendedEntities");
+
+export const floorsContext = createContext("floors");
+
+export const labelsContext = createContext("labels");
diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts
index 22d5e3c0a7..b08cb49c49 100644
--- a/src/data/script_i18n.ts
+++ b/src/data/script_i18n.ts
@@ -14,7 +14,9 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
+import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
+import { LabelRegistryEntry } from "./label_registry";
import {
ActionType,
ActionTypes,
@@ -40,6 +42,8 @@ const actionTranslationBaseKey =
export const describeAction = (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
+ labelRegistry: LabelRegistryEntry[],
+ floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -48,6 +52,8 @@ export const describeAction = (
return tryDescribeAction(
hass,
entityRegistry,
+ labelRegistry,
+ floorRegistry,
action,
actionType,
ignoreAlias
@@ -66,6 +72,8 @@ export const describeAction = (
const tryDescribeAction = (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
+ labelRegistry: LabelRegistryEntry[],
+ floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -82,10 +90,12 @@ const tryDescribeAction = (
const targets: string[] = [];
if (config.target) {
- for (const [key, label] of Object.entries({
+ for (const [key, name] of Object.entries({
area_id: "areas",
device_id: "devices",
entity_id: "entities",
+ floor_id: "floors",
+ label_id: "labels",
})) {
if (!(key in config.target)) {
continue;
@@ -99,7 +109,7 @@ const tryDescribeAction = (
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
- { name: label }
+ { name }
)
);
break;
@@ -147,6 +157,32 @@ const tryDescribeAction = (
)
);
}
+ } else if (key === "floor_id") {
+ const floor = floorRegistry.find(
+ (flr) => flr.floor_id === targetThing
+ );
+ if (floor?.name) {
+ targets.push(floor.name);
+ } else {
+ targets.push(
+ hass.localize(
+ `${actionTranslationBaseKey}.service.description.target_unknown_floor`
+ )
+ );
+ }
+ } else if (key === "label_id") {
+ const label = labelRegistry.find(
+ (lbl) => lbl.label_id === targetThing
+ );
+ if (label?.name) {
+ targets.push(label.name);
+ } else {
+ targets.push(
+ hass.localize(
+ `${actionTranslationBaseKey}.service.description.target_unknown_label`
+ )
+ );
+ }
} else {
targets.push(targetThing);
}
diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts
index e5cf082d3c..2c7aab1dc1 100644
--- a/src/layouts/hass-tabs-subpage-data-table.ts
+++ b/src/layouts/hass-tabs-subpage-data-table.ts
@@ -496,8 +496,9 @@ export class HaTabsSubpageDataTable extends LitElement {
${this.showFilters && !showPane
? html`
${localize("ui.components.subpage-data-table.filters")}${localize("ui.components.subpage-data-table.filters", {
+ number: this.data.length,
+ })}
${this.filters
? html`
-
`
+
+
+
+
+ ${this.hass.localize(
+ "ui.components.subpage-data-table.show_results",
+ { number: this.data.length }
+ )}
+
+
+ `
: nothing}
`;
}
@@ -779,7 +791,6 @@ export class HaTabsSubpageDataTable extends LitElement {
}
ha-dialog {
- --dialog-z-index: 100;
--mdc-dialog-min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
@@ -794,7 +805,7 @@ export class HaTabsSubpageDataTable extends LitElement {
}
.filter-dialog-content {
- height: calc(100vh - 1px - var(--header-height));
+ height: calc(100vh - 1px - 61px - var(--header-height));
display: flex;
flex-direction: column;
}
diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts
index 6986aba974..bac8c011fb 100644
--- a/src/panels/config/automation/action/ha-automation-action-row.ts
+++ b/src/panels/config/automation/action/ha-automation-action-row.ts
@@ -42,8 +42,14 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
-import { fullEntitiesContext } from "../../../../data/context";
+import {
+ floorsContext,
+ fullEntitiesContext,
+ labelsContext,
+} from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
+import { FloorRegistryEntry } from "../../../../data/floor_registry";
+import { LabelRegistryEntry } from "../../../../data/label_registry";
import {
Action,
NonConditionAction,
@@ -146,6 +152,14 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
+ @state()
+ @consume({ context: labelsContext, subscribe: true })
+ _labelReg!: LabelRegistryEntry[];
+
+ @state()
+ @consume({ context: floorsContext, subscribe: true })
+ _floorReg!: FloorRegistryEntry[];
+
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@@ -210,7 +224,13 @@ export default class HaAutomationActionRow extends LitElement {
.path=${ACTION_ICONS[type!]}
>`}
${capitalizeFirstLetter(
- describeAction(this.hass, this._entityReg, this.action)
+ describeAction(
+ this.hass,
+ this._entityReg,
+ this._labelReg,
+ this._floorReg,
+ this.action
+ )
)}
@@ -573,7 +593,15 @@ export default class HaAutomationActionRow extends LitElement {
),
inputType: "string",
placeholder: capitalizeFirstLetter(
- describeAction(this.hass, this._entityReg, this.action, undefined, true)
+ describeAction(
+ this.hass,
+ this._entityReg,
+ this._labelReg,
+ this._floorReg,
+ this.action,
+ undefined,
+ true
+ )
),
defaultValue: this.action.alias,
confirmText: this.hass.localize("ui.common.submit"),
diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts
index ecb40f5b71..9ec4cb6b01 100644
--- a/src/panels/config/automation/ha-automation-picker.ts
+++ b/src/panels/config/automation/ha-automation-picker.ts
@@ -422,6 +422,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
+ const automations = this._automations(
+ this.automations,
+ this._entityReg,
+ this.hass.areas,
+ this._categories,
+ this._labels,
+ this._filteredAutomations
+ );
return html`
filter.value?.length)
- .length
+ Object.values(this._filters).filter((filter) =>
+ Array.isArray(filter.value)
+ ? filter.value.length
+ : filter.value &&
+ Object.values(filter.value).some((val) =>
+ Array.isArray(val) ? val.length : val
+ )
+ ).length
}
.columns=${this._columns(
this.narrow,
@@ -446,14 +464,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this.hass.locale
)}
initialGroupColumn="category"
- .data=${this._automations(
- this.automations,
- this._entityReg,
- this.hass.areas,
- this._categories,
- this._labels,
- this._filteredAutomations
- )}
+ .data=${automations}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize(
diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts
index f9f2fd16e6..68c402ee5c 100644
--- a/src/panels/config/devices/ha-config-devices-dashboard.ts
+++ b/src/panels/config/devices/ha-config-devices-dashboard.ts
@@ -591,7 +591,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.tabs=${configSections.devices}
.route=${this.route}
.searchLabel=${this.hass.localize(
- "ui.panel.config.devices.picker.search"
+ "ui.panel.config.devices.picker.search",
+ { number: devicesOutput.length }
)}
.columns=${this._columns(this.hass.localize, this.narrow)}
.data=${devicesOutput}
@@ -600,8 +601,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@selection-changed=${this._handleSelectionChanged}
.filter=${this._filter}
hasFilters
- .filters=${Object.values(this._filters).filter(
- (filter) => filter.value?.length
+ .filters=${Object.values(this._filters).filter((filter) =>
+ Array.isArray(filter.value)
+ ? filter.value.length
+ : filter.value &&
+ Object.values(filter.value).some((val) =>
+ Array.isArray(val) ? val.length : val
+ )
).length}
@clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange}
diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts
index 5a7f5461aa..e98bc7b037 100644
--- a/src/panels/config/entities/ha-config-entities.ts
+++ b/src/panels/config/entities/ha-config-entities.ts
@@ -27,7 +27,6 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
-import { until } from "lit/directives/until";
import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -67,7 +66,6 @@ import {
removeEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
-import { entryIcon } from "../../../data/icons";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
@@ -207,21 +205,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon",
template: (entry) =>
entry.icon
- ? html`
-
- `
- : html`
-
- `,
+ ? html``
+ : entry.entity
+ ? html`
+
+ `
+ : html``,
},
name: {
main: true,
@@ -577,12 +573,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)}
.data=${filteredEntities}
.searchLabel=${this.hass.localize(
- "ui.panel.config.entities.picker.search"
+ "ui.panel.config.entities.picker.search",
+ { number: filteredEntities.length }
)}
hasFilters
.filters=${
- Object.values(this._filters).filter((filter) => filter.value?.length)
- .length
+ Object.values(this._filters).filter((filter) =>
+ Array.isArray(filter.value)
+ ? filter.value.length
+ : filter.value &&
+ Object.values(filter.value).some((val) =>
+ Array.isArray(val) ? val.length : val
+ )
+ ).length
}
.filter=${this._filter}
selectable
diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts
index f7fd2fd195..fe3151b260 100644
--- a/src/panels/config/ha-panel-config.ts
+++ b/src/panels/config/ha-panel-config.ts
@@ -35,7 +35,11 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
-import { fullEntitiesContext } from "../../data/context";
+import {
+ floorsContext,
+ fullEntitiesContext,
+ labelsContext,
+} from "../../data/context";
import {
entityRegistryByEntityId,
entityRegistryById,
@@ -45,6 +49,8 @@ import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../types";
+import { subscribeLabelRegistry } from "../../data/label_registry";
+import { subscribeFloorRegistry } from "../../data/floor_registry";
declare global {
// for fire event
@@ -379,11 +385,27 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
initialValue: [],
});
+ private _labelsContext = new ContextProvider(this, {
+ context: labelsContext,
+ initialValue: [],
+ });
+
+ private _floorsContext = new ContextProvider(this, {
+ context: floorsContext,
+ initialValue: [],
+ });
+
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entitiesContext.setValue(entities);
}),
+ subscribeLabelRegistry(this.hass.connection!, (labels) => {
+ this._labelsContext.setValue(labels);
+ }),
+ subscribeFloorRegistry(this.hass.connection!, (floors) => {
+ this._floorsContext.setValue(floors);
+ }),
];
}
diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts
index b32fa20750..e44659e34b 100644
--- a/src/panels/config/helpers/ha-config-helpers.ts
+++ b/src/panels/config/helpers/ha-config-helpers.ts
@@ -486,6 +486,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
+ const helpers = this._getItems(
+ this.hass.localize,
+ this._stateItems,
+ this._entityEntries,
+ this._configEntries,
+ this._entityReg,
+ this._categories,
+ this._labels,
+ this._filteredStateItems
+ );
return html`
filter.value?.length
+ .filters=${Object.values(this._filters).filter((filter) =>
+ Array.isArray(filter.value)
+ ? filter.value.length
+ : filter.value &&
+ Object.values(filter.value).some((val) =>
+ Array.isArray(val) ? val.length : val
+ )
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
- .data=${this._getItems(
- this.hass.localize,
- this._stateItems,
- this._entityEntries,
- this._configEntries,
- this._entityReg,
- this._categories,
- this._labels,
- this._filteredStateItems
- )}
+ .data=${helpers}
initialGroupColumn="category"
.activeFilters=${this._activeFilters}
@clear-filter=${this._clearFilter}
diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts
index c07d31f7be..b798c0e3de 100644
--- a/src/panels/config/scene/ha-scene-dashboard.ts
+++ b/src/panels/config/scene/ha-scene-dashboard.ts
@@ -425,6 +425,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
+ const scenes = this._scenes(
+ this.scenes,
+ this._entityReg,
+ this.hass.areas,
+ this._categories,
+ this._labels,
+ this._filteredScenes
+ );
return html`
filter.value?.length
+ .filters=${Object.values(this._filters).filter((filter) =>
+ Array.isArray(filter.value)
+ ? filter.value.length
+ : filter.value &&
+ Object.values(filter.value).some((val) =>
+ Array.isArray(val) ? val.length : val
+ )
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
id="entity_id"
initialGroupColumn="category"
- .data=${this._scenes(
- this.scenes,
- this._entityReg,
- this.hass.areas,
- this._categories,
- this._labels,
- this._filteredScenes
- )}
+ .data=${scenes}
.empty=${!this.scenes.length}
.activeFilters=${this._activeFilters}
.noDataText=${this.hass.localize(
diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts
index 2e769e8371..941d78a417 100644
--- a/src/panels/config/script/ha-script-picker.ts
+++ b/src/panels/config/script/ha-script-picker.ts
@@ -437,6 +437,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
+ const scripts = this._scripts(
+ this.scripts,
+ this._entityReg,
+ this.hass.areas,
+ this._categories,
+ this._labels,
+ this._filteredScripts
+ );
return html`
filter.value?.length
+ .filters=${Object.values(this._filters).filter((filter) =>
+ Array.isArray(filter.value)
+ ? filter.value.length
+ : filter.value &&
+ Object.values(filter.value).some((val) =>
+ Array.isArray(val) ? val.length : val
+ )
).length}
.columns=${this._columns(
this.narrow,
this.hass.localize,
this.hass.locale
)}
- .data=${this._scripts(
- this.scripts,
- this._entityReg,
- this.hass.areas,
- this._categories,
- this._labels,
- this._filteredScripts
- )}
+ .data=${scripts}
.empty=${!this.scripts.length}
.activeFilters=${this._activeFilters}
id="entity_id"
diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts
index fde97f8aeb..0fe7fc98c5 100644
--- a/src/panels/history/ha-panel-history.ts
+++ b/src/panels/history/ha-panel-history.ts
@@ -9,6 +9,7 @@ import { property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
+import { computeDomain } from "../../common/entity/compute_domain";
import { navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
@@ -27,37 +28,29 @@ import "../../components/ha-menu-button";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import {
- AreaDeviceLookup,
- AreaEntityLookup,
- getAreaDeviceLookup,
- getAreaEntityLookup,
-} from "../../data/area_registry";
-import {
- DeviceEntityLookup,
- getDeviceEntityLookup,
- subscribeDeviceRegistry,
-} from "../../data/device_registry";
-import { subscribeEntityRegistry } from "../../data/entity_registry";
-import {
- HistoryResult,
- computeHistory,
- subscribeHistory,
- HistoryStates,
EntityHistoryState,
+ HistoryResult,
+ HistoryStates,
+ LineChartState,
LineChartUnit,
computeGroupKey,
- LineChartState,
+ computeHistory,
+ subscribeHistory,
} from "../../data/history";
-import { fetchStatistics, Statistics } from "../../data/recorder";
+import { Statistics, fetchStatistics } from "../../data/recorder";
+import {
+ expandAreaTarget,
+ expandDeviceTarget,
+ expandFloorTarget,
+ expandLabelTarget,
+} from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
-import { SubscribeMixin } from "../../mixins/subscribe-mixin";
+import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
-import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
-import { computeDomain } from "../../common/entity/compute_domain";
-class HaPanelHistory extends SubscribeMixin(LitElement) {
+class HaPanelHistory extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@property({ reflect: true, type: Boolean }) public narrow = false;
@@ -83,12 +76,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
@state() private _statisticsHistory?: HistoryResult;
- @state() private _deviceEntityLookup?: DeviceEntityLookup;
-
- @state() private _areaEntityLookup?: AreaEntityLookup;
-
- @state() private _areaDeviceLookup?: AreaDeviceLookup;
-
@state()
private _showBack?: boolean;
@@ -123,18 +110,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
this._unsubscribeHistory();
}
- public hassSubscribe(): UnsubscribeFunc[] {
- return [
- subscribeEntityRegistry(this.hass.connection!, (entities) => {
- this._deviceEntityLookup = getDeviceEntityLookup(entities);
- this._areaEntityLookup = getAreaEntityLookup(entities);
- }),
- subscribeDeviceRegistry(this.hass.connection!, (devices) => {
- this._areaDeviceLookup = getAreaDeviceLookup(devices);
- }),
- ];
- }
-
private _goBack(): void {
history.back();
}
@@ -332,7 +307,9 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
const entityIds = searchParams.entity_id;
const deviceIds = searchParams.device_id;
const areaIds = searchParams.area_id;
- if (entityIds || deviceIds || areaIds) {
+ const floorIds = searchParams.floor_id;
+ const labelsIds = searchParams.label_id;
+ if (entityIds || deviceIds || areaIds || floorIds || labelsIds) {
this._targetPickerValue = {};
}
if (entityIds) {
@@ -347,6 +324,14 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
const splitIds = areaIds.split(",");
this._targetPickerValue!.area_id = splitIds;
}
+ if (floorIds) {
+ const splitIds = floorIds.split(",");
+ this._targetPickerValue!.floor_id = splitIds;
+ }
+ if (labelsIds) {
+ const splitIds = labelsIds.split(",");
+ this._targetPickerValue!.label_id = splitIds;
+ }
const startDate = searchParams.start_date;
if (startDate) {
@@ -522,95 +507,77 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
private _getEntityIds(): string[] {
return this.__getEntityIds(
this._targetPickerValue,
- this._deviceEntityLookup,
- this._areaEntityLookup,
- this._areaDeviceLookup
+ this.hass.entities,
+ this.hass.devices,
+ this.hass.areas
);
}
private __getEntityIds = memoizeOne(
(
targetPickerValue: HassServiceTarget,
- deviceEntityLookup: DeviceEntityLookup | undefined,
- areaEntityLookup: AreaEntityLookup | undefined,
- areaDeviceLookup: AreaDeviceLookup | undefined
+ entities: HomeAssistant["entities"],
+ devices: HomeAssistant["devices"],
+ areas: HomeAssistant["areas"]
): string[] => {
- if (
- !targetPickerValue ||
- deviceEntityLookup === undefined ||
- areaEntityLookup === undefined ||
- areaDeviceLookup === undefined
- ) {
+ if (!targetPickerValue) {
return [];
}
- const entityIds = new Set();
- let {
- area_id: searchingAreaId,
- device_id: searchingDeviceId,
- entity_id: searchingEntityId,
- } = targetPickerValue;
+ const targetSelector = { target: {} };
+ const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
+ const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
+ const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
+ const targetFloors = new Set(ensureArray(targetPickerValue.floor_id));
+ const targetLabels = new Set(ensureArray(targetPickerValue.label_id));
- if (searchingAreaId) {
- searchingAreaId = ensureArray(searchingAreaId);
- for (const singleSearchingAreaId of searchingAreaId) {
- const foundEntities = areaEntityLookup[singleSearchingAreaId];
- if (foundEntities?.length) {
- for (const foundEntity of foundEntities) {
- if (foundEntity.entity_category === null) {
- entityIds.add(foundEntity.entity_id);
- }
- }
- }
+ targetLabels.forEach((labelId) => {
+ const expanded = expandLabelTarget(
+ this.hass,
+ labelId,
+ areas,
+ devices,
+ entities,
+ targetSelector
+ );
+ expanded.devices.forEach((id) => targetDevices.add(id));
+ expanded.entities.forEach((id) => targetEntities.add(id));
+ expanded.areas.forEach((id) => targetAreas.add(id));
+ });
- const foundDevices = areaDeviceLookup[singleSearchingAreaId];
- if (!foundDevices?.length) {
- continue;
- }
+ targetFloors.forEach((floorId) => {
+ const expanded = expandFloorTarget(
+ this.hass,
+ floorId,
+ areas,
+ targetSelector
+ );
+ expanded.areas.forEach((id) => targetAreas.add(id));
+ });
- for (const foundDevice of foundDevices) {
- const foundDeviceEntities = deviceEntityLookup[foundDevice.id];
- if (!foundDeviceEntities?.length) {
- continue;
- }
+ targetAreas.forEach((areaId) => {
+ const expanded = expandAreaTarget(
+ this.hass,
+ areaId,
+ devices,
+ entities,
+ targetSelector
+ );
+ expanded.devices.forEach((id) => targetDevices.add(id));
+ expanded.entities.forEach((id) => targetEntities.add(id));
+ });
- for (const foundDeviceEntity of foundDeviceEntities) {
- if (
- (!foundDeviceEntity.area_id ||
- foundDeviceEntity.area_id === singleSearchingAreaId) &&
- foundDeviceEntity.entity_category === null
- ) {
- entityIds.add(foundDeviceEntity.entity_id);
- }
- }
- }
- }
- }
+ targetDevices.forEach((deviceId) => {
+ const expanded = expandDeviceTarget(
+ this.hass,
+ deviceId,
+ entities,
+ targetSelector
+ );
+ expanded.entities.forEach((id) => targetEntities.add(id));
+ });
- if (searchingDeviceId) {
- searchingDeviceId = ensureArray(searchingDeviceId);
- for (const singleSearchingDeviceId of searchingDeviceId) {
- const foundEntities = deviceEntityLookup[singleSearchingDeviceId];
- if (!foundEntities?.length) {
- continue;
- }
-
- for (const foundEntity of foundEntities) {
- if (foundEntity.entity_category === null) {
- entityIds.add(foundEntity.entity_id);
- }
- }
- }
- }
-
- if (searchingEntityId) {
- searchingEntityId = ensureArray(searchingEntityId);
- for (const singleSearchingEntityId of searchingEntityId) {
- entityIds.add(singleSearchingEntityId);
- }
- }
-
- return [...entityIds];
+ return Array.from(targetEntities);
}
);
@@ -639,6 +606,12 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
","
);
}
+ if (this._targetPickerValue.label_id) {
+ params.label_id = ensureArray(this._targetPickerValue.label_id).join(",");
+ }
+ if (this._targetPickerValue.floor_id) {
+ params.floor_id = ensureArray(this._targetPickerValue.floor_id).join(",");
+ }
if (this._targetPickerValue.area_id) {
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
}
diff --git a/src/translations/en.json b/src/translations/en.json
index d4e2852c0a..bf3e8b9053 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -501,6 +501,7 @@
},
"subpage-data-table": {
"filters": "Filters",
+ "show_results": "show {number} results",
"clear_filter": "Clear filter",
"close_filter": "Close filters",
"exit_selection_mode": "Exit selection mode",
@@ -2270,7 +2271,8 @@
"category": "Category"
},
"create_helper": "Create helper",
- "no_helpers": "Looks like you don't have any helpers yet!"
+ "no_helpers": "Looks like you don't have any helpers yet!",
+ "search": "Search {number} helpers"
},
"dialog": {
"create": "Create",
@@ -2684,6 +2686,7 @@
"assign_category": "Assign category",
"no_category_support": "You can't assign an category to this automation",
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
+ "search": "Search {number} automations",
"headers": {
"toggle": "Enable/disable",
"name": "Name",
@@ -3241,7 +3244,9 @@
"target_template": "templated {name}",
"target_unknown_entity": "unknown entity",
"target_unknown_device": "unknown device",
- "target_unknown_area": "unknown area"
+ "target_unknown_area": "unknown area",
+ "target_unknown_floor": "unknown floor",
+ "target_unknown_label": "unknown label"
}
},
"play_media": {
@@ -3575,7 +3580,8 @@
"delete": "[%key:ui::common::delete%]",
"duplicate": "[%key:ui::common::duplicate%]",
"empty_header": "Create your first script",
- "empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay."
+ "empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.",
+ "search": "Search {number} scripts"
},
"dialog_new": {
"header": "Create script",
@@ -3683,7 +3689,8 @@
"no_category_support": "You can't assign an category to this scene",
"no_category_entity_reg": "To assign an category to an scene it needs to have a unique ID.",
"empty_header": "Create your first scene",
- "empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV."
+ "empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV.",
+ "search": "Search {number} scenes"
},
"editor": {
"default_name": "New scene",
@@ -4008,7 +4015,7 @@
"confirm_delete": "Are you sure you want to delete this device?",
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": {
- "search": "Search devices",
+ "search": "Search {number} devices",
"state": "State"
}
},
@@ -4019,7 +4026,7 @@
"header": "Entities",
"introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.",
"introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.",
- "search": "Search entities",
+ "search": "Search {number} entities",
"unnamed_entity": "Unnamed entity",
"status": {
"restored": "Restored",