Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Lamon e923d9251f Swap google-timezones-json 2026-06-20 06:12:38 +00:00
201 changed files with 1879 additions and 10291 deletions
@@ -1,18 +0,0 @@
diff --git a/lib/cook-raw-quasi.js b/lib/cook-raw-quasi.js
index 3ea8fa7be8e357c1066d7417caeeecd841415208..6bf04ab0bed8897b5ff2898ca835867aec5cee6a 100644
--- a/lib/cook-raw-quasi.js
+++ b/lib/cook-raw-quasi.js
@@ -1,10 +1,11 @@
'use strict';
-function cookRawQuasi({transform}, raw) {
+function cookRawQuasi({transformSync}, raw) {
// This nasty hack is needed until https://github.com/babel/babel/issues/9242 is resolved.
const args = {raw};
- transform('cooked`' + args.raw + '`', {
+ // Babel 8 removed synchronous `transform`; use `transformSync` instead.
+ transformSync('cooked`' + args.raw + '`', {
babelrc: false,
configFile: false,
plugins: [
+1
View File
@@ -104,6 +104,7 @@ module.exports.babelOptions = ({
{
useBuiltIns: "usage",
corejs: dependencies["core-js"],
bugfixes: true,
shippedProposals: true,
},
],
@@ -1,75 +0,0 @@
import type { DemoTrace } from "./types";
export const notTriggeredTrace: DemoTrace = {
trace: {
last_step: "trigger/0",
run_id: "788767ce152d3d4475134bf1107986d4",
state: "stopped",
script_execution: "not_triggered",
not_triggered: true,
timestamp: {
start: "2021-03-25T04:36:51.223337+00:00",
finish: "2021-03-25T04:36:51.223341+00:00",
},
// Not-triggered traces have no trigger description.
trigger: null,
domain: "automation",
item_id: "1781703842452",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223340+00:00",
changed_variables: {
trigger: {
id: "0",
idx: "0",
alias: null,
platform: "light.turned_on",
},
},
result: {
reason: "new_state_not_a_match",
data: {
entity_id: "light.bed_light",
to_state: "off",
},
},
},
],
},
config: {
id: "1781703842452",
alias: "Light Turned On Notification",
description: "Send a notification when a specific light is turned on.",
triggers: [
{
trigger: "light.turned_on",
target: {
floor_id: "test",
},
options: {
for: "00:00:00",
behavior: "each",
},
},
],
conditions: [],
actions: [
{
action: "notify.notify",
data: {
message: "A light was turned on.",
},
},
],
mode: "single",
},
context: {
id: "01KVAX7CG7XBDYGJYAGA4XJHGX",
parent_id: "01KVAX7CG631JRX4H3JS5JJ11Q",
user_id: null,
},
},
logbookEntries: [],
};
@@ -24,33 +24,6 @@ const traces: DemoTrace[] = [
error: 'Variable "beer" cannot be None',
}),
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
mockDemoTrace({
state: "stopped",
script_execution: "not_triggered",
not_triggered: true,
// Not-triggered traces have no trigger description.
trigger: null,
trace: {
"trigger/0": [
{
path: "trigger/0",
changed_variables: {
trigger: {
id: "0",
idx: "0",
alias: null,
platform: "light.turned_on",
},
},
result: {
reason: "new_state_not_a_match",
data: { entity_id: "light.bed_light", to_state: "off" },
},
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
},
}),
];
@customElement("demo-automation-trace-timeline")
+8 -28
View File
@@ -2,20 +2,17 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/trace/ha-trace-path-details";
import type { HatScriptGraph } from "../../../../src/components/trace/hat-script-graph";
import "../../../../src/components/trace/hat-script-graph";
import "../../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import { basicTrace } from "../../data/traces/basic_trace";
import { motionLightTrace } from "../../data/traces/motion-light-trace";
import { notTriggeredTrace } from "../../data/traces/not-triggered-trace";
import type { DemoTrace } from "../../data/traces/types";
const traces: DemoTrace[] = [basicTrace, motionLightTrace, notTriggeredTrace];
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
@customElement("demo-automation-trace")
export class DemoAutomationTrace extends LitElement {
@@ -23,25 +20,18 @@ export class DemoAutomationTrace extends LitElement {
@state() private _selected = {};
@queryAll("hat-script-graph") private _graphs!: NodeListOf<HatScriptGraph>;
protected render() {
if (!this.hass) {
return nothing;
}
return html`
${traces.map((trace, idx) => {
const graph = this._graphs?.[idx];
const selectedPath = this._selected[idx];
const selectedNode = selectedPath
? graph?.renderedNodes[selectedPath]
: undefined;
return html`
${traces.map(
(trace, idx) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-script-graph
.trace=${trace.trace}
.selected=${selectedPath}
.selected=${this._selected[idx]}
@graph-node-selected=${this._handleGraphNodeSelected}
.sampleIdx=${idx}
></hat-script-graph>
@@ -50,25 +40,15 @@ export class DemoAutomationTrace extends LitElement {
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${selectedPath}
.selectedPath=${this._selected[idx]}
@value-changed=${this._handleTimelineValueChanged}
.sampleIdx=${idx}
></hat-trace-timeline>
${selectedNode && graph
? html`<ha-trace-path-details
.hass=${this.hass}
.trace=${trace.trace}
.selected=${selectedNode}
.logbookEntries=${trace.logbookEntries}
.trackedNodes=${graph.trackedNodes}
.renderedNodes=${graph.renderedNodes}
></ha-trace-path-details>`
: nothing}
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`;
})}
`
)}
`;
}
+1
View File
@@ -353,6 +353,7 @@ export class DemoEntityState extends LitElement {
title: "Icon",
template: (entry) => html`
<state-badge
.hass=${hass}
.stateObj=${entry.stateObj}
.stateColor=${true}
></state-badge>
+18 -18
View File
@@ -28,7 +28,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "8.0.0",
"@babel/runtime": "7.29.7",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.3",
"@codemirror/commands": "6.10.3",
@@ -50,12 +50,12 @@
"@formatjs/intl-numberformat": "9.3.11",
"@formatjs/intl-pluralrules": "6.3.10",
"@formatjs/intl-relativetimeformat": "12.3.10",
"@fullcalendar/core": "6.1.21",
"@fullcalendar/daygrid": "6.1.21",
"@fullcalendar/interaction": "6.1.21",
"@fullcalendar/list": "6.1.21",
"@fullcalendar/luxon3": "6.1.21",
"@fullcalendar/timegrid": "6.1.21",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.7.0-ha.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
@@ -72,9 +72,10 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.23",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.2.1",
"@tsparticles/preset-links": "4.2.1",
"@tsparticles/engine": "4.1.3",
"@tsparticles/preset-links": "4.1.3",
"@vibrant/color": "4.0.4",
"@vvo/tzdb": "6.198.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"barcode-detector": "3.2.0",
@@ -91,7 +92,6 @@
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.4.2",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
@@ -127,10 +127,10 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "8.0.0",
"@babel/helper-define-polyfill-provider": "1.0.0",
"@babel/plugin-transform-runtime": "8.0.0",
"@babel/preset-env": "8.0.0",
"@babel/core": "7.29.7",
"@babel/helper-define-polyfill-provider": "0.6.8",
"@babel/plugin-transform-runtime": "7.29.7",
"@babel/preset-env": "7.29.7",
"@bundle-stats/plugin-webpack-filter": "4.22.2",
"@eslint/js": "10.0.1",
"@html-eslint/eslint-plugin": "0.62.0",
@@ -138,7 +138,7 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.15",
"@rsdoctor/rspack-plugin": "1.5.13",
"@rspack/core": "2.0.8",
"@rspack/dev-server": "2.0.3",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -151,13 +151,13 @@
"@types/leaflet-draw": "1.0.13",
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.2",
"@types/luxon": "3.7.1",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@vitest/coverage-v8": "4.1.9",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "patch:babel-plugin-template-html-minifier@npm%3A4.1.0#~/.yarn/patches/babel-plugin-template-html-minifier-npm-4.1.0-9a3c00055a.patch",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
"del": "8.0.1",
"eslint": "10.5.0",
@@ -208,7 +208,7 @@
"lit-html": "3.3.3",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.21",
"@fullcalendar/daygrid": "6.1.20",
"globals": "17.6.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
+2 -2
View File
@@ -1,4 +1,4 @@
import timezones from "google-timezones-json";
import { timeZonesNames } from "@vvo/tzdb";
import { TimeZone } from "../../data/translation";
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
@@ -10,7 +10,7 @@ const RESOLVED_TIME_ZONE =
RESOLVED_RAW &&
(RESOLVED_RAW === "UTC" ||
RESOLVED_RAW === "Etc/UTC" ||
RESOLVED_RAW in timezones)
timeZonesNames.includes(RESOLVED_RAW))
? RESOLVED_RAW
: undefined;
@@ -1,7 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { unitFromParts } from "./value_parts";
interface EntityUnitStubConfig {
entity: string;
@@ -41,5 +40,5 @@ export const computeEntityUnitDisplay = (
? hass.formatEntityAttributeValueToParts(stateObj, config.attribute)
: hass.formatEntityStateToParts(stateObj);
return unitFromParts(parts);
return parts.find((part) => part.type === "unit")?.value ?? "";
};
+1 -2
View File
@@ -160,8 +160,7 @@ const computeStateToPartsFromEntityAttributes = (
const type = MONETARY_TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive value parts so the number stays a single part
// (e.g. "-" + "12" + "." + "00" → "-12.00")
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
-29
View File
@@ -1,29 +0,0 @@
import type { ValuePart } from "../../types";
// Joins every part except the unit, keeping native order so the sign and
// grouping stay with the value (e.g. "-2,548.14").
export const valueFromParts = (parts: ValuePart[]): string =>
parts
.filter((part) => part.type !== "unit")
.map((part) => part.value)
.join("")
.trim();
export const unitFromParts = (parts: ValuePart[]): string =>
parts.find((part) => part.type === "unit")?.value ?? "";
export type UnitPosition = "before" | "after";
// Whether the unit sits before or after the value in the locale's native order
// (e.g. "$5" / "€ 5" → "before", "5 €" / "5 %" → "after").
export const unitPosition = (parts: ValuePart[]): UnitPosition => {
const unitIndex = parts.findIndex((part) => part.type === "unit");
if (unitIndex === -1) {
return "after";
}
const lastValueIndex = parts.reduceRight(
(acc, part, i) => (acc === -1 && part.type === "value" ? i : acc),
-1
);
return unitIndex < lastValueIndex ? "before" : "after";
};
+7 -23
View File
@@ -1,5 +1,5 @@
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { ItemType, RelatedResult } from "../../data/search";
import type { RelatedResult } from "../../data/search";
export interface RelatedIdSets {
areas: Set<string>;
@@ -8,30 +8,14 @@ export interface RelatedIdSets {
}
/**
* Build a set of related IDs, merging in the current (queried) item.
* `search/related` does not echo the queried item back, but it is the closest
* related item (e.g. a card editor's own entity), so it is merged into the
* matching group when it is an area, device, or entity.
* Build a set of related IDs for a given related result.
* @param related - The related result to build the sets from.
* @param current - The queried item to merge in.
* @returns The related ID sets, including the current item.
* @returns The related ID sets.
*/
export const buildRelatedIdSets = (
related?: RelatedResult,
current?: { itemType: ItemType; itemId: string }
): RelatedIdSets => ({
areas: new Set([
...(related?.area || []),
...(current?.itemType === "area" ? [current.itemId] : []),
]),
devices: new Set([
...(related?.device || []),
...(current?.itemType === "device" ? [current.itemId] : []),
]),
entities: new Set([
...(related?.entity || []),
...(current?.itemType === "entity" ? [current.itemId] : []),
]),
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
areas: new Set(related?.area || []),
devices: new Set(related?.device || []),
entities: new Set(related?.entity || []),
});
/**
@@ -1,18 +1,16 @@
import { consume, type ContextType } from "@lit/context";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import "./ha-progress-button";
import { apiContext } from "../../data/context";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import type { Appearance } from "../ha-button";
@customElement("ha-call-service-button")
class HaCallServiceButton extends LitElement {
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@@ -58,7 +56,7 @@ class HaCallServiceButton extends LitElement {
this.shadowRoot!.querySelector("ha-progress-button")!;
try {
await this._api.callService(
await this.hass.callService(
this.domain,
this.service,
this.data,
@@ -445,7 +445,6 @@ export class StateHistoryChartLine extends LitElement {
private _formatYAxisLabel = (value: number) => {
const label = formatNumber(value, this.hass.locale, {
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
maximumFractionDigits: this._yAxisFractionDigits,
});
const width = measureTextWidth(label, 12) + 5;
-1
View File
@@ -552,7 +552,6 @@ export class StatisticsChart extends LitElement {
private _formatYAxisLabel = (value: number) =>
formatNumber(value, this.hass.locale, {
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
maximumFractionDigits: this._yAxisFractionDigits,
});
+19 -59
View File
@@ -1,5 +1,4 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { consume } from "@lit/context";
import { mdiPlus, mdiShape } from "@mdi/js";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -7,14 +6,10 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { RelatedIdSets } from "../../common/search/related-context";
import { relatedContext } from "../../data/context";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import {
entityComboBoxKeys,
getEntities,
markEntitiesRelated,
sortEntitiesByRelatedRank,
type EntityComboBoxItem,
} from "../../data/entity/entity_picker";
import { domainToName } from "../../data/integration";
@@ -136,20 +131,6 @@ export class HaEntityPicker extends LitElement {
@state() private _pendingEntityId?: string;
@state()
@consume({ context: relatedContext, subscribe: true })
private _relatedIdSets?: RelatedIdSets;
private get _hasRelatedContext(): boolean {
const related = this._relatedIdSets;
return (
!!related &&
(related.entities.size > 0 ||
related.devices.size > 0 ||
related.areas.size > 0)
);
}
protected willUpdate(changedProperties: PropertyValues<this>) {
if (
this._pendingEntityId &&
@@ -180,7 +161,11 @@ export class HaEntityPicker extends LitElement {
: undefined;
if (stateObj) {
return html`
<state-badge slot="start" .stateObj=${stateObj}></state-badge>
<state-badge
slot="start"
.stateObj=${stateObj}
.hass=${this.hass}
></state-badge>
`;
}
if (extraOption.icon_path) {
@@ -231,7 +216,11 @@ export class HaEntityPicker extends LitElement {
);
return html`
<state-badge .stateObj=${stateObj} slot="start"></state-badge>
<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
slot="start"
></state-badge>
<span slot="headline">${primary}</span>
<span slot="supporting-text">${secondary}</span>
`;
@@ -261,6 +250,7 @@ export class HaEntityPicker extends LitElement {
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
@@ -343,22 +333,8 @@ export class HaEntityPicker extends LitElement {
})
);
private _sortByRelatedContext = memoizeOne(
(
items: EntityComboBoxItem[],
related: RelatedIdSets,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
language: string
): EntityComboBoxItem[] =>
sortEntitiesByRelatedRank(
markEntitiesRelated(items, related, entities, devices),
language
)
);
private _getItems = () => {
const entityItems = this._getEntitiesMemoized(
const items = this._getEntitiesMemoized(
this.hass,
this.includeDomains,
this.excludeDomains,
@@ -369,23 +345,14 @@ export class HaEntityPicker extends LitElement {
this.excludeEntities,
this.value
);
const sortedItems = this._hasRelatedContext
? this._sortByRelatedContext(
entityItems,
this._relatedIdSets!,
this.hass.entities,
this.hass.devices,
this.hass.locale.language
)
: entityItems;
if (this.extraOptions?.length) {
const resolvedExtras = this.extraOptions.map((opt) => ({
...opt,
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
}));
return [...resolvedExtras, ...sortedItems];
return [...resolvedExtras, ...items];
}
return sortedItems;
return items;
};
private _shouldHideClearIcon() {
@@ -417,7 +384,6 @@ export class HaEntityPicker extends LitElement {
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys}
.noSort=${this._hasRelatedContext}
use-top-label
.addButtonLabel=${this.addButton
? (this.addButtonLabel ??
@@ -436,23 +402,17 @@ export class HaEntityPicker extends LitElement {
search,
filteredItems
) => {
// Float related items to the top by closeness, keeping search relevance
// order within each tier.
const items = this._hasRelatedContext
? sortEntitiesByRelatedRank(filteredItems)
: filteredItems;
// If there is exact match for entity id, put it first
const index = items.findIndex(
const index = filteredItems.findIndex(
(item) => item.stateObj?.entity_id === search
);
if (index === -1) {
return items;
return filteredItems;
}
const [exactMatch] = items.splice(index, 1);
items.unshift(exactMatch);
return items;
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
};
public async open() {
+5 -6
View File
@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import { mdiFlash, mdiFlashOff } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
@@ -7,9 +6,9 @@ import { customElement, property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { apiContext } from "../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { forwardHaptic } from "../../data/haptics";
import type { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-icon-button";
import "../ha-switch";
@@ -30,8 +29,8 @@ const isOn = (stateObj?: HassEntity) =>
@customElement("ha-entity-toggle")
export class HaEntityToggle extends LitElement {
@consume({ context: apiContext, subscribe: true })
private _api?: ContextType<typeof apiContext>;
// hass is not a property so that we only re-render on stateObj changes
public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@@ -119,7 +118,7 @@ export class HaEntityToggle extends LitElement {
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
private async _callService(turnOn): Promise<void> {
if (!this._api || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic(this, "light");
@@ -150,7 +149,7 @@ export class HaEntityToggle extends LitElement {
this._isOn = turnOn;
try {
await this._api.callService(serviceDomain, service, {
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
} finally {
+20 -39
View File
@@ -1,5 +1,3 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
@@ -8,19 +6,13 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import {
consumeEntityRegistryEntry,
consumeLocalize,
} from "../../common/decorators/consume-context-entry";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { unitFromParts, valueFromParts } from "../../common/entity/value_parts";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import type { LocalizeFunc } from "../../common/translations/localize";
import { formattersContext } from "../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
import "../ha-label-badge";
import "../ha-state-icon";
@@ -48,15 +40,7 @@ const getTruncatedKey = (domainKey: string, stateKey: string) => {
@customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@state()
@consumeEntityRegistryEntry({ entityIdPath: ["state", "entity_id"] })
private _entry?: EntityRegistryDisplayEntry;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public state?: HassEntity;
@@ -93,8 +77,10 @@ export class HaStateLabelBadge extends LitElement {
return html`
<ha-label-badge
class="warning"
label=${this._localize("state_badge.default.error")}
description=${this._localize("state_badge.default.entity_not_found")}
label=${this.hass!.localize("state_badge.default.error")}
description=${this.hass!.localize(
"state_badge.default.entity_not_found"
)}
>
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</ha-label-badge>
@@ -108,7 +94,7 @@ export class HaStateLabelBadge extends LitElement {
// 4. Icon determined via entity state
// 5. Value string as fallback
const domain = computeStateDomain(entityState);
const entry = this._entry;
const entry = this.hass?.entities[entityState.entity_id];
const showIcon =
this.icon || this._computeShowIcon(domain, entityState, entry);
@@ -177,23 +163,20 @@ export class HaStateLabelBadge extends LitElement {
case "sun":
case "timer":
return null;
// @ts-expect-error we don't break and go to default
case "sensor":
if (entry?.platform === "moon") {
return null;
}
break;
// eslint-disable-next-line: disable=no-fallthrough
default:
break;
return entityState.state === UNAVAILABLE ||
entityState.state === UNKNOWN
? "—"
: this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "value"
)?.value;
}
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
return "—";
}
if (!this._formatters) {
return null;
}
return valueFromParts(
this._formatters.formatEntityStateToParts(entityState)
);
}
private _computeShowIcon(
@@ -228,11 +211,11 @@ export class HaStateLabelBadge extends LitElement {
) {
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
return this._localize(`state_badge.default.${entityState.state}`);
return this.hass!.localize(`state_badge.default.${entityState.state}`);
}
const domainStateKey = getTruncatedKey(domain, entityState.state);
if (domainStateKey) {
return this._localize(`state_badge.${domainStateKey}`);
return this.hass!.localize(`state_badge.${domainStateKey}`);
}
// Person and device tracker state can be zone name
if (domain === "person" || domain === "device_tracker") {
@@ -241,12 +224,10 @@ export class HaStateLabelBadge extends LitElement {
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
if (!this._formatters) {
return null;
}
return (
unitFromParts(this._formatters.formatEntityStateToParts(entityState)) ||
null
this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "unit"
)?.value || null
);
}
+6 -1
View File
@@ -343,7 +343,11 @@ export class HaStatisticPicker extends LitElement {
return html`
${item.stateObj
? html`
<state-badge .stateObj=${item.stateObj} slot="start"></state-badge>
<state-badge
.hass=${this.hass}
.stateObj=${item.stateObj}
slot="start"
></state-badge>
`
: item.icon_path
? html`
@@ -484,6 +488,7 @@ export class HaStatisticPicker extends LitElement {
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`
: nothing}
+15 -28
View File
@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -15,12 +14,13 @@ import {
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { connectionContext } from "../../data/context";
import { isBrandUrl } from "../../util/brands-url";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@customElement("state-badge")
export class StateBadge extends LitElement {
public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ attribute: false }) public overrideIcon?: string;
@@ -36,10 +36,6 @@ export class StateBadge extends LitElement {
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
@property({ type: Boolean, reflect: true }) public icon = true;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state() private _iconStyle: Record<string, string | undefined> = {};
connectedCallback(): void {
@@ -110,15 +106,14 @@ export class StateBadge extends LitElement {
></ha-state-icon>`;
}
public willUpdate(changedProps: PropertyValues) {
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (
!changedProps.has("stateObj") &&
!changedProps.has("overrideImage") &&
!changedProps.has("overrideIcon") &&
!changedProps.has("stateColor") &&
!changedProps.has("color") &&
!changedProps.has("_connection")
!changedProps.has("color")
) {
return;
}
@@ -138,10 +133,12 @@ export class StateBadge extends LitElement {
stateObj.attributes.entity_picture) &&
!this.overrideIcon
) {
let imageUrl = this._resolveImageUrl(
let imageUrl =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture
);
stateObj.attributes.entity_picture;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}
@@ -182,7 +179,11 @@ export class StateBadge extends LitElement {
}
}
} else if (this.overrideImage) {
backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`;
let imageUrl = this.overrideImage;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
backgroundImage = `url(${imageUrl})`;
this.icon = false;
}
}
@@ -191,20 +192,6 @@ export class StateBadge extends LitElement {
this.style.backgroundImage = backgroundImage;
}
// Sign the image URL via the connection context so brand images
// (/api/brands/...) get their access token. Without a way to sign, a brands
// request would be rejected (and logged/blocked by core), so skip it until
// we can sign.
private _resolveImageUrl(url: string | undefined): string {
if (!url) {
return "";
}
if (this._connection) {
return this._connection.hassUrl(url);
}
return isBrandUrl(url) ? "" : url;
}
protected getClass() {
const cls = new Map(
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
+1
View File
@@ -24,6 +24,7 @@ class StateInfo extends LitElement {
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
return html`<state-badge
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateColor=${true}
.color=${this.color}
@@ -173,6 +173,7 @@ export class HaAreaControlsPicker extends LitElement {
domainItems = multiTermSortedSearch(
domainItems,
searchString,
this._domainSearchKeys,
(item) => item.id,
fuseIndex
);
@@ -225,6 +226,7 @@ export class HaAreaControlsPicker extends LitElement {
entityItems = multiTermSortedSearch(
entityItems,
searchString,
this._entitySearchKeys,
(item) => item.id,
fuseIndex
);
+5 -12
View File
@@ -1,11 +1,10 @@
import { consume, type ContextType } from "@lit/context";
import { mdiTextureBox } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areasContext, floorsContext } from "../data/context";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-items-display-editor";
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
@@ -18,13 +17,7 @@ export interface AreasDisplayValue {
@customElement("ha-areas-display-editor")
export class HaAreasDisplayEditor extends LitElement {
@consume({ context: areasContext, subscribe: true })
@state()
private _areas!: ContextType<typeof areasContext>;
@consume({ context: floorsContext, subscribe: true })
@state()
private _floors!: ContextType<typeof floorsContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -42,10 +35,10 @@ export class HaAreasDisplayEditor extends LitElement {
public showNavigationButton = false;
protected render(): TemplateResult {
const areas = Object.values(this._areas);
const areas = Object.values(this.hass.areas);
const items: DisplayItem[] = areas.map((area) => {
const { floor } = getAreaContext(area, this._floors);
const { floor } = getAreaContext(area, this.hass.floors);
return {
value: area.area_id,
label: area.name,
@@ -1,19 +1,15 @@
import { consume, type ContextType } from "@lit/context";
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import type { LocalizeFunc } from "../common/translations/localize";
import { areasContext, floorsContext } from "../data/context";
import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-expansion-panel";
import "./ha-floor-icon";
import "./ha-items-display-editor";
@@ -34,17 +30,7 @@ const UNASSIGNED_FLOOR = "__unassigned__";
@customElement("ha-areas-floors-display-editor")
export class HaAreasFloorsDisplayEditor extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: areasContext, subscribe: true })
@state()
private _areas!: ContextType<typeof areasContext>;
@consume({ context: floorsContext, subscribe: true })
@state()
private _floors!: ContextType<typeof floorsContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -65,14 +51,13 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
protected render(): TemplateResult {
const groupedAreasItems = this._groupedAreasItems(
this._areas,
this._floors
this.hass.areas,
this.hass.floors
);
const filteredFloors = this._sortedFloors(
this._floors,
this.value?.floors_display?.order,
this._localize
this.hass.floors,
this.value?.floors_display?.order
).filter(
(floor) =>
// Only include floors that have areas assigned to them
@@ -139,14 +124,15 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
private _groupedAreasItems = memoizeOne(
(
areas: ContextType<typeof areasContext>,
floors: ContextType<typeof floorsContext>
hassAreas: HomeAssistant["areas"],
// update items if floors change
_hassFloors: HomeAssistant["floors"]
): Record<string, DisplayItem[]> => {
const areaList = Object.values(areas);
const areas = Object.values(hassAreas);
const groupedItems: Record<string, DisplayItem[]> = areaList.reduce(
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => {
const { floor } = getAreaContext(area, floors);
const { floor } = getAreaContext(area, this.hass.floors);
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
if (!acc[floorId]) {
@@ -169,24 +155,23 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
private _sortedFloors = memoizeOne(
(
floors: ContextType<typeof floorsContext>,
order: string[] | undefined,
localize: LocalizeFunc
hassFloors: HomeAssistant["floors"],
order: string[] | undefined
): FloorRegistryEntry[] => {
const sortedFloors = getFloors(floors, order);
const noFloors = sortedFloors.length === 0;
sortedFloors.push({
const floors = getFloors(hassFloors, order);
const noFloors = floors.length === 0;
floors.push({
floor_id: UNASSIGNED_FLOOR,
name: noFloors
? localize("ui.panel.lovelace.strategy.areas.areas")
: localize("ui.panel.lovelace.strategy.areas.other_areas"),
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
icon: null,
level: null,
aliases: [],
created_at: 0,
modified_at: 0,
});
return sortedFloors;
return floors;
}
);
@@ -195,9 +180,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
const newIndex = ev.detail.newIndex;
const oldIndex = ev.detail.oldIndex;
const floorIds = this._sortedFloors(
this._floors,
this.value?.floors_display?.order,
this._localize
this.hass.floors,
this.value?.floors_display?.order
).map((floor) => floor.floor_id);
const newOrder = [...floorIds];
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
@@ -220,9 +204,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
const currentFloorId = (ev.currentTarget as any).floorId;
const floorIds = this._sortedFloors(
this._floors,
this.value?.floors_display?.order,
this._localize
this.hass.floors,
this.value?.floors_display?.order
).map((floor) => floor.floor_id);
const oldAreaDisplay = this.value?.areas_display ?? {};
@@ -240,14 +223,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
continue;
}
const hidden = oldHidden.filter((areaId) => {
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
return id === floorId;
});
if (hidden?.length) {
newHidden.push(...hidden);
}
const order = oldOrder.filter((areaId) => {
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
return id === floorId;
});
if (order?.length) {
+4 -2
View File
@@ -7,7 +7,6 @@ import { customElement, property, state } from "lit/decorators";
import { AsyncValueTask } from "../common/controllers/async-value-task";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { getValueAttribute } from "../common/entity/get_states";
import { valueFromParts } from "../common/entity/value_parts";
import { formattersContext } from "../data/context";
const isObjectValue = (value: unknown): boolean =>
@@ -95,7 +94,10 @@ class HaAttributeValue extends LitElement {
this.stateObj!,
this.attribute
);
return valueFromParts(parts);
return parts
.filter((part) => part.type === "value")
.map((part) => part.value)
.join("");
}
return this._formatters!.formatEntityAttributeValue(
+17 -57
View File
@@ -1,18 +1,10 @@
import { consume, type ContextType } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeEntityStates } from "../common/decorators/consume-context-entry";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name";
import {
configContext,
connectionContext,
entitiesContext,
} from "../data/context";
import { entityIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-items-display-editor";
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
@@ -23,21 +15,7 @@ export interface EntitiesDisplayValue {
@customElement("ha-entities-display-editor")
export class HaEntitiesDisplayEditor extends LitElement {
@state()
@consumeEntityStates({ entityIdPath: ["entitiesIds"] })
private _entityStates?: Record<string, HassEntity>;
@consume({ context: entitiesContext, subscribe: true })
@state()
private _entitiesReg!: ContextType<typeof entitiesContext>;
@consume({ context: configContext, subscribe: true })
@state()
private _config!: ContextType<typeof configContext>;
@consume({ context: connectionContext, subscribe: true })
@state()
private _connection!: ContextType<typeof connectionContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -54,13 +32,20 @@ export class HaEntitiesDisplayEditor extends LitElement {
@property({ type: Boolean }) public required = false;
protected render(): TemplateResult {
const items = this._items(
this.entitiesIds,
this._entityStates,
this._entitiesReg,
this._config,
this._connection
);
const entities = this.entitiesIds
.map((entityId) => this.hass.states[entityId])
.filter(Boolean);
const items: DisplayItem[] = entities.map((entity) => ({
value: entity.entity_id,
label: computeStateName(entity),
icon: entityIcon(
this.hass.entities,
this.hass.config,
this.hass.connection,
entity
),
}));
const value: DisplayValue = {
order: this.value?.order ?? [],
@@ -76,31 +61,6 @@ export class HaEntitiesDisplayEditor extends LitElement {
`;
}
private _items = memoizeOne(
(
entitiesIds: string[],
entityStates: Record<string, HassEntity> | undefined,
entitiesReg: ContextType<typeof entitiesContext>,
config: ContextType<typeof configContext>,
connection: ContextType<typeof connectionContext>
): DisplayItem[] => {
const entities = entitiesIds
.map((entityId) => entityStates?.[entityId])
.filter((stateObj): stateObj is HassEntity => Boolean(stateObj));
return entities.map((entity) => ({
value: entity.entity_id,
label: computeStateName(entity),
icon: entityIcon(
entitiesReg,
config.config,
connection.connection,
entity
),
}));
}
);
private _itemDisplayChanged(ev) {
ev.stopPropagation();
const value = ev.detail.value as DisplayValue;
+6 -18
View File
@@ -1,18 +1,13 @@
import { consume } from "@lit/context";
import { mdiDelete, mdiFileUpload } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ensureArray } from "../common/array/ensure-array";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { transform } from "../common/decorators/transform";
import { fireEvent } from "../common/dom/fire_event";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import type { LocalizeFunc } from "../common/translations/localize";
import { internationalizationContext } from "../data/context";
import type { FrontendLocaleData } from "../data/translation";
import type { HomeAssistantInternationalization } from "../types";
import type { HomeAssistant } from "../types";
import { bytesToString } from "../util/bytes-to-string";
import "./ha-button";
import "./ha-icon-button";
@@ -27,17 +22,10 @@ declare global {
@customElement("ha-file-upload")
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() @consumeLocalize() private _localize?: LocalizeFunc;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@property() public accept!: string;
@property() public icon?: string;
@@ -92,7 +80,7 @@ export class HaFileUpload extends LitElement {
}
public render(): TemplateResult {
const localize = this.localize || this._localize!;
const localize = this.localize || this.hass!.localize;
return html`
${this.uploading
? html`<div class="container">
@@ -107,8 +95,8 @@ export class HaFileUpload extends LitElement {
>
${this.progress
? html`<div class="progress">
${this.progress}${this._locale &&
blankBeforePercent(this._locale)}%
${this.progress}${this.hass &&
blankBeforePercent(this.hass!.locale)}%
</div>`
: nothing}
</div>
+18 -57
View File
@@ -1,23 +1,15 @@
import { consume, type ContextType } from "@lit/context";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { stringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { deepEqual } from "../common/util/deep-equal";
import {
apiContext,
devicesContext,
internationalizationContext,
statesContext,
} from "../data/context";
import type { RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./input/ha-input-search";
import type { HaInputSearch } from "./input/ha-input-search";
@@ -32,24 +24,7 @@ interface HaFilterDevicesItem extends HaListVirtualizedItem {
@customElement("ha-filter-devices")
export class HaFilterDevices extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: statesContext, subscribe: true })
@state()
private _states!: ContextType<typeof statesContext>;
@consume({ context: devicesContext, subscribe: true })
@state()
private _devicesReg!: ContextType<typeof devicesContext>;
@consume({ context: internationalizationContext, subscribe: true })
@state()
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@@ -100,7 +75,7 @@ export class HaFilterDevices extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.devices.caption")}
${this.hass.localize("ui.panel.config.devices.caption")}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
@@ -120,13 +95,7 @@ export class HaFilterDevices extends LitElement {
</ha-input-search>
<ha-list-selectable-virtualized
multi
.rows=${this._devices(
this._devicesReg,
this._filter || "",
this._localize,
this._states,
this._i18n.locale.language
)}
.rows=${this._devices(this.hass.devices, this._filter || "")}
.rowRenderer=${this._renderItem}
@ha-list-item-selected=${this._handleAdded}
@ha-list-item-deselected=${this._handleRemoved}
@@ -152,24 +121,13 @@ export class HaFilterDevices extends LitElement {
private _handleAdded(ev: CustomEvent<number>) {
this.value = [
...(this.value ?? []),
this._devices(
this._devicesReg,
this._filter || "",
this._localize,
this._states,
this._i18n.locale.language
)[ev.detail].id,
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
];
}
private _handleRemoved(ev: CustomEvent<number>) {
const id = this._devices(
this._devicesReg,
this._filter || "",
this._localize,
this._states,
this._i18n.locale.language
)[ev.detail].id;
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
.id;
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
}
@@ -195,24 +153,27 @@ export class HaFilterDevices extends LitElement {
private _devices = memoizeOne(
(
devices: ContextType<typeof devicesContext>,
filter: string,
localize: LocalizeFunc,
states: ContextType<typeof statesContext>,
language: string | undefined
devices: HomeAssistant["devices"],
filter: string
): HaFilterDevicesItem[] => {
const values = Object.values(devices);
return values
.map((device) => ({
id: device.id,
interactive: true,
name: computeDeviceNameDisplay(device, localize, states),
name: computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
),
}))
.filter(
({ name }) =>
!filter || name.toLowerCase().includes(filter.toLowerCase())
)
.sort((a, b) => stringCompare(a.name, b.name, language));
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
@@ -233,7 +194,7 @@ export class HaFilterDevices extends LitElement {
for (const deviceId of this.value) {
value.push(deviceId);
if (this.type) {
relatedPromises.push(findRelated(this._api, "device", deviceId));
relatedPromises.push(findRelated(this.hass, "device", deviceId));
}
}
const results = await Promise.all(relatedPromises);
+25 -58
View File
@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -6,14 +5,12 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { stringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { internationalizationContext, statesContext } from "../data/context";
import { domainToName } from "../data/integration";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-domain-icon";
import "./ha-expansion-panel";
@@ -23,17 +20,7 @@ import type { HaInputSearch } from "./input/ha-input-search";
@customElement("ha-filter-domains")
export class HaFilterDomains extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: statesContext, subscribe: true })
@state()
private _states!: ContextType<typeof statesContext>;
@consume({ context: internationalizationContext, subscribe: true })
@state()
private _i18n!: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@@ -56,7 +43,7 @@ export class HaFilterDomains extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.domains.caption")}
${this.hass.localize("ui.panel.config.domains.caption")}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
@@ -78,13 +65,7 @@ export class HaFilterDomains extends LitElement {
multi
>
${repeat(
this._domains(
this._states,
this._localize,
this._i18n.locale.language,
this._filter,
this.value
),
this._domains(this.hass.states, this._filter, this.value),
(i) => i,
(domain) =>
html`<ha-check-list-item
@@ -97,7 +78,7 @@ export class HaFilterDomains extends LitElement {
.domain=${domain}
brand-fallback
></ha-domain-icon>
${domainToName(this._localize, domain)}
${domainToName(this.hass.localize, domain)}
</ha-check-list-item>`
)}
</ha-list> `
@@ -106,34 +87,26 @@ export class HaFilterDomains extends LitElement {
`;
}
private _domains = memoizeOne(
(
states: ContextType<typeof statesContext>,
localize: LocalizeFunc,
language: string | undefined,
filter: string | undefined,
_value
) => {
const domains = new Set<string>();
Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId));
});
private _domains = memoizeOne((states, filter, _value) => {
const domains = new Set<string>();
Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId));
});
return Array.from(domains.values())
.map((domain) => ({
domain,
name: domainToName(localize, domain),
}))
.filter(
(entry) =>
!filter ||
entry.domain.toLowerCase().includes(filter) ||
entry.name.toLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a.name, b.name, language))
.map((entry) => entry.domain);
}
);
return Array.from(domains.values())
.map((domain) => ({
domain,
name: domainToName(this.hass.localize, domain),
}))
.filter(
(entry) =>
!filter ||
entry.domain.toLowerCase().includes(filter) ||
entry.name.toLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
.map((entry) => entry.domain);
});
protected updated(changed: PropertyValues<this>) {
if (changed.has("expanded") && this.expanded) {
@@ -156,13 +129,7 @@ export class HaFilterDomains extends LitElement {
}
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
const domains = this._domains(
this._states,
this._localize,
this._i18n.locale.language,
this._filter,
this.value
);
const domains = this._domains(this.hass.states, this._filter, this.value);
const visibleDomains = new Set(domains);
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
+11 -29
View File
@@ -1,25 +1,18 @@
import { consume, type ContextType } from "@lit/context";
import { mdiFilterVariantRemove } from "@mdi/js";
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 { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { stringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { deepEqual } from "../common/util/deep-equal";
import {
apiContext,
internationalizationContext,
statesContext,
} from "../data/context";
import type { RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-list";
@@ -29,20 +22,7 @@ import type { HaInputSearch } from "./input/ha-input-search";
@customElement("ha-filter-entities")
export class HaFilterEntities extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: statesContext, subscribe: true })
@state()
private _states!: ContextType<typeof statesContext>;
@consume({ context: internationalizationContext, subscribe: true })
@state()
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@@ -82,7 +62,7 @@ export class HaFilterEntities extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.entities.caption")}
${this.hass.localize("ui.panel.config.entities.caption")}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
@@ -102,10 +82,9 @@ export class HaFilterEntities extends LitElement {
<ha-list class="ha-scrollbar" multi>
<lit-virtualizer
.items=${this._entities(
this._states,
this.hass.states,
this.type,
this._filter || "",
this._i18n.locale.language,
this.value
)}
.keyFunction=${this._keyFunction}
@@ -184,10 +163,9 @@ export class HaFilterEntities extends LitElement {
private _entities = memoizeOne(
(
states: ContextType<typeof statesContext>,
states: HomeAssistant["states"],
type: this["type"],
filter: string,
language: string | undefined,
_value
) => {
const values = Object.values(states);
@@ -202,7 +180,11 @@ export class HaFilterEntities extends LitElement {
.includes(filter))
)
.sort((a, b) =>
stringCompare(computeStateName(a), computeStateName(b), language)
stringCompare(
computeStateName(a),
computeStateName(b),
this.hass.locale.language
)
);
}
);
@@ -221,7 +203,7 @@ export class HaFilterEntities extends LitElement {
for (const entityId of this.value) {
if (this.type) {
relatedPromises.push(findRelated(this._api, "entity", entityId));
relatedPromises.push(findRelated(this.hass, "entity", entityId));
}
}
+12 -38
View File
@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -6,21 +5,14 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import { deepEqual } from "../common/util/deep-equal";
import type { LocalizeFunc } from "../common/translations/localize";
import {
apiContext,
areasContext,
floorsContext,
internationalizationContext,
} from "../data/context";
import { getFloorAreaLookup } from "../data/floor_registry";
import type { RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-floor-icon";
import "./ha-icon";
@@ -34,24 +26,7 @@ import type { HaListSelectable } from "./list/ha-list-selectable";
@customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: areasContext, subscribe: true })
@state()
private _areasReg!: ContextType<typeof areasContext>;
@consume({ context: floorsContext, subscribe: true })
@state()
private _floorsReg!: ContextType<typeof floorsContext>;
@consume({ context: internationalizationContext, subscribe: true })
@state()
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: {
floors?: string[];
@@ -80,7 +55,7 @@ export class HaFilterFloorAreas extends LitElement {
}
protected render() {
const areas = this._areas(this._areasReg, this._floorsReg);
const areas = this._areas(this.hass.areas, this.hass.floors);
return html`
<ha-expansion-panel
@@ -90,7 +65,7 @@ export class HaFilterFloorAreas extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.areas.caption")}
${this.hass.localize("ui.panel.config.areas.caption")}
${this.value?.areas?.length || this.value?.floors?.length
? html`<div class="badge">
${(this.value?.areas?.length || 0) +
@@ -110,7 +85,9 @@ export class HaFilterFloorAreas extends LitElement {
multi
@ha-list-item-selected=${this._handleAdded}
@ha-list-item-deselected=${this._handleRemoved}
aria-label=${this._localize("ui.panel.config.areas.caption")}
aria-label=${this.hass.localize(
"ui.panel.config.areas.caption"
)}
>
${repeat(
areas?.floors || [],
@@ -164,8 +141,8 @@ export class HaFilterFloorAreas extends LitElement {
.type=${"areas"}
class=${classMap({
rtl: computeRTL(
this._i18n.language,
this._i18n.translationMetadata.translations
this.hass.language,
this.hass.translationMetadata.translations
),
floor: hasFloor,
})}
@@ -248,10 +225,7 @@ export class HaFilterFloorAreas extends LitElement {
}
private _areas = memoizeOne(
(
areaReg: ContextType<typeof areasContext>,
floorReg: ContextType<typeof floorsContext>
) => {
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
const areas = Object.values(areaReg);
const floors = Object.values(floorReg);
const floorAreaLookup = getFloorAreaLookup(areas);
@@ -287,7 +261,7 @@ export class HaFilterFloorAreas extends LitElement {
if (this.value.areas) {
for (const areaId of this.value.areas) {
if (this.type) {
relatedPromises.push(findRelated(this._api, "area", areaId));
relatedPromises.push(findRelated(this.hass, "area", areaId));
}
}
}
@@ -295,7 +269,7 @@ export class HaFilterFloorAreas extends LitElement {
if (this.value.floors) {
for (const floorId of this.value.floors) {
if (this.type) {
relatedPromises.push(findRelated(this._api, "floor", floorId));
relatedPromises.push(findRelated(this.hass, "floor", floorId));
}
}
}
+13 -23
View File
@@ -1,4 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import { consume } from "@lit/context";
import type { SelectedDetail } from "@material/mwc-list";
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -6,14 +6,13 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { internationalizationContext, labelsContext } from "../data/context";
import { labelsContext } from "../data/context";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon";
@@ -26,20 +25,14 @@ import type { HaInputSearch } from "./input/ha-input-search";
@customElement("ha-filter-labels")
export class HaFilterLabels extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: internationalizationContext, subscribe: true })
@state()
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: labelsContext, subscribe: true })
@state()
private _labels?: LabelRegistryEntry[];
@@ -52,12 +45,7 @@ export class HaFilterLabels extends LitElement {
private _filteredLabels = memoizeOne(
// `_value` used to recalculate the memoization when the selection changes
(
labels: LabelRegistryEntry[],
filter: string | undefined,
language: string | undefined,
_value
) =>
(labels: LabelRegistryEntry[], filter: string | undefined, _value) =>
labels
.filter(
(label) =>
@@ -66,7 +54,11 @@ export class HaFilterLabels extends LitElement {
label.label_id.toLowerCase().includes(filter)
)
.sort((a, b) =>
stringCompare(a.name || a.label_id, b.name || b.label_id, language)
stringCompare(
a.name || a.label_id,
b.name || b.label_id,
this.hass.locale.language
)
)
);
@@ -79,7 +71,7 @@ export class HaFilterLabels extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.labels.caption")}
${this.hass.localize("ui.panel.config.labels.caption")}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
@@ -104,7 +96,6 @@ export class HaFilterLabels extends LitElement {
this._filteredLabels(
this._labels || [],
this._filter,
this._i18n.locale.language,
this.value
),
(label) => label.label_id,
@@ -138,7 +129,7 @@ export class HaFilterLabels extends LitElement {
class="add"
>
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
${this._localize("ui.panel.config.labels.manage_labels")}
${this.hass.localize("ui.panel.config.labels.manage_labels")}
</ha-list-item>`
: nothing}
`;
@@ -178,7 +169,6 @@ export class HaFilterLabels extends LitElement {
const filteredLabels = this._filteredLabels(
this._labels || [],
this._filter,
this._i18n.locale.language,
this.value
);
+3
View File
@@ -5,6 +5,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon";
@@ -13,6 +14,8 @@ import "./ha-list";
@customElement("ha-filter-states")
export class HaFilterStates extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property({ attribute: false }) public value?: string[];
@@ -8,6 +8,7 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon";
@@ -21,6 +22,8 @@ import "../panels/config/voice-assistants/expose/expose-assistant-icon";
@customElement("ha-filter-voice-assistants")
export class HaFilterVoiceAssistants extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@@ -75,6 +78,7 @@ export class HaFilterVoiceAssistants extends LitElement {
<voice-assistant-brand-icon
slot="graphic"
.voiceAssistantId=${voiceAssistantId}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
${voiceAssistants[voiceAssistantId].name}
+8 -17
View File
@@ -1,13 +1,10 @@
import { consume, type ContextType } from "@lit/context";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../common/entity/supports-feature";
import type { LocalizeFunc } from "../common/translations/localize";
import { apiContext, formattersContext } from "../data/context";
import "./ha-button";
import type { LawnMowerEntity, LawnMowerEntityState } from "../data/lawn_mower";
import { LawnMowerEntityFeature } from "../data/lawn_mower";
import type { HomeAssistant } from "../types";
interface LawnMowerAction {
action: string;
@@ -42,19 +39,13 @@ const LAWN_MOWER_ACTIONS: Partial<
@customElement("ha-lawn_mower-action-button")
class HaLawnMowerActionButton extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
public render() {
const action = LAWN_MOWER_ACTIONS[this.stateObj.state];
const state = this.stateObj.state;
const action = LAWN_MOWER_ACTIONS[state];
if (action && supportsFeature(this.stateObj, action.feature)) {
return html`
@@ -64,14 +55,14 @@ class HaLawnMowerActionButton extends LitElement {
.service=${action.service}
size="s"
>
${this._localize(`ui.card.lawn_mower.actions.${action.action}`)}
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
</ha-button>
`;
}
return html`
<ha-button appearance="plain" disabled>
${this._formatters?.formatEntityState(this.stateObj)}
${this.hass.formatEntityState(this.stateObj)}
</ha-button>
`;
}
@@ -80,7 +71,7 @@ class HaLawnMowerActionButton extends LitElement {
ev.stopPropagation();
const stateObj = this.stateObj;
const service = ev.target.service;
this._api.callService("lawn_mower", service, {
this.hass.callService("lawn_mower", service, {
entity_id: stateObj.entity_id,
});
}
+1
View File
@@ -243,6 +243,7 @@ export class HaNavigationPicker extends LitElement {
items = multiTermSortedSearch(
items,
searchString,
DEFAULT_SEARCH_KEYS,
(item) => item.id,
fuseIndex
);
+1
View File
@@ -492,6 +492,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
this._allItems,
searchString,
this.searchKeys || DEFAULT_SEARCH_KEYS,
(item) => item.id,
index
);
+1
View File
@@ -78,6 +78,7 @@ export class HaPictureUpload extends LitElement {
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
+25 -27
View File
@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import { mdiCamera } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
@@ -9,11 +8,9 @@ import { customElement, property, query, state } from "lit/decorators";
// WebAssembly port of ZXing:
import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import { configContext } from "../data/context";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-dropdown";
@@ -36,13 +33,7 @@ prepareZXingModule({
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public description?: string;
@@ -115,7 +106,7 @@ class HaQrScanner extends LitElement {
${this._error || this._warning}
${this._error
? html`<ha-button @click=${this._retry} slot="action">
${this._localize("ui.components.qr-scanner.retry")}
${this.hass.localize("ui.components.qr-scanner.retry")}
</ha-button>`
: nothing}
</ha-alert>`
@@ -135,7 +126,7 @@ class HaQrScanner extends LitElement {
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}>
<ha-icon-button
slot="trigger"
.label=${this._localize(
.label=${this.hass.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
@@ -155,24 +146,28 @@ class HaQrScanner extends LitElement {
</div>`
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this._localize("ui.components.qr-scanner.only_https_supported")
: this._localize("ui.components.qr-scanner.not_supported")}
? this.hass.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.hass.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this._localize("ui.components.qr-scanner.manual_input")}</p>
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<ha-input
.label=${this._localize("ui.components.qr-scanner.enter_qr_code")}
.label=${this.hass.localize(
"ui.components.qr-scanner.enter_qr_code"
)}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></ha-input>
<ha-button @click=${this._manualSubmit}>
${this._localize("ui.common.submit")}
${this.hass.localize("ui.common.submit")}
</ha-button>
</div>`}`;
}
private get _nativeBarcodeScanner(): boolean {
return Boolean(this._config.auth.external?.config.hasBarCodeScanner);
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
}
private async _loadQrScanner() {
@@ -187,7 +182,7 @@ class HaQrScanner extends LitElement {
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._reportError(
this._localize("ui.components.qr-scanner.no_camera_found")
this.hass.localize("ui.components.qr-scanner.no_camera_found")
);
return;
}
@@ -275,7 +270,7 @@ class HaQrScanner extends LitElement {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
this._localize("ui.components.qr-scanner.wrong_code", {
this.hass.localize("ui.components.qr-scanner.wrong_code", {
format: msg.payload.format,
rawValue: msg.payload.rawValue,
})
@@ -293,17 +288,20 @@ class HaQrScanner extends LitElement {
}
return true;
});
this._config.auth.external!.fireMessage({
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
title:
this.title || this._localize("ui.components.qr-scanner.app.title"),
this.title ||
this.hass.localize("ui.components.qr-scanner.app.title"),
description:
this.description ||
this._localize("ui.components.qr-scanner.app.description"),
this.hass.localize("ui.components.qr-scanner.app.description"),
alternative_option_label:
this.alternativeOptionLabel ||
this._localize("ui.components.qr-scanner.app.alternativeOptionLabel"),
this.hass.localize(
"ui.components.qr-scanner.app.alternativeOptionLabel"
),
},
});
}
@@ -311,7 +309,7 @@ class HaQrScanner extends LitElement {
private _closeExternalScanner() {
this._removeListener?.();
this._removeListener = undefined;
this._config.auth.external!.fireMessage({
this.hass.auth.external!.fireMessage({
type: "bar_code/close",
});
}
@@ -320,7 +318,7 @@ class HaQrScanner extends LitElement {
if (!this._nativeBarcodeScanner) {
return;
}
this._config.auth.external!.fireMessage({
this.hass.auth.external!.fireMessage({
type: "bar_code/notify",
payload: {
message,
@@ -23,6 +23,7 @@ export class HaAreasDisplaySelector extends LitElement {
protected render() {
return html`
<ha-areas-display-editor
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -4,6 +4,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import type { ColorTempSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-labeled-slider";
import { generateColorTemperatureGradient } from "../../dialogs/more-info/components/lights/light-color-temp-picker";
import {
@@ -14,6 +15,8 @@ import {
@customElement("ha-selector-color_temp")
export class HaColorTempSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ColorTempSelector;
@property() public value?: string;
@@ -37,6 +37,7 @@ export class HaFileSelector extends LitElement {
protected render() {
return html`
<ha-file-upload
.hass=${this.hass}
.accept=${this.selector.file?.accept}
.icon=${mdiFile}
.label=${this.label}
@@ -84,6 +84,7 @@ export class HaLocationSelector extends LitElement {
<p>${this.label ? this.label : ""}</p>
<ha-locations-editor
class="flex"
.hass=${this.hass}
.helper=${this.helper}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@@ -1,6 +1,5 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiClose, mdiConnection, mdiMemory, mdiPencil, mdiUsb } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@@ -406,12 +405,11 @@ export class HaSerialPortSelector extends LitElement {
}
let groupItems: SerialPickerItem[] = grouped[type];
if (searchString) {
const fuseIndex = Fuse.createIndex(DEFAULT_SEARCH_KEYS, groupItems);
groupItems = multiTermSortedSearch(
groupItems,
searchString,
(item) => item.id,
fuseIndex
DEFAULT_SEARCH_KEYS,
(item) => item.id
);
}
if (!groupItems.length) {
@@ -3,13 +3,15 @@ import { customElement, property, query } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { StringSelector } from "../../data/selector";
import type { ValueChangedEvent } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-textarea";
import "../input/ha-input";
import "../input/ha-input-multi";
@customElement("ha-selector-text")
export class HaTextSelector extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: any;
@property() public name?: string;
@@ -23,6 +23,7 @@ export class HaThemeSelector extends LitElement {
protected render() {
return html`
<ha-theme-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
+2
View File
@@ -1116,6 +1116,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return multiTermSortedSearch(
items,
searchTerm,
weightedKeys,
(item) => item.id,
fuseIndex
);
@@ -1232,6 +1233,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<state-badge
slot="start"
.stateObj=${(item as EntityComboBoxItem).stateObj}
.hass=${this.hass}
></state-badge>
`
: type === "device" && (item as DevicePickerItem).domain
+7 -15
View File
@@ -1,12 +1,10 @@
import { consume, type ContextType } from "@lit/context";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { internationalizationContext, uiContext } from "../data/context";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@@ -25,13 +23,7 @@ export class HaThemePicker extends LitElement {
@property({ attribute: "include-default", type: Boolean })
public includeDefault = false;
@state()
@consume({ context: uiContext, subscribe: true })
private _ui?: ContextType<typeof uiContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -64,8 +56,8 @@ export class HaThemePicker extends LitElement {
private _getItems = () =>
this._getThemeOptions(
this._ui?.themes.themes || {},
this._i18n?.locale.language || "en",
this.hass?.themes.themes || {},
this.hass?.locale.language || "en",
this.includeDefault
);
@@ -78,10 +70,10 @@ export class HaThemePicker extends LitElement {
return html`
<ha-generic-picker
.label=${this.label ??
this._i18n?.localize("ui.components.theme-picker.theme") ??
this.hass?.localize("ui.components.theme-picker.theme") ??
"Theme"}
.placeholder=${this.noThemeLabel ??
this._i18n?.localize("ui.components.theme-picker.no_theme")}
this.hass?.localize("ui.components.theme-picker.no_theme")}
.helper=${this.helper}
.value=${this.value}
.valueRenderer=${this._valueRenderer}
+1
View File
@@ -73,6 +73,7 @@ export class HaThemeSettings extends LitElement {
${this.showThemePicker
? html`
<ha-theme-picker
.hass=${this.hass}
.label=${this.labels?.theme}
.noThemeLabel=${this.labels?.noTheme}
.value=${themeSettings?.theme || undefined}
+14 -22
View File
@@ -1,4 +1,4 @@
import timezones from "google-timezones-json";
import { getTimeZones } from "@vvo/tzdb";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -13,31 +13,23 @@ const SEARCH_KEYS = [
{ name: "secondary", weight: 8 },
];
// google-timezones-json is missing the bare "UTC" and "Etc/UTC" zones, even
// though both are valid IANA identifiers and common server defaults. Without
// them a "UTC" configuration shows up as an unknown time zone. Add them back.
// @vvo/tzdb is missing the bare "UTC" zone, even though it is a valid IANA
// identifier and a common server default. Add UTC back so a
// "UTC" configuration can be selected.
const ADDITIONAL_TIMEZONES: PickerComboBoxItem[] = [
{ id: "UTC", primary: "(GMT+00:00) UTC", secondary: "UTC" },
{ id: "Etc/UTC", primary: "(GMT+00:00) UTC", secondary: "Etc/UTC" },
{ id: "UTC", primary: "+00:00 UTC", secondary: "UTC" },
];
// google-timezones-json also ships an invalid IANA identifier. Correct it so
// the zone can be selected (the backend rejects the invalid id).
const TIMEZONE_ID_CORRECTIONS: Record<string, string> = {
"Asia/Yuzhno-Sakhalinsk": "Asia/Sakhalin",
};
export const getTimezoneOptions = (): PickerComboBoxItem[] => {
const options: PickerComboBoxItem[] = Object.entries(
timezones as Record<string, string>
).map(([key, value]) => {
const id = TIMEZONE_ID_CORRECTIONS[key] ?? key;
return {
id,
primary: value,
secondary: id,
};
});
const options: PickerComboBoxItem[] = getTimeZones({ includeUtc: true }).map(
(timezone) => {
return {
id: timezone.name,
primary: timezone.rawFormat,
secondary: timezone.name,
};
}
);
for (const timezone of ADDITIONAL_TIMEZONES) {
if (!options.some((option) => option.id === timezone.id)) {
-6
View File
@@ -2,7 +2,6 @@ import {
mdiAlertCircleOutline,
mdiCheckCircleOutline,
mdiChevronDown,
mdiCircleOffOutline,
mdiHelpCircleOutline,
mdiProgressClock,
mdiProgressWrench,
@@ -85,11 +84,6 @@ class HaTracePicker extends LitElement {
"ui.panel.config.automation.trace.picker.debugged"
);
item.icon_path = mdiProgressWrench;
} else if (trace.not_triggered) {
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.not_triggered"
);
item.icon_path = mdiCircleOffOutline;
} else if (trace.script_execution === "finished") {
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.finished",
+11 -17
View File
@@ -1,12 +1,9 @@
import { consume, type ContextType } from "@lit/context";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import type { HassEntity } from "home-assistant-js-websocket";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { apiContext } from "../data/context";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-button";
const STATES_INTERCEPTABLE: Record<
@@ -49,10 +46,7 @@ const STATES_INTERCEPTABLE: Record<
@customElement("ha-vacuum-state")
export class HaVacuumState extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@@ -74,19 +68,19 @@ export class HaVacuumState extends LitElement {
}
private _computeInterceptable(
stateString: string,
state: string,
supportedFeatures: number | undefined
) {
return stateString in STATES_INTERCEPTABLE && supportedFeatures !== 0;
return state in STATES_INTERCEPTABLE && supportedFeatures !== 0;
}
private _computeLabel(stateString: string, interceptable: boolean) {
private _computeLabel(state: string, interceptable: boolean) {
return interceptable
? this._localize(
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[stateString].action}`
? this.hass.localize(
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[state].action}`
)
: this._localize(
`component.vacuum.entity_component._.state.${stateString}`
: this.hass.localize(
`component.vacuum.entity_component._.state.${state}`
);
}
@@ -94,7 +88,7 @@ export class HaVacuumState extends LitElement {
ev.stopPropagation();
const stateObj = this.stateObj;
const service = STATES_INTERCEPTABLE[stateObj.state].service;
await this._api.callService("vacuum", service, {
await this.hass.callService("vacuum", service, {
entity_id: stateObj.entity_id,
});
}
+15 -39
View File
@@ -1,39 +1,14 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement, property } from "lit/decorators";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../common/decorators/transform";
import type { HassEntity } from "home-assistant-js-websocket";
import { formatNumber } from "../common/number/format_number";
import {
configContext,
formattersContext,
internationalizationContext,
} from "../data/context";
import type { FrontendLocaleData } from "../data/translation";
import { haStyle } from "../resources/styles";
import type {
HomeAssistantConfig,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../types";
import type { HomeAssistant } from "../types";
@customElement("ha-water_heater-state")
export class HaWaterHeaterState extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: HomeAssistantFormatters;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state()
@consume({ context: configContext, subscribe: true })
private _hassConfig?: HomeAssistantConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@@ -41,16 +16,17 @@ export class HaWaterHeaterState extends LitElement {
return html`
<div class="target">
<span class="state-label label">
${this._formatters?.formatEntityState(this.stateObj)}
${this.hass.formatEntityState(this.stateObj)}
</span>
<span class="label">${this._computeTarget()}</span>
<span class="label"
>${this._computeTarget(this.hass, this.stateObj)}</span
>
</div>
`;
}
private _computeTarget() {
if (!this._locale || !this._hassConfig || !this.stateObj) return null;
const stateObj = this.stateObj;
private _computeTarget(hass: HomeAssistant, stateObj: HassEntity) {
if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined.
if (
@@ -59,17 +35,17 @@ export class HaWaterHeaterState extends LitElement {
) {
return `${formatNumber(
stateObj.attributes.target_temp_low,
this._locale
this.hass.locale
)} ${formatNumber(
stateObj.attributes.target_temp_high,
this._locale
)} ${this._hassConfig.config.unit_system.temperature}`;
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${formatNumber(
stateObj.attributes.temperature,
this._locale
)} ${this._hassConfig.config.unit_system.temperature}`;
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
}
return "";
+3 -1
View File
@@ -13,7 +13,7 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { ThemeMode } from "../../types";
import type { HomeAssistant, ThemeMode } from "../../types";
import "../ha-input-helper-text";
import "./ha-map";
import type { HaMap } from "./ha-map";
@@ -45,6 +45,8 @@ export interface MarkerLocation {
@customElement("ha-locations-editor")
export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public locations?: MarkerLocation[];
@property() public helper?: string;
@@ -100,6 +100,7 @@ class DialogJoinMediaPlayers extends LitElement {
: nothing}
<div class="content">
<ha-media-player-toggle
.hass=${this.hass}
.entityId=${entityId}
checked
disabled
@@ -107,6 +108,7 @@ class DialogJoinMediaPlayers extends LitElement {
${this._mediaPlayerEntities(this.hass.entities).map(
(entity) =>
html`<ha-media-player-toggle
.hass=${this.hass}
.entityId=${entity.entity_id}
.checked=${this._selectedEntities.includes(entity.entity_id)}
@change=${this._handleSelectedChange}
@@ -101,6 +101,7 @@ class DialogMediaPlayerBrowse extends LitElement {
</span>
<ha-media-manage-button
slot="actionItems"
.hass=${this.hass}
.currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia}
></ha-media-manage-button>
@@ -1,16 +1,13 @@
import { consume, type ContextType } from "@lit/context";
import { mdiFolderEdit } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import { configContext } from "../../data/context";
import type { MediaPlayerItem } from "../../data/media-player";
import {
isLocalMediaSourceContentId,
isImageUploadMediaSourceContentId,
} from "../../data/media_source";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
import "../ha-svg-icon";
import "../ha-button";
import { showMediaManageDialog } from "./show-media-manage-dialog";
@@ -23,13 +20,7 @@ declare global {
@customElement("ha-media-manage-button")
class MediaManageButton extends LitElement {
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) currentItem?: MediaPlayerItem;
@@ -40,7 +31,7 @@ class MediaManageButton extends LitElement {
!this.currentItem ||
!(
isLocalMediaSourceContentId(this.currentItem.media_content_id || "") ||
(this._config.user?.is_admin &&
(this.hass!.user?.is_admin &&
isImageUploadMediaSourceContentId(this.currentItem.media_content_id))
)
) {
@@ -49,7 +40,9 @@ class MediaManageButton extends LitElement {
return html`
<ha-button appearance="filled" size="s" @click=${this._manage}>
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
${this._localize("ui.components.media-browser.file_management.manage")}
${this.hass.localize(
"ui.components.media-browser.file_management.manage"
)}
</ha-button>
`;
}
@@ -1,66 +1,35 @@
import { consume, type ContextType } from "@lit/context";
import { type CSSResultGroup, LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeRTL } from "../../common/util/compute_rtl";
import {
areasContext,
devicesContext,
entitiesContext,
floorsContext,
internationalizationContext,
} from "../../data/context";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-switch";
import "../ha-svg-icon";
@customElement("ha-media-player-toggle")
class HaMediaPlayerToggle extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@property({ type: Boolean }) public checked = false;
@property({ type: Boolean }) public disabled = false;
@state()
@consumeEntityState({ entityIdPath: ["entityId"] })
private _stateObj?: HassEntity;
@consume({ context: entitiesContext, subscribe: true })
@state()
private _entities!: ContextType<typeof entitiesContext>;
@consume({ context: devicesContext, subscribe: true })
@state()
private _devices!: ContextType<typeof devicesContext>;
@consume({ context: areasContext, subscribe: true })
@state()
private _areas!: ContextType<typeof areasContext>;
@consume({ context: floorsContext, subscribe: true })
@state()
private _floors!: ContextType<typeof floorsContext>;
@consume({ context: internationalizationContext, subscribe: true })
@state()
private _i18n!: ContextType<typeof internationalizationContext>;
private _computeDisplayData = memoizeOne(
(
entityId: string,
entities: ContextType<typeof entitiesContext>,
devices: ContextType<typeof devicesContext>,
areas: ContextType<typeof areasContext>,
floors: ContextType<typeof floorsContext>,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
isRTL: boolean,
stateObj: HassEntity
stateObj: HomeAssistant["states"][string]
) => {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
@@ -81,11 +50,7 @@ class HaMediaPlayerToggle extends LitElement {
);
protected render() {
const stateObj = this._stateObj;
if (!stateObj) {
return nothing;
}
const stateObj = this.hass.states[this.entityId];
let icon = mdiSpeaker;
if (stateObj.state === "playing") {
@@ -95,16 +60,16 @@ class HaMediaPlayerToggle extends LitElement {
}
const isRTL = computeRTL(
this._i18n.language,
this._i18n.translationMetadata.translations
this.hass.language,
this.hass.translationMetadata.translations
);
const { primary, secondary } = this._computeDisplayData(
this.entityId,
this._entities,
this._devices,
this._areas,
this._floors,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors,
isRTL,
stateObj
);
+2 -2
View File
@@ -7,8 +7,8 @@ import { customElement, property } from "lit/decorators";
export class HaProgressRing extends ProgressRing {
@property() public size?: "tiny" | "small" | "medium" | "large";
protected willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
public updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("size")) {
switch (this.size) {
+2 -19
View File
@@ -17,10 +17,9 @@ import type {
ChooseActionTraceStep,
TraceExtended,
} from "../../data/trace";
import { getDataFromPath, isTriggerPath } from "../../data/trace";
import { getDataFromPath } from "../../data/trace";
import "../../panels/logbook/ha-logbook-renderer";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-code-editor";
import "../ha-icon-button";
import "../ha-tab-group";
@@ -70,7 +69,7 @@ export class HaTracePathDetails extends LitElement {
protected render(): TemplateResult {
return html`
<div class="padded-box trace-info">
${this._renderNotTriggeredNotice()} ${this._renderSelectedTraceInfo()}
${this._renderSelectedTraceInfo()}
</div>
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
@@ -96,22 +95,6 @@ export class HaTracePathDetails extends LitElement {
`;
}
private _renderNotTriggeredNotice() {
if (
!this.trace.not_triggered ||
!this.selected?.path ||
!isTriggerPath(this.selected.path) ||
!(this.selected.path in this.trace.trace)
) {
return nothing;
}
return html`<ha-alert alert-type="info">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.not_triggered"
)}
</ha-alert>`;
}
private _renderSelectedTraceInfo() {
const paths = this.trace.trace;
-6
View File
@@ -20,9 +20,6 @@ export class HatGraphNode extends LitElement {
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
notEnabled = false;
@property({ attribute: "not-triggered", reflect: true, type: Boolean })
notTriggered = false;
@property({ attribute: "graph-start", reflect: true, type: Boolean })
graphStart = false;
@@ -130,9 +127,6 @@ export class HatGraphNode extends LitElement {
--stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr);
}
:host([not-triggered]) circle {
stroke-dasharray: 4 3;
}
:host([not-enabled]) circle {
--stroke-clr: var(--disabled-clr);
}
+7 -9
View File
@@ -44,6 +44,7 @@ import type {
IfActionTraceStep,
TraceExtended,
} from "../../data/trace";
import type { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-service-icon";
import "./hat-graph-branch";
@@ -75,6 +76,8 @@ export class HatScriptGraph extends LitElement {
@query("hat-graph-node[active], hat-graph-branch[active]")
private _activeNode?: HTMLElement;
public hass!: HomeAssistant;
public renderedNodes: Record<string, NodeInfo> = {};
public trackedNodes: Record<string, NodeInfo> = {};
@@ -87,27 +90,21 @@ export class HatScriptGraph extends LitElement {
private _renderTrigger(config: Trigger, i: number) {
const path = `trigger/${i}`;
const tracked = this.trace && path in this.trace.trace;
// A not-triggered trace records the trigger that evaluated a change but
// decided not to fire. It is still selectable (to view the reason), but
// must not be shown as the path that ran.
const notTriggered = !!(tracked && this.trace.not_triggered);
const track = tracked && !notTriggered;
const track = this.trace && path in this.trace.trace;
this.renderedNodes[path] = { config, path, type: "trigger" };
if (tracked) {
if (track) {
this.trackedNodes[path] = this.renderedNodes[path];
}
return html`
<hat-graph-node
graph-start
?track=${track}
?not-triggered=${notTriggered}
@focus=${this._selectNode(config, path, "trigger")}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${"enabled" in config && config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${tracked ? "0" : "-1"}
tabindex=${track ? "0" : "-1"}
></hat-graph-node>
`;
}
@@ -454,6 +451,7 @@ export class HatScriptGraph extends LitElement {
${node.action
? html`<ha-service-icon
slot="icon"
.hass=${this.hass}
.service=${node.action}
></ha-service-icon>`
: nothing}
@@ -2,7 +2,6 @@ import { consume } from "@lit/context";
import {
mdiAlertCircle,
mdiCircle,
mdiCircleOffOutline,
mdiCircleOutline,
mdiProgressClock,
mdiProgressWrench,
@@ -324,23 +323,6 @@ class ActionRenderer {
}
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
if (this.trace.not_triggered) {
this._renderEntry(
triggerStep.path,
this.hass.localize(
"ui.panel.config.automation.trace.messages.evaluated_not_triggered",
{
time: formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale,
this.hass.config
),
}
),
mdiCircleOffOutline
);
return index + 1;
}
this._renderEntry(
triggerStep.path,
this.hass.localize(
@@ -743,16 +725,6 @@ export class HaAutomationTracer extends LitElement {
),
icon: mdiProgressWrench,
};
} else if (this.trace.not_triggered) {
entry = {
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.not_triggered",
{
time: renderFinishedAt(),
}
),
icon: mdiCircleOffOutline,
};
} else if (this.trace.script_execution === "finished") {
entry = {
description: this.hass.localize(
+5 -12
View File
@@ -1,21 +1,14 @@
import { consume, type ContextType } from "@lit/context";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { haStyle } from "../resources/styles";
import { configContext, uiContext } from "../data/context";
import type { HomeAssistant } from "../types";
import { voiceAssistants } from "../data/expose";
import { brandsUrl } from "../util/brands-url";
@customElement("voice-assistant-brand-icon")
export class VoiceAssistantBrandicon extends LitElement {
@state()
@consume({ context: uiContext, subscribe: true })
private _ui!: ContextType<typeof uiContext>;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public voiceAssistantId!: string;
@@ -28,9 +21,9 @@ export class VoiceAssistantBrandicon extends LitElement {
{
domain: voiceAssistants[this.voiceAssistantId].domain,
type: "icon",
darkOptimized: this._ui.themes?.darkMode,
darkOptimized: this.hass.themes?.darkMode,
},
this._config.auth.data.hassUrl
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
+1 -5
View File
@@ -6,15 +6,11 @@ export interface AlexaEntity {
interfaces: string[];
}
export interface AlexaEntityConfig {
name?: string | null;
}
export const fetchCloudAlexaEntities = (hass: HomeAssistant) =>
hass.callWS<AlexaEntity[]>({ type: "cloud/alexa/entities" });
export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) =>
hass.callWS<AlexaEntityConfig>({
hass.callWS<AlexaEntity>({
type: "cloud/alexa/entities/get",
entity_id,
});
-3
View File
@@ -1124,9 +1124,6 @@ const describeLegacyCondition = (
hasAttribute: attribute !== "" ? "true" : "false",
attribute: attribute,
numberOfEntities: entities.length,
// With "any", entities are joined with "or", which takes a singular
// verb in English even for multiple entities ("A or B is ...").
matchAny: condition.match === "any" ? "true" : "false",
entities:
condition.match === "any"
? formatListWithOrs(hass.locale, entities)
+2 -13
View File
@@ -172,23 +172,12 @@ export const removeCloudData = (hass: HomeAssistant) =>
export const updateCloudGoogleEntityConfig = (
hass: HomeAssistant,
entity_id: string,
values: { disable_2fa?: boolean; name?: string | null; aliases?: string[] }
disable_2fa: boolean
) =>
hass.callWS({
type: "cloud/google_assistant/entities/update",
entity_id,
...values,
});
export const updateCloudAlexaEntityConfig = (
hass: HomeAssistant,
entity_id: string,
name: string | null
) =>
hass.callWS({
type: "cloud/alexa/entities/update",
entity_id,
name,
disable_2fa,
});
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
+1 -30
View File
@@ -1,6 +1,6 @@
import { createContext } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import type {
HomeAssistant,
HomeAssistantApi,
@@ -196,33 +196,4 @@ declare global {
}
}
/**
* Set the related context to an entity (or clear it when no entity), so nearby
* pickers float relevant entities.
* @param node - The node to fire the event on.
* @param context - The context to set, or undefined to clear.
*/
export const fireRelatedContext = (
node: HTMLElement,
context: RelatedContextItem | undefined
): void => {
fireEvent(node, "hass-related-context", context);
};
/**
* Set the related context to an entity (or clear it when no entity), so nearby
* pickers float relevant entities. Fired by editors.
* @param node - The node to fire the event on.
* @param entityId - The entity to set, or undefined to clear.
*/
export const fireEntityRelatedContext = (
node: HTMLElement,
entityId: string | undefined
): void => {
fireRelatedContext(
node,
entityId ? { itemType: "entity", itemId: entityId } : undefined
);
};
// #endregion related-context
-73
View File
@@ -1,10 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { getEntityAreaId } from "../../common/entity/context/get_entity_context";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../common/entity/compute_state_name";
import type { RelatedIdSets } from "../../common/search/related-context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
@@ -15,7 +12,6 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity";
export interface EntityComboBoxItem extends PickerComboBoxItem {
domain_name?: string;
stateObj?: HassEntity;
relatedRank?: number;
}
export const entityComboBoxKeys: FuseWeightedKey[] = [
@@ -190,72 +186,3 @@ export const getEntities = (
return items;
};
const RELATED_RANK_UNRELATED = 3;
const entityRelatedRank = (
entityId: string | undefined,
related: RelatedIdSets,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): number => {
if (!entityId) {
return RELATED_RANK_UNRELATED;
}
if (related.entities.has(entityId)) {
return 0;
}
const deviceId = entities[entityId]?.device_id;
if (deviceId && related.devices.has(deviceId)) {
return 1;
}
const areaId = getEntityAreaId(entityId, entities, devices);
if (areaId && related.areas.has(areaId)) {
return 2;
}
return RELATED_RANK_UNRELATED;
};
/**
* Annotate entity items with their closeness to the related context, so they
* can be floated to the top. The entity itself ranks closest, then its device,
* then its area; anything unrelated keeps the lowest rank.
*/
export const markEntitiesRelated = (
items: EntityComboBoxItem[],
related: RelatedIdSets,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): EntityComboBoxItem[] =>
items.map((item) => ({
...item,
relatedRank: entityRelatedRank(
item.stateObj?.entity_id,
related,
entities,
devices
),
}));
/**
* Sort entity items by related closeness (entity, then device, then area, then
* the rest). Pass `language` to break ties within a tier alphabetically by
* label; omit it to keep the incoming order (e.g. search relevance).
*/
export const sortEntitiesByRelatedRank = (
items: EntityComboBoxItem[],
language?: string
): EntityComboBoxItem[] =>
[...items].sort((a, b) => {
const rankDiff =
(a.relatedRank ?? RELATED_RANK_UNRELATED) -
(b.relatedRank ?? RELATED_RANK_UNRELATED);
if (rankDiff !== 0 || language === undefined) {
return rankDiff;
}
return caseInsensitiveStringCompare(
a.sorting_label ?? "",
b.sorting_label ?? "",
language
);
});
-2
View File
@@ -5,8 +5,6 @@ export interface GoogleEntity {
traits: string[];
might_2fa: boolean;
disable_2fa?: boolean;
name?: string | null;
aliases?: string[] | null;
}
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
+1 -4
View File
@@ -184,11 +184,8 @@ export const createHistoricState = (
// translate the bare description here.
export const localizeTriggerSource = (
localize: LocalizeFunc,
source: string | null
source: string
) => {
if (!source) {
return "";
}
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
const phrase = triggerPhrases[key];
if (source.startsWith(phrase)) {
+4 -17
View File
@@ -18,17 +18,12 @@ interface BaseTraceStep {
export interface TriggerTraceStep extends BaseTraceStep {
changed_variables: {
trigger: {
alias?: string | null;
// Absent on not-triggered traces, which have no trigger description.
description?: string;
alias?: string;
description: string;
[key: string]: unknown;
};
[key: string]: unknown;
};
// Present on not-triggered traces: a machine-readable reason code explaining
// why the trigger evaluated a relevant change but decided not to fire, plus
// optional diagnostic context.
result?: { reason: string; data?: Record<string, unknown> };
}
export interface ConditionTraceStep extends BaseTraceStep {
@@ -66,7 +61,6 @@ export interface ChooseChoiceActionTraceStep extends BaseTraceStep {
export type ActionTraceStep =
| BaseTraceStep
| TriggerTraceStep
| ConditionTraceStep
| CallServiceActionTraceStep
| ChooseActionTraceStep
@@ -79,9 +73,6 @@ interface BaseTrace {
last_step: string | null;
run_id: string;
state: "running" | "stopped" | "debugged";
// True for traces recording that a trigger evaluated a relevant change but
// did not fire. These are counted separately from actual runs.
not_triggered?: boolean;
timestamp: {
start: string;
finish: string | null;
@@ -102,10 +93,7 @@ interface BaseTrace {
| "error"
// The exception is in the trace itself or in the last element of the trace
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
| "cancelled"
// No action was executed because a trigger evaluated a relevant change but
// decided not to fire; the reason is in the trigger step of the trace
| "not_triggered";
| "cancelled";
}
interface BaseTraceExtended {
@@ -115,8 +103,7 @@ interface BaseTraceExtended {
export interface AutomationTrace extends BaseTrace {
domain: "automation";
// `null` for not-triggered traces, which have no trigger description.
trigger: string | null;
trigger: string;
}
export interface AutomationTraceExtended
-1
View File
@@ -322,7 +322,6 @@ export interface ZWaveJSDataCollectionStatus {
export interface ZWaveJSRefreshNodeStatusMessage {
event: string;
stage?: string;
progress?: number;
}
export interface ZWaveJSRebuildRoutesStatusMessage {
@@ -39,7 +39,11 @@ class EntityPreviewRow extends LitElement {
return nothing;
}
const stateObj = this.stateObj;
return html`<state-badge .stateObj=${stateObj} stateColor></state-badge>
return html`<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
stateColor
></state-badge>
<div class="name" .title=${computeStateName(stateObj)}>
${computeStateName(stateObj)}
</div>
@@ -197,7 +201,12 @@ class EntityPreviewRow extends LitElement {
stateObj.state === "on" || stateObj.state === "off" || noValue;
return html`
${showToggle
? html` <ha-entity-toggle .stateObj=${stateObj}></ha-entity-toggle> `
? html`
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
`
: this.hass.formatEntityState(stateObj)}
`;
}
@@ -102,13 +102,6 @@ export class DialogEnterCode
this._showClearButton = !!val;
}
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.preventDefault();
this._submit();
}
}
protected render() {
if (!this._dialogParams || !this.hass) {
return nothing;
@@ -134,7 +127,6 @@ export class DialogEnterCode
autoValidate
validateOnInitialRender
pattern=${ifDefined(this._dialogParams.codePattern)}
@keydown=${this._handleKeyDown}
inputmode="text"
></ha-input>
<ha-dialog-footer slot="footer">
@@ -171,7 +163,6 @@ export class DialogEnterCode
inputmode="numeric"
?autofocus=${!this._narrow}
password-toggle
@keydown=${this._handleKeyDown}
></ha-input>
<div class="keypad">
${BUTTONS.map((value) =>
@@ -1,33 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import "../../../../panels/config/voice-assistants/voice-assistant-settings";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-view-voice-assistant-settings")
class MoreInfoViewVoiceAssistantSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@property({ attribute: false }) public params?: { assistant: string };
protected render() {
if (!this.params || !this.entry) {
return nothing;
}
return html`<voice-assistant-settings
.hass=${this.hass}
.entityId=${this.entry.entity_id}
.assistant=${this.params.assistant}
.entry=${this.entry}
></voice-assistant-settings>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-voice-assistant-settings": MoreInfoViewVoiceAssistantSettings;
}
}
@@ -7,7 +7,6 @@ import type { ExposeEntitySettings } from "../../../../data/expose";
import { voiceAssistants } from "../../../../data/expose";
import "../../../../panels/config/voice-assistants/entity-voice-settings";
import type { HomeAssistant } from "../../../../types";
import { showVoiceAssistantSettingsView } from "./show-view-voice-assistant-settings";
@customElement("ha-more-info-view-voice-assistants")
class MoreInfoViewVoiceAssistants extends LitElement {
@@ -34,19 +33,9 @@ class MoreInfoViewVoiceAssistants extends LitElement {
.entityId=${this.entry.entity_id}
.entry=${this.entry}
.exposed=${this._calculateExposed(this.entry)}
@edit-assistant=${this._editAssistant}
></entity-voice-settings>`;
}
private _editAssistant(ev: CustomEvent) {
const assistant = ev.detail.assistant;
showVoiceAssistantSettingsView(
this,
voiceAssistants[assistant].name,
assistant
);
}
static get styles(): CSSResultGroup {
return [
css`
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export const loadVoiceAssistantSettingsView = () =>
import("./ha-more-info-view-voice-assistant-settings");
export const showVoiceAssistantSettingsView = (
element: HTMLElement,
title: string,
assistant: string
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-voice-assistant-settings",
viewImport: loadVoiceAssistantSettingsView,
viewTitle: title,
viewParams: { assistant },
});
};
@@ -42,7 +42,7 @@ export class MoreInfoLogbook extends LitElement {
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._entityIdAsList(this.entityId)}
name-detail="none"
.scope=${"entity"}
narrow
no-icon
graph-color
+12 -6
View File
@@ -310,6 +310,7 @@ export class QuickBar extends LitElement {
<state-badge
slot="start"
.stateObj=${(item as EntityComboBoxItem).stateObj}
.hass=${this.hass}
></state-badge>
`
: "domain" in item && item.domain
@@ -460,7 +461,8 @@ export class QuickBar extends LitElement {
navigateItems = this._filterGroup(
"navigate",
navigateItems,
filter
filter,
navigateComboBoxKeys
) as NavigationComboBoxItem[];
}
@@ -481,7 +483,8 @@ export class QuickBar extends LitElement {
commandItems = this._filterGroup(
"command",
commandItems,
filter
filter,
commandComboBoxKeys
) as ActionCommandComboBoxItem[];
}
@@ -511,7 +514,8 @@ export class QuickBar extends LitElement {
this._filterGroup(
"entity",
entityItems,
filter
filter,
entityComboBoxKeys
) as EntityComboBoxItem[]
);
} else {
@@ -547,7 +551,7 @@ export class QuickBar extends LitElement {
if (filter) {
deviceItems = sortRelatedFirst(
this._filterGroup("device", deviceItems, filter)
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
);
} else {
deviceItems = this._sortRelatedByLabel(deviceItems);
@@ -579,7 +583,7 @@ export class QuickBar extends LitElement {
if (filter) {
areaItems = sortRelatedFirst(
this._filterGroup("area", areaItems, filter)
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
);
} else {
areaItems = this._sortRelatedByLabel(areaItems);
@@ -657,13 +661,15 @@ export class QuickBar extends LitElement {
private _filterGroup(
type: QuickBarSection,
items: PickerComboBoxItem[],
searchTerm: string
searchTerm: string,
weightedKeys: FuseWeightedKey[]
) {
const fuseIndex = this._fuseIndexes[type](items);
return multiTermSortedSearch(
items,
searchTerm,
weightedKeys,
(item: PickerComboBoxItem) => item.id,
fuseIndex
);
@@ -64,9 +64,13 @@ export class CloudStepIntro extends LitElement {
<div class="logos">
<voice-assistant-brand-icon
.voiceAssistantId=${"cloud.google_assistant"}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
<voice-assistant-brand-icon .voiceAssistantId=${"cloud.alexa"}>
<voice-assistant-brand-icon
.voiceAssistantId=${"cloud.alexa"}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
</div>
<h2>
+1
View File
@@ -176,6 +176,7 @@ class OnboardingLocation extends LitElement {
</div>
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._markerLocations(
this._location,
this._places,
+16 -38
View File
@@ -32,7 +32,7 @@ interface AppPanelConfig {
}
// Time to wait for app to start before we ask the user if we should try again
const START_WAIT_TIME = 30000; // ms
const START_WAIT_TIME = 20000; // ms
const RETRY_START_WAIT_TIME = 5000; // ms
@customElement("ha-panel-app")
@@ -140,12 +140,6 @@ class HaPanelApp extends LitElement {
${ref(this._iframeRef)}
>
</iframe>
${!this._iframeLoaded
? html`<hass-loading-screen
class="loading-overlay"
.message=${this._loadingMessage}
></hass-loading-screen>`
: nothing}
`;
}
@@ -290,6 +284,8 @@ class HaPanelApp extends LitElement {
return;
}
this._loadingMessage = undefined;
if (this._fetchDataTimeout) {
clearTimeout(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
@@ -331,32 +327,28 @@ class HaPanelApp extends LitElement {
private async _checkLoaded(ev: Event): Promise<void> {
const iframe = ev.target as HTMLIFrameElement;
this._iframeLoaded = true;
const is502 =
!!this._addon &&
iframe.contentDocument?.body.textContent === "502: Bad Gateway";
if (
!this._addon ||
iframe.contentDocument?.body.textContent !== "502: Bad Gateway"
) {
return;
}
// While the app is still starting, reload the iframe silently behind the
// loading screen instead of revealing the error page and tearing down
// the panel.
if (is502 && this._autoRetryUntil && Date.now() < this._autoRetryUntil) {
// Auto-retry if within the retry window
if (this._autoRetryUntil && Date.now() < this._autoRetryUntil) {
this._reloadIframe();
return;
}
this._iframeLoaded = true;
if (!is502) {
return;
}
// Retry window elapsed, ask the user whether to keep waiting.
// Clear auto-retry window and show dialog
this._autoRetryUntil = undefined;
await this.updateComplete;
showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.app.error_app_not_ready"),
title: this._addon!.name,
title: this._addon.name,
confirmText: this.hass.localize("ui.panel.app.retry"),
dismissText: this.hass.localize("ui.common.no"),
confirm: () => {
@@ -370,7 +362,7 @@ class HaPanelApp extends LitElement {
private async _reloadIframe(): Promise<void> {
const addonSlug = this._addon!.slug;
this._iframeLoaded = false;
this._loadingMessage = this.hass.localize("ui.panel.app.app_starting");
this._addon = undefined;
await Promise.all([
this.updateComplete,
new Promise((resolve) => {
@@ -378,15 +370,7 @@ class HaPanelApp extends LitElement {
}),
]);
// Guard for user navigating away during the delay
if (this._getAddonSlug() !== addonSlug) {
return;
}
// Reload the iframe content in place so the loading screen stays up
// without rebuilding the panel.
const iframeWindow = this._iframeRef.value?.contentWindow;
if (iframeWindow) {
iframeWindow.location.reload();
} else {
if (this._getAddonSlug() === addonSlug) {
this._fetchData(addonSlug);
}
}
@@ -450,12 +434,6 @@ class HaPanelApp extends LitElement {
:host {
display: block;
height: 100%;
position: relative;
}
hass-loading-screen.loading-overlay {
position: absolute;
inset: 0;
}
iframe {
@@ -24,7 +24,10 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -51,7 +54,7 @@ import {
updateAreaRegistryEntry,
} from "../../../data/area/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import { sortDeviceRegistryByName } from "../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -248,7 +251,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
super.updated(changedProps);
if (changedProps.has("areaId")) {
this._findRelated();
fireRelatedContext(this, {
fireEvent(this, "hass-related-context", {
itemType: "area",
itemId: this.areaId,
});
@@ -619,7 +622,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
.time=${this._logbookTime}
.entityIds=${this._allEntities(memberships)}
.deviceIds=${this._allDeviceIds(memberships.devices)}
name-detail="device"
.scope=${"area"}
virtualize
narrow
no-icon
@@ -375,6 +375,7 @@ export class HaAutomationAddSearch extends LitElement {
<state-badge
slot="start"
.stateObj=${(item as EntityComboBoxItem).stateObj}
.hass=${this.hass}
></state-badge>
`
: type === "device" && (item as DevicePickerItem).domain
@@ -779,6 +780,7 @@ export class HaAutomationAddSearch extends LitElement {
return multiTermSortedSearch<PickerComboBoxItem>(
items,
searchTerm,
searchKeys,
(item) => item.id,
fuseIndex
);
@@ -493,6 +493,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@click=${this._showHelp}
></ha-icon-button>
<ha-filter-floor-areas
.hass=${this.hass}
.type=${"automation"}
.value=${this._filters["ha-filter-floor-areas"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -502,6 +503,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-devices
.hass=${this.hass}
.type=${"automation"}
.value=${this._filters["ha-filter-devices"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -511,6 +513,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-entities
.hass=${this.hass}
.type=${"automation"}
.value=${this._filters["ha-filter-entities"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -520,6 +523,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-entities>
<ha-filter-labels
.hass=${this.hass}
.value=${this._filters["ha-filter-labels"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -538,6 +542,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -8,11 +8,12 @@ import type {
import { css, html } from "lit";
import { property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
showAlertDialog,
@@ -166,8 +167,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
}
this._relatedContextAreaId = areaId;
fireRelatedContext(
fireEvent(
this,
"hass-related-context",
areaId
? {
itemType: "area",
@@ -34,7 +34,7 @@ import type {
NodeInfo,
} from "../../../components/trace/hat-script-graph";
import type { AutomationEntity } from "../../../data/automation";
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { LogbookEntry } from "../../../data/logbook";
import { getLogbookDataForContext } from "../../../data/logbook";
@@ -228,6 +228,7 @@ export class HaAutomationTrace extends LitElement {
<div class="main">
<div class="graph">
<hat-script-graph
.hass=${this.hass}
.trace=${this._trace}
.selected=${this._selected?.path}
@graph-node-selected=${this._pickNode}
@@ -319,9 +320,6 @@ export class HaAutomationTrace extends LitElement {
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("triggers");
this.hass.loadBackendTranslation("conditions");
if (!this.automationId) {
return;
}
@@ -377,8 +375,9 @@ export class HaAutomationTrace extends LitElement {
(entry) => entry.entity_id === this._entityId
)?.area_id
: undefined;
fireRelatedContext(
fireEvent(
this,
"hass-related-context",
areaId
? {
itemType: "area",
@@ -86,6 +86,7 @@ export class DialogUploadBackup
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFolderUpload}
.accept=${SUPPORTED_UPLOAD_FORMAT}
@@ -508,6 +508,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
</div>
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
.value=${this._filters[TYPE_FILTER]}
.states=${this._states(this.hass.localize, isHassio)}
@@ -516,6 +517,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
></ha-filter-states>
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize("ui.panel.config.backup.locations")}
.value=${this._filters[LOCATIONS_FILTER]}
.states=${this._locations(
@@ -60,7 +60,7 @@ export class DialogManageCloudhook extends LitElement {
>
<div>
<p>
${cloudhook.managed
${!cloudhook.managed
? html`
${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.managed_by_integration"
@@ -2,7 +2,6 @@ import { mdiDotsVertical } from "@mdi/js";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { isNavigationClick } from "../../../common/dom/is-navigation-click";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
@@ -92,11 +91,7 @@ class PanelDeveloperTools extends LitElement {
panel=${tab.panel}
.active=${page === tab.panel}
>
<a
href="/config/developer-tools/${tab.panel}"
@click=${this._handleTabAnchorClick}
>${this.hass.localize(tab.translationKey)}</a
>
${this.hass.localize(tab.translationKey)}
</ha-tab-group-tab>
`
)}
@@ -110,14 +105,6 @@ class PanelDeveloperTools extends LitElement {
`;
}
private _handleTabAnchorClick(ev: MouseEvent) {
ev.stopPropagation();
const href = isNavigationClick(ev);
if (href) {
navigate(href);
}
}
private _handlePageSelected(ev: CustomEvent<{ name: string }>) {
const newPage = ev.detail.name;
if (!newPage) {
@@ -155,16 +142,6 @@ class PanelDeveloperTools extends LitElement {
--ha-tab-indicator-color: var(--app-header-text-color, white);
--ha-tab-track-color: transparent;
}
ha-tab-group-tab::part(base) {
padding: 0;
}
ha-tab-group-tab a {
color: inherit;
text-decoration: none;
display: flex;
align-items: center;
padding: 1em 1.5em;
}
`;
}
@@ -171,7 +171,10 @@ export class DeveloperYamlConfig extends LitElement {
)}
</div>
<div class="card-actions">
<ha-call-service-button domain="homeassistant" service="reload_all"
<ha-call-service-button
.hass=${this.hass}
domain="homeassistant"
service="reload_all"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.all"
)}
@@ -179,6 +182,7 @@ export class DeveloperYamlConfig extends LitElement {
</div>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="homeassistant"
service="reload_core_config"
>${this.hass.localize(
@@ -190,6 +194,7 @@ export class DeveloperYamlConfig extends LitElement {
(reloadable) => html`
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
.domain=${reloadable.domain}
service="reload"
>${reloadable.name}
@@ -26,7 +26,10 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
@@ -62,7 +65,7 @@ import {
disableConfigEntry,
sortConfigEntries,
} from "../../../data/config_entries";
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import {
removeConfigEntryFromDevice,
@@ -384,7 +387,8 @@ export class HaConfigDevicePage extends LitElement {
super.updated(changedProps);
if (changedProps.has("deviceId")) {
this._findRelated();
fireRelatedContext(this, {
// Broadcast device context for quick bar
fireEvent(this, "hass-related-context", {
itemType: "device",
itemId: this.deviceId,
});
@@ -921,7 +925,7 @@ export class HaConfigDevicePage extends LitElement {
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
name-detail="entity"
.scope=${"device"}
virtualize
narrow
no-icon
@@ -842,6 +842,7 @@ export class HaConfigDeviceDashboard extends LitElement {
</ha-alert>`
: nothing}
<ha-filter-floor-areas
.hass=${this.hass}
type="device"
.value=${this._filters["ha-filter-floor-areas"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -859,6 +860,7 @@ export class HaConfigDeviceDashboard extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-integrations>
<ha-filter-states
.hass=${this.hass}
.value=${this._filters["ha-filter-states"]?.value}
.states=${this._states(this.hass.localize)}
.label=${this.hass.localize("ui.panel.config.devices.picker.state")}
@@ -869,6 +871,7 @@ export class HaConfigDeviceDashboard extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-states>
<ha-filter-labels
.hass=${this.hass}
.value=${this._filters["ha-filter-labels"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -7,8 +7,9 @@ import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
import "../../../../components/input/ha-input";
import "./ha-energy-upstream-device-picker";
import type { HaInput } from "../../../../components/input/ha-input";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
@@ -122,6 +123,27 @@ export class DialogEnergyDeviceSettingsWater
const pickableUnit = this._volume_units?.join(", ") || "";
const includedInDeviceOptions = !this._possibleParents.length
? [
{
value: "-",
disabled: true,
label: this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
),
},
]
: this._possibleParents.map((stat) => ({
value: stat.stat_consumption,
label:
stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
),
}));
return html`
<ha-dialog
.open=${this._open}
@@ -183,23 +205,20 @@ export class DialogEnergyDeviceSettingsWater
>
</ha-input>
<ha-energy-upstream-device-picker
.hass=${this.hass}
<ha-select
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device"
)}
.value=${this._device?.included_in_stat || ""}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device_helper"
)}
.value=${this._device?.included_in_stat}
.possibleParents=${this._possibleParents}
.statsMetadata=${this._params.statsMetadata}
.emptyLabel=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
)}
.disabled=${!this._device}
@value-changed=${this._parentChanged}
></ha-energy-upstream-device-picker>
@selected=${this._parentSelected}
clearable
.options=${includedInDeviceOptions}
>
</ha-select>
<ha-dialog-footer slot="footer">
<ha-button
@@ -274,7 +293,7 @@ export class DialogEnergyDeviceSettingsWater
this._updateDirtyState(this._device);
}
private _parentChanged(ev: ValueChangedEvent<string>) {
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
const newDevice = {
...this._device!,
included_in_stat: ev.detail.value,
@@ -305,7 +324,7 @@ export class DialogEnergyDeviceSettingsWater
width: 100%;
margin-bottom: var(--ha-space-4);
}
ha-energy-upstream-device-picker {
ha-select {
display: block;
margin-top: var(--ha-space-4);
width: 100%;
@@ -7,8 +7,12 @@ import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-select";
import type {
HaSelectOption,
HaSelectSelectEvent,
} from "../../../../components/ha-select";
import "../../../../components/input/ha-input";
import "./ha-energy-upstream-device-picker";
import type { HaInput } from "../../../../components/input/ha-input";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
@@ -119,6 +123,28 @@ export class DialogEnergyDeviceSettings
return nothing;
}
const includedInDeviceOptions: HaSelectOption[] = this._possibleParents
.length
? this._possibleParents.map((stat) => ({
value: stat.stat_consumption,
label:
stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
),
}))
: [
{
value: "-",
disabled: true,
label: this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
),
},
];
return html`
<ha-dialog
.open=${this._open}
@@ -179,23 +205,20 @@ export class DialogEnergyDeviceSettings
>
</ha-input>
<ha-energy-upstream-device-picker
.hass=${this.hass}
<ha-select
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.included_in_device"
)}
.value=${this._device?.included_in_stat || ""}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.included_in_device_helper"
)}
.value=${this._device?.included_in_stat}
.possibleParents=${this._possibleParents}
.statsMetadata=${this._params.statsMetadata}
.emptyLabel=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
)}
.disabled=${!this._device}
@value-changed=${this._parentChanged}
></ha-energy-upstream-device-picker>
@selected=${this._parentSelected}
clearable
.options=${includedInDeviceOptions}
>
</ha-select>
<ha-dialog-footer slot="footer">
<ha-button
@@ -270,7 +293,7 @@ export class DialogEnergyDeviceSettings
this._updateDirtyState(this._device);
}
private _parentChanged(ev: ValueChangedEvent<string>) {
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
const newDevice = {
...this._device!,
included_in_stat: ev.detail.value,
@@ -303,7 +326,7 @@ export class DialogEnergyDeviceSettings
ha-statistic-picker {
width: 100%;
}
ha-energy-upstream-device-picker {
ha-select {
display: block;
margin-top: var(--ha-space-4);
width: 100%;
@@ -1,229 +0,0 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiChartLine, mdiShape } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTL } from "../../../../common/util/compute_rtl";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-combo-box-item";
import "../../../../components/ha-generic-picker";
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
import "../../../../components/ha-svg-icon";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { domainToName } from "../../../../data/integration";
import {
getStatisticLabel,
type StatisticsMetaData,
} from "../../../../data/recorder";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
interface UpstreamDeviceComboBoxItem extends PickerComboBoxItem {
stateObj?: HassEntity;
}
const SEARCH_KEYS = [
{ name: "primary", weight: 10 },
{ name: "search_labels.entityName", weight: 10 },
{ name: "search_labels.friendlyName", weight: 9 },
{ name: "search_labels.deviceName", weight: 8 },
{ name: "search_labels.areaName", weight: 6 },
{ name: "search_labels.domainName", weight: 4 },
{ name: "id", weight: 2 },
];
@customElement("ha-energy-upstream-device-picker")
export class HaEnergyUpstreamDevicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: "empty-label" }) public emptyLabel?: string;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false })
public possibleParents: DeviceConsumptionEnergyPreference[] = [];
@property({ attribute: false })
public statsMetadata?: Record<string, StatisticsMetaData>;
private _computeItem = (statisticId: string): UpstreamDeviceComboBoxItem => {
// Use the energy config's custom display name when the user has set one.
const name = this.possibleParents.find(
(parent) => parent.stat_consumption === statisticId
)?.name;
const stateObj = this.hass.states[statisticId];
if (stateObj) {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const isRTL = computeRTL(
this.hass.language,
this.hass.translationMetadata.translations
);
const friendlyName = computeStateName(stateObj); // Keep this for search
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return {
id: statisticId,
primary: name || entityName || deviceName || statisticId,
secondary,
stateObj,
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
areaName: areaName || null,
friendlyName,
},
};
}
const label = getStatisticLabel(
this.hass,
statisticId,
this.statsMetadata?.[statisticId]
);
const isExternal = statisticId.includes(":") && !statisticId.includes(".");
if (isExternal) {
const domainName = domainToName(
this.hass.localize,
statisticId.split(":")[0]
);
return {
id: statisticId,
primary: name || label,
secondary: domainName,
icon_path: mdiChartLine,
search_labels: { label, domainName },
};
}
return {
id: statisticId,
primary: name || label,
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
icon_path: mdiShape,
search_labels: { label },
};
};
private _items = memoizeOne(
(
_hass: HomeAssistant,
_value: string | undefined,
_possibleParents: DeviceConsumptionEnergyPreference[],
_statsMetadata?: Record<string, StatisticsMetaData>
): UpstreamDeviceComboBoxItem[] => {
const items = this.possibleParents.map((parent) =>
this._computeItem(parent.stat_consumption)
);
// Make sure the current value is selectable even if it is no longer
// part of the possible parents, so it doesn't render as unknown.
if (this.value && !items.some((item) => item.id === this.value)) {
items.unshift(this._computeItem(this.value));
}
return items;
}
);
private _getItems = () =>
this._items(
this.hass,
this.value,
this.possibleParents,
this.statsMetadata
);
private _renderItem = (item: UpstreamDeviceComboBoxItem) => html`
${item.stateObj
? html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`
: item.icon_path
? html`
<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.icon_path}
></ha-svg-icon>
`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
`;
private _rowRenderer: RenderItemFunction<UpstreamDeviceComboBoxItem> = (
item,
index
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${this._renderItem(item)}
</ha-combo-box-item>
`;
private _valueRenderer: PickerValueRenderer = (value) =>
this._renderItem(
this._getItems().find((item) => item.id === value) ??
this._computeItem(value)
);
protected render() {
return html`
<ha-generic-picker
.hass=${this.hass}
use-top-label
.label=${this.label}
.helper=${this.helper}
.value=${this.value}
.disabled=${this.disabled}
.emptyLabel=${this.emptyLabel}
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
.searchKeys=${SEARCH_KEYS}
@value-changed=${this._valueChanged}
></ha-generic-picker>
`;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
this.value = value;
fireEvent(this, "value-changed", { value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-energy-upstream-device-picker": HaEnergyUpstreamDevicePicker;
}
}
@@ -1011,9 +1011,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.voice_assistants_description"
)}
${this.entry.aliases.filter((a) => a !== null).length
? this.entry.aliases
.filter((a): a is string => a !== null)
.join(", ")
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
@@ -985,6 +985,7 @@ export class HaConfigEntities extends LitElement {
</ha-alert>`
: nothing}
<ha-filter-floor-areas
.hass=${this.hass}
type="entity"
.value=${this._filters["ha-filter-floor-areas"]}
@data-table-filter-changed=${this._filterChanged}
@@ -994,6 +995,7 @@ export class HaConfigEntities extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-devices
.hass=${this.hass}
.type=${"entity"}
.value=${this._filters["ha-filter-devices"]}
@data-table-filter-changed=${this._filterChanged}
@@ -1003,6 +1005,7 @@ export class HaConfigEntities extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-domains
.hass=${this.hass}
.value=${this._filters["ha-filter-domains"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -1019,6 +1022,7 @@ export class HaConfigEntities extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-integrations>
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.entities.picker.headers.status"
)}
@@ -1031,6 +1035,7 @@ export class HaConfigEntities extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-states>
<ha-filter-labels
.hass=${this.hass}
.value=${this._filters["ha-filter-labels"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -1039,6 +1044,7 @@ export class HaConfigEntities extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"

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