Compare commits

..

6 Commits

Author SHA1 Message Date
Petar Petrov 7aab3d1baf text tweak 2026-06-22 14:08:22 +03:00
Petar Petrov 6dbf33b9cd Add bottom margin to ha-alert in pending-config dialog
Without it the changes list sits flush against the reverted-state info
alert.
2026-06-22 13:59:09 +03:00
Petar Petrov a6a5eb2050 Show auto-revert countdown in HTTP pending-config dialog
Surface the new revert_at deadline (core PR #174428) in the popup that
opens after saving HTTP server settings. While pending, the dialog shows
a ticking 'Settings will revert in M:SS.' line; when the deadline passes
it switches into a reverted state with an info alert and a Close button.
Confirm/Revert remain available until the deadline.
2026-06-22 13:43:55 +03:00
Petar Petrov 757079983a Merge remote-tracking branch 'origin/dev' into port-8123 2026-06-22 13:32:39 +03:00
Petar Petrov cb65657479 HTTP config: confirm or revert pending changes after restart (#52452)
* HTTP config: prompt admin to confirm or revert pending changes after restart

After saving HTTP server settings, core now writes the new config as pending,
restarts Home Assistant, and keeps the previous stable config as a recovery
fallback. The frontend now:

- Reads {stable, pending} from http/config and adds a promoteHttpConfig call.
- Asks the admin to confirm before saving (since saving now auto-restarts).
- After reconnect, shows a non-dismissable popup for admins with the changed
  fields, where Confirm promotes pending -> stable and Revert clears pending
  (which triggers another restart).
- Renders an info banner above the form whenever pending is set.

* Drop pending banner and pending handling from HTTP form

The popup blocks any other interaction while pending exists, so the form
is only reachable when pending is null. Stop fetching/displaying the
pending config and the unconfirmed-config banner.

* Use HTTP settings in dialog title
2026-06-17 14:35:51 +03:00
Petar Petrov d99526ff60 Add HTTP server settings to the network panel (#51981)
* Fix focus loss in ha-input-multi when items change

* Add HTTP server settings to the network panel

* Surface fetch errors and validate before saving in HTTP config form

* Update src/panels/config/network/ha-config-http-form.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* Group HTTP form fields into collapsible sections

* Add bottom margin to ha-input-multi add button

* Only apply add-button margin when helper text is present

* Update HTTP config form to new WebSocket API

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-06-04 13:06:14 +03:00
150 changed files with 1656 additions and 3258 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
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>
+6 -6
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",
@@ -127,10 +127,10 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "8.0.0",
"@babel/core": "7.29.7",
"@babel/helper-define-polyfill-provider": "1.0.0",
"@babel/plugin-transform-runtime": "8.0.0",
"@babel/preset-env": "8.0.0",
"@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",
@@ -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",
+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 {
+11 -33
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,14 @@ 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 +41,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 +78,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 +95,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);
@@ -188,12 +175,7 @@ export class HaStateLabelBadge extends LitElement {
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
return "—";
}
if (!this._formatters) {
return null;
}
return valueFromParts(
this._formatters.formatEntityStateToParts(entityState)
);
return valueFromParts(this.hass!.formatEntityStateToParts(entityState));
}
private _computeShowIcon(
@@ -228,11 +210,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 +223,8 @@ export class HaStateLabelBadge extends LitElement {
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
if (!this._formatters) {
return null;
}
return (
unitFromParts(this._formatters.formatEntityStateToParts(entityState)) ||
null
unitFromParts(this.hass!.formatEntityStateToParts(entityState)) || 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) {
+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}
+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 "";
+5 -2
View File
@@ -80,7 +80,7 @@ class HaInputMulti extends LitElement {
<div class="items">
${repeat(
this._items,
(item, index) => `${item}-${index}`,
(_item, index) => index,
(item, index) => {
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
return html`
@@ -128,7 +128,7 @@ class HaInputMulti extends LitElement {
)}
</div>
</ha-sortable>
<div class="layout horizontal">
<div class="layout horizontal add-row">
<ha-button
size="s"
appearance="filled"
@@ -217,6 +217,9 @@ class HaInputMulti extends LitElement {
margin-bottom: 8px;
--ha-input-padding-bottom: 0;
}
.add-row:has(+ ha-input-helper-text) {
margin-bottom: var(--ha-space-1);
}
ha-icon-button {
display: block;
}
+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) {
+4
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> = {};
@@ -454,6 +457,7 @@ export class HatScriptGraph extends LitElement {
${node.action
? html`<ha-service-icon
slot="icon"
.hass=${this.hass}
.service=${node.action}
></ha-service-icon>`
: nothing}
+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 -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
);
});
+41
View File
@@ -0,0 +1,41 @@
import type { HomeAssistant } from "../types";
export interface HttpConfig {
server_host?: string[];
server_port?: number;
ssl_certificate?: string;
ssl_peer_certificate?: string;
ssl_key?: string;
cors_allowed_origins?: string[];
use_x_forwarded_for?: boolean;
trusted_proxies?: string[];
use_x_frame_options?: boolean;
ip_ban_enabled?: boolean;
login_attempts_threshold?: number;
ssl_profile?: "modern" | "intermediate";
}
export interface HttpConfigState {
stable: HttpConfig;
pending: HttpConfig | null;
revert_at: string | null;
}
export interface SaveHttpConfigResult {
restart: boolean;
}
export const fetchHttpConfig = (hass: HomeAssistant) =>
hass.callWS<HttpConfigState>({ type: "http/config" });
export const saveHttpConfig = (
hass: HomeAssistant,
config: HttpConfig | null
) =>
hass.callWS<SaveHttpConfigResult>({
type: "http/config/configure",
config,
});
export const promoteHttpConfig = (hass: HomeAssistant) =>
hass.callWS<undefined>({ type: "http/config/promote" });
-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)}
`;
}
@@ -0,0 +1,328 @@
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog";
import type { HttpConfig } from "../../data/http";
import { promoteHttpConfig, saveHttpConfig } from "../../data/http";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { HttpPendingConfigDialogParams } from "./show-dialog-http-pending-config";
const HTTP_FIELDS: (keyof HttpConfig)[] = [
"server_port",
"server_host",
"ssl_certificate",
"ssl_key",
"ssl_peer_certificate",
"ssl_profile",
"cors_allowed_origins",
"use_x_forwarded_for",
"trusted_proxies",
"use_x_frame_options",
"ip_ban_enabled",
"login_attempts_threshold",
];
@customElement("dialog-http-pending-config")
export class DialogHttpPendingConfig
extends LitElement
implements HassDialog<HttpPendingConfigDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: HttpPendingConfigDialogParams;
@state() private _open = false;
@state() private _busy: "confirm" | "revert" | undefined;
@state() private _error?: string;
@state() private _secondsRemaining?: number;
@state() private _reverted = false;
private _interval?: number;
public showDialog(params: HttpPendingConfigDialogParams): void {
this._params = params;
this._open = true;
this._busy = undefined;
this._error = undefined;
this._reverted = false;
this._startCountdown();
}
public closeDialog(): boolean {
this._open = false;
this._stopCountdown();
return true;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._stopCountdown();
}
private _dialogClosed(): void {
this._params = undefined;
this._busy = undefined;
this._error = undefined;
this._reverted = false;
this._secondsRemaining = undefined;
this._stopCountdown();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _startCountdown(): void {
this._stopCountdown();
const revertAt = this._params?.state.revert_at;
if (!revertAt) {
this._secondsRemaining = undefined;
return;
}
const target = new Date(revertAt).getTime();
const tick = () => {
const remaining = Math.max(0, Math.ceil((target - Date.now()) / 1000));
this._secondsRemaining = remaining;
if (remaining === 0) {
this._stopCountdown();
this._reverted = true;
}
};
tick();
if (this._secondsRemaining && this._secondsRemaining > 0) {
this._interval = window.setInterval(tick, 1000);
}
}
private _stopCountdown(): void {
if (this._interval) {
window.clearInterval(this._interval);
this._interval = undefined;
}
}
private get _changedFields(): (keyof HttpConfig)[] {
if (!this._params?.state.pending) {
return [];
}
const { stable, pending } = this._params.state;
return HTTP_FIELDS.filter(
(key) => JSON.stringify(stable[key]) !== JSON.stringify(pending[key])
);
}
protected render() {
if (!this._params) {
return nothing;
}
const changes = this._changedFields;
return html`
<ha-dialog
.open=${this._open}
.headerTitle=${this.hass.localize(
"ui.dialogs.http_pending_config.title"
)}
prevent-scrim-close
width="medium"
@closed=${this._dialogClosed}
>
<span slot="headerNavigationIcon"></span>
<div class="content">
${this._reverted
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.dialogs.http_pending_config.reverted"
)}
</ha-alert>
`
: html`
<p>
${this.hass.localize(
"ui.dialogs.http_pending_config.description"
)}
</p>
${this._secondsRemaining !== undefined
? html`
<p class="countdown">
${this.hass.localize(
"ui.dialogs.http_pending_config.auto_revert",
{
time:
secondsToDuration(this._secondsRemaining) ?? "0",
}
)}
</p>
`
: nothing}
`}
${changes.length
? html`
<p class="changes-label">
${this.hass.localize(
"ui.dialogs.http_pending_config.changes_label"
)}
</p>
<ul>
${changes.map(
(key) => html`
<li>
${this.hass.localize(
`ui.panel.config.network.http.fields.${key}` as any
)}
</li>
`
)}
</ul>
`
: nothing}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
</div>
<ha-dialog-footer slot="footer">
${this._reverted
? html`
<ha-button slot="primaryAction" @click=${this._close}>
${this.hass.localize("ui.dialogs.http_pending_config.close")}
</ha-button>
`
: html`
<ha-button
slot="secondaryAction"
appearance="plain"
.loading=${this._busy === "revert"}
.disabled=${this._busy === "confirm"}
@click=${this._revert}
>
${this.hass.localize("ui.dialogs.http_pending_config.revert")}
</ha-button>
<ha-button
slot="primaryAction"
.loading=${this._busy === "confirm"}
.disabled=${this._busy === "revert"}
@click=${this._confirm}
>
${this.hass.localize(
"ui.dialogs.http_pending_config.confirm"
)}
</ha-button>
`}
</ha-dialog-footer>
</ha-dialog>
`;
}
private async _confirm(): Promise<void> {
if (this._busy) {
return;
}
this._busy = "confirm";
this._error = undefined;
try {
await promoteHttpConfig(this.hass);
this._notifyResolved();
this._open = false;
} catch (err: any) {
// A confirm fired right as the backend auto-reverts may race the
// restart and surface as a connection-lost error. Treat it as resolved;
// the disconnected overlay takes over.
if (err?.error?.code === ERR_CONNECTION_LOST) {
this._notifyResolved();
this._open = false;
return;
}
this._error = this.hass.localize(
"ui.dialogs.http_pending_config.confirm_error",
{ error: err.message ?? "" }
);
this._busy = undefined;
}
}
private _close(): void {
this._notifyResolved();
this._open = false;
}
private async _revert(): Promise<void> {
if (this._busy || !this._params) {
return;
}
this._busy = "revert";
this._error = undefined;
try {
await saveHttpConfig(this.hass, null);
this._notifyResolved();
this._open = false;
} catch (err: any) {
// The restart triggered by clearing pending may cut the WS connection
// before we get a reply. The disconnected overlay takes over from here.
if (err?.error?.code === ERR_CONNECTION_LOST) {
this._notifyResolved();
this._open = false;
return;
}
this._error = this.hass.localize(
"ui.dialogs.http_pending_config.revert_error",
{ error: err.message ?? "" }
);
this._busy = undefined;
}
}
private _notifyResolved(): void {
this._params?.onResolved?.();
// The form on Settings > System > Network may be mounted and showing
// stale state; let it know to refetch.
window.dispatchEvent(new Event("http-config-resolved"));
}
static styles = [
haStyleDialog,
css`
.content {
line-height: var(--ha-line-height-normal);
}
p {
margin: 0 0 var(--ha-space-4) 0;
}
.countdown {
color: var(--secondary-text-color);
}
.changes-label {
font-weight: var(--ha-font-weight-medium);
margin-bottom: var(--ha-space-2);
}
ul {
margin: 0 0 var(--ha-space-4) 0;
padding-left: var(--ha-space-6);
color: var(--secondary-text-color);
}
li {
margin-bottom: var(--ha-space-1);
}
ha-alert {
display: block;
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-4);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-http-pending-config": DialogHttpPendingConfig;
}
}
@@ -0,0 +1,22 @@
import { fireEvent } from "../../common/dom/fire_event";
import type { HttpConfigState } from "../../data/http";
export interface HttpPendingConfigDialogParams {
state: HttpConfigState;
onResolved?: () => void;
}
export const loadHttpPendingConfigDialog = () =>
import("./dialog-http-pending-config");
export const showHttpPendingConfigDialog = (
element: HTMLElement,
dialogParams: HttpPendingConfigDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-http-pending-config",
dialogImport: loadHttpPendingConfigDialog,
dialogParams,
addHistory: false,
});
};
+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>
+42 -1
View File
@@ -5,9 +5,12 @@ import { customElement, state } from "lit/decorators";
import { storage } from "../common/decorators/storage";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { navigate } from "../common/navigate";
import { fetchHttpConfig } from "../data/http";
import type { HttpConfigState } from "../data/http";
import type { WindowWithPreloads } from "../data/preloads";
import type { RecorderInfo } from "../data/recorder";
import { getRecorderInfo } from "../data/recorder";
import { showHttpPendingConfigDialog } from "../dialogs/http-pending-config/show-dialog-http-pending-config";
import "../resources/custom-card-support";
import { HassElement } from "../state/hass-element";
import QuickBarMixin from "../state/quick-bar-mixin";
@@ -39,6 +42,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
@state() private _databaseMigration?: boolean;
private _httpPendingDialogOpen = false;
private _panelUrl: string;
@storage({ key: "ha-version", state: false, subscribe: false })
@@ -70,14 +75,24 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
protected willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this._databaseMigration === undefined &&
changedProps.has("hass") &&
this.hass?.config &&
changedProps.get("hass")?.config !== this.hass?.config
oldHass?.config !== this.hass.config
) {
this.checkDataBaseMigration();
}
// Wait for `hass.user` to populate so the admin guard can run; it arrives
// asynchronously after `hass.config`.
if (
changedProps.has("hass") &&
this.hass?.user &&
oldHass?.user !== this.hass.user
) {
this.checkHttpPendingConfig();
}
}
protected update(changedProps: PropertyValues<this>) {
@@ -208,6 +223,32 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
}
}
protected async checkHttpPendingConfig() {
if (__DEMO__ || this._httpPendingDialogOpen) {
return;
}
if (!this.hass?.user?.is_admin) {
return;
}
let httpConfig: HttpConfigState;
try {
httpConfig = await fetchHttpConfig(this.hass);
} catch (_err) {
// The check re-runs on the next reconnect; ignore transient failures.
return;
}
if (!httpConfig.pending || this._httpPendingDialogOpen) {
return;
}
this._httpPendingDialogOpen = true;
showHttpPendingConfigDialog(this, {
state: httpConfig,
onResolved: () => {
this._httpPendingDialogOpen = false;
},
});
}
protected async checkDataBaseMigration() {
if (__DEMO__) {
this._databaseMigration = false;
+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,
@@ -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"
@@ -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}
@@ -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}
@@ -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;
}
}
@@ -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"
@@ -669,6 +669,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
class=${this.narrow ? "narrow" : ""}
>
<ha-filter-floor-areas
.hass=${this.hass}
.type=${"entity"}
.value=${this._filters["ha-filter-floor-areas"]}
@data-table-filter-changed=${this._filterChanged}
@@ -678,6 +679,7 @@ export class HaConfigHelpers extends SubscribeMixin(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}
@@ -687,6 +689,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-labels
.hass=${this.hass}
.value=${this._filters["ha-filter-labels"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -705,6 +708,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -109,6 +109,7 @@ export class ZHAClusterAttributes extends LitElement {
</div>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="zha"
service="set_zigbee_cluster_attribute"
.data=${this._setAttributeServiceData}
@@ -96,6 +96,7 @@ export class ZHAClusterCommands extends LitElement {
</div>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="zha"
service="issue_zigbee_cluster_command"
.data=${this._issueClusterCommandServiceData}
@@ -91,6 +91,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
<state-badge
@click=${this._openMoreInfo}
.title=${entity.stateName!}
.hass=${this.hass}
.stateObj=${this.hass!.states[entity.entity_id]}
slot="item-icon"
></state-badge>
@@ -100,8 +100,6 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _lowSecurityReason?: number;
@state() private _interviewProgress?: number;
@state() private _device?: ZWaveJSAddNodeDevice;
@state() private _deviceOptions?: ZWaveJSAddNodeSmartStartOptions;
@@ -252,6 +250,7 @@ class DialogZWaveJSAddNode extends LitElement {
return html`
<div>
<ha-qr-scanner
.hass=${this.hass}
@qr-code-scanned=${this._qrCodeScanned}
@qr-code-closed=${this.closeDialog}
@qr-code-more-options=${this._qrScanShowMoreOptions}
@@ -340,10 +339,6 @@ class DialogZWaveJSAddNode extends LitElement {
) {
return html`
<zwave-js-add-node-loading
.hass=${this.hass}
.progress=${this._step === "interviewing"
? this._interviewProgress
: undefined}
.description=${this.hass.localize(
`ui.panel.config.zwave_js.add_node.${this._step !== "rename_device" ? "getting_device_information" : "saving_device"}`
)}
@@ -384,7 +379,6 @@ class DialogZWaveJSAddNode extends LitElement {
}
return html`<zwave-js-add-node-loading
.hass=${this.hass}
.delay=${1000}
></zwave-js-add-node-loading>`;
}
@@ -709,13 +703,9 @@ class DialogZWaveJSAddNode extends LitElement {
break;
case "node added":
this._step = "interviewing";
this._interviewProgress = undefined;
this._lowSecurity = message.node.low_security;
this._lowSecurityReason = message.node.low_security_reason;
break;
case "interview progress":
this._interviewProgress = message.progress;
break;
case "interview completed":
this._unsubscribeAddZwaveNode();
this._step = "configure_device";
@@ -1091,7 +1081,6 @@ class DialogZWaveJSAddNode extends LitElement {
this._dskPin = "";
this._lowSecurity = false;
this._lowSecurityReason = undefined;
this._interviewProgress = undefined;
this._inclusionStrategy = undefined;
if (this._addNodeTimeoutHandle) {
@@ -1,36 +1,21 @@
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { blankBeforePercent } from "../../../../../../common/translations/blank_before_percent";
import "../../../../../../components/animation/ha-fade-in";
import "../../../../../../components/ha-spinner";
import "../../../../../../components/progress/ha-progress-ring";
import { WakeLockMixin } from "../../../../../../mixins/wakelock-mixin";
import type { HomeAssistant } from "../../../../../../types";
@customElement("zwave-js-add-node-loading")
export class ZWaveJsAddNodeLoading extends WakeLockMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public description?: string;
@property({ type: Number }) public progress?: number;
@property({ type: Number }) public delay = 0;
render() {
return html`
<ha-fade-in .delay=${this.delay}>
<div class="loading">
${this.progress !== undefined
? html`
<ha-progress-ring size="large" .value=${this.progress}>
${Math.round(this.progress)}${blankBeforePercent(
this.hass.locale
)}%
</ha-progress-ring>
`
: html`<ha-spinner size="large"></ha-spinner>`}
<ha-spinner size="large"></ha-spinner>
</div>
${this.description ? html`<p>${this.description}</p>` : nothing}
</ha-fade-in>
@@ -4,12 +4,10 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { blankBeforePercent } from "../../../../../common/translations/blank_before_percent";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-dialog";
import "../../../../../components/progress/ha-progress-ring";
import { reinterviewZwaveNode } from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@@ -23,12 +21,8 @@ class DialogZWaveJSReinterviewNode extends LitElement {
@state() private _status?: string;
// Completed interview stages for rendering checklists
// Can be removed once min schema is bumped to 50
@state() private _stages?: string[];
@state() private _progress?: number;
@state() private _open = false;
private _subscribed?: Promise<UnsubscribeFunc>;
@@ -37,7 +31,6 @@ class DialogZWaveJSReinterviewNode extends LitElement {
params: ZWaveJSReinterviewNodeDialogParams
): Promise<void> {
this._stages = undefined;
this._progress = undefined;
this.device_id = params.device_id;
this._open = true;
}
@@ -74,21 +67,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
${this._status === "started"
? html`
<div class="flex-container">
${this._progress !== undefined
? html`
<ha-progress-ring
size="large"
.value=${this._progress}
aria-label=${this.hass.localize(
"ui.panel.config.zwave_js.reinterview_node.in_progress"
)}
>
${Math.round(this._progress)}${blankBeforePercent(
this.hass.locale
)}%
</ha-progress-ring>
`
: html`<ha-spinner></ha-spinner>`}
<ha-spinner></ha-spinner>
<div class="status">
<p>
<b>
@@ -140,7 +119,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div>
`
: ``}
${this._progress === undefined && this._stages
${this._stages
? html`
<div class="stages">
${this._stages.map(
@@ -194,16 +173,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
if (message.event === "interview started") {
this._status = "started";
}
if (message.event === "interview progress") {
this._status = "started";
this._progress = message.progress;
}
// If upstream supports granular progress reporting,
// ignore the legacy per-stage events that drive the stage checklist.
if (
message.event === "interview stage completed" &&
this._progress === undefined
) {
if (message.event === "interview stage completed") {
if (this._stages === undefined) {
this._stages = [message.stage];
} else {
@@ -235,7 +205,6 @@ class DialogZWaveJSReinterviewNode extends LitElement {
this.device_id = undefined;
this._status = undefined;
this._stages = undefined;
this._progress = undefined;
this._unsubscribe();
@@ -277,7 +246,6 @@ class DialogZWaveJSReinterviewNode extends LitElement {
}
.flex-container ha-spinner,
.flex-container ha-progress-ring,
.flex-container ha-svg-icon {
margin-right: 20px;
margin-inline-end: 20px;
@@ -130,6 +130,7 @@ class DialogZWaveJSUpdateFirmwareNode extends DirtyStateProviderMixin<FirmwareFo
}
const beginFirmwareUpdateHTML = html`<ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFileUpload}
.label=${this.hass.localize(
+4 -1
View File
@@ -190,7 +190,10 @@ export class SystemLogCard extends LitElement {
>`}
<div class="card-actions">
<ha-call-service-button domain="system_log" service="clear"
<ha-call-service-button
.hass=${this.hass}
domain="system_log"
service="clear"
>${this.hass.localize(
"ui.panel.config.logs.clear"
)}</ha-call-service-button
@@ -0,0 +1,361 @@
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
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 type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-form/ha-form";
import type { HaForm } from "../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../components/ha-form/types";
import { fetchHttpConfig, saveHttpConfig } from "../../../data/http";
import type { HttpConfig } from "../../../data/http";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
const SCHEMA = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "server_port",
required: true,
selector: { number: { min: 1, max: 65535, mode: "box" } },
},
{
name: "server_host",
selector: { text: { multiple: true } },
},
{
name: "ssl",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.ssl"),
schema: [
{
name: "ssl_certificate",
selector: { text: {} },
},
{
name: "ssl_key",
selector: { text: {} },
},
{
name: "ssl_peer_certificate",
selector: { text: {} },
},
{
name: "ssl_profile",
selector: {
select: {
options: [
{
value: "modern",
label: localize(
"ui.panel.config.network.http.ssl_profile_modern"
),
},
{
value: "intermediate",
label: localize(
"ui.panel.config.network.http.ssl_profile_intermediate"
),
},
],
},
},
},
],
},
{
name: "reverse_proxy",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.reverse_proxy"),
schema: [
{
name: "use_x_forwarded_for",
selector: { boolean: {} },
},
{
name: "trusted_proxies",
selector: { text: { multiple: true } },
},
],
},
{
name: "ip_banning",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.ip_banning"),
schema: [
{
name: "ip_ban_enabled",
selector: { boolean: {} },
},
{
name: "login_attempts_threshold",
required: true,
selector: { number: { min: -1, max: 1000, mode: "box" } },
},
],
},
{
name: "advanced",
type: "expandable",
flatten: true,
title: localize("ui.panel.config.network.http.sections.advanced"),
schema: [
{
name: "cors_allowed_origins",
selector: { text: { multiple: true } },
},
{
name: "use_x_frame_options",
selector: { boolean: {} },
},
],
},
] as const
);
@customElement("ha-config-http-form")
class HaConfigHttpForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _stable?: HttpConfig;
@state() private _config?: HttpConfig;
@state() private _error?: string;
@state() private _fieldErrors: Record<string, string> = {};
@state() private _saving = false;
@state() private _showNoChanges = false;
@query("ha-form") private _form?: HaForm;
@query("ha-alert") private _firstAlert?: HTMLElement;
private _onConfigResolved = () => this._fetchConfig();
protected override firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._fetchConfig();
}
public override connectedCallback() {
super.connectedCallback();
window.addEventListener("http-config-resolved", this._onConfigResolved);
}
public override disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("http-config-resolved", this._onConfigResolved);
}
protected render() {
if (!this._stable && !this._error) {
return nothing;
}
const schema = SCHEMA(this.hass.localize);
return html`
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.network.http.caption")}
>
<div class="card-content">
<p class="description">
${this.hass.localize("ui.panel.config.network.http.description")}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
${this._showNoChanges
? html`
<ha-alert alert-type="success">
${this.hass.localize(
"ui.panel.config.network.http.save_no_changes"
)}
</ha-alert>
`
: nothing}
${this._config
? html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.error=${this._fieldErrors}
.disabled=${this._saving}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
`
: nothing}
</div>
${this._config
? html`
<div class="card-actions">
<ha-button
@click=${this._save}
.disabled=${this._saving}
.loading=${this._saving}
>
${this.hass.localize("ui.panel.config.network.http.save")}
</ha-button>
</div>
`
: nothing}
</ha-card>
`;
}
private async _fetchConfig(): Promise<void> {
try {
// Pending is exclusively handled by the global confirm/revert dialog, so
// the form only ever displays stable.
const { stable } = await fetchHttpConfig(this.hass);
this._stable = stable;
this._config = { ...stable };
} catch (err: any) {
this._error = err.message;
}
}
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string => {
if ("type" in schema && schema.type === "expandable") {
// Expandable sections render their own title; never label them.
return "";
}
return this.hass.localize(
`ui.panel.config.network.http.fields.${schema.name}` as any
);
};
private _computeHelper = (
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string => {
if ("type" in schema && schema.type === "expandable") {
return "";
}
return (
this.hass.localize(
`ui.panel.config.network.http.helpers.${schema.name}` as any
) || ""
);
};
private _valueChanged(ev: CustomEvent): void {
this._config = ev.detail.value;
this._error = undefined;
this._fieldErrors = {};
this._showNoChanges = false;
}
private async _save(): Promise<void> {
if (!this._config || !this._stable) {
return;
}
if (this._form && !this._form.reportValidity()) {
return;
}
if (JSON.stringify(this._stable) === JSON.stringify(this._config)) {
this._showNoChanges = true;
return;
}
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.network.http.save_confirm.title"
),
text: this.hass.localize(
"ui.panel.config.network.http.save_confirm.text"
),
confirmText: this.hass.localize(
"ui.panel.config.network.http.save_confirm.confirm"
),
});
if (!confirmed) {
return;
}
this._saving = true;
this._error = undefined;
this._fieldErrors = {};
this._showNoChanges = false;
try {
const result = await saveHttpConfig(this.hass, this._config);
if (!result.restart) {
this._showNoChanges = true;
}
// restart === true: a restart is in flight. The reply usually races with
// the connection drop; if we do reach this branch, the disconnected
// overlay will appear in moments. Leave the form as is.
} catch (err: any) {
// The restart kills the WS connection before the ack — that's expected.
if (
err?.error?.code === ERR_CONNECTION_LOST ||
err === ERR_CONNECTION_LOST
) {
return;
}
// voluptuous formats errors as "<message> @ data['<field>']".
// If a field is identified, mark it inline; otherwise show a card-level
// alert.
const fieldMatch = err.message?.match(/\bdata\['([^']+)'\]/);
if (fieldMatch) {
this._fieldErrors = { [fieldMatch[1]]: err.message };
} else {
this._error = err.message;
}
} finally {
this._saving = false;
}
await this.updateComplete;
await this._form?.updateComplete;
// Inline field errors render inside ha-form's shadow root, so fall back to
// it when no top-level alert is present.
const target =
this._firstAlert ??
this._form?.shadowRoot?.querySelector<HTMLElement>("ha-alert");
target?.scrollIntoView({ behavior: "smooth", block: "center" });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.description {
margin-top: 0;
color: var(--secondary-text-color);
}
ha-alert {
display: block;
margin-bottom: var(--ha-space-4);
}
.card-actions {
display: flex;
gap: var(--ha-space-2);
justify-content: flex-end;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-http-form": HaConfigHttpForm;
}
}
@@ -8,6 +8,7 @@ import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-icon-next";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-http-form";
import "./ha-config-network";
import "./ha-config-url-form";
import "./supervisor-hostname";
@@ -40,6 +41,7 @@ class HaConfigSectionNetwork extends LitElement {
<supervisor-network .hass=${this.hass}></supervisor-network>`
: ""}
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
<ha-config-http-form .hass=${this.hass}></ha-config-http-form>
<ha-config-network .hass=${this.hass}></ha-config-network>
${NETWORK_BROWSERS.some((component) =>
isComponentLoaded(this.hass.config, component)
@@ -88,6 +90,7 @@ class HaConfigSectionNetwork extends LitElement {
supervisor-hostname,
supervisor-network,
ha-config-url-form,
ha-config-http-form,
ha-config-network,
.discovery-card {
display: block;
@@ -512,6 +512,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-icon-button>
<ha-filter-floor-areas
.hass=${this.hass}
.type=${"scene"}
.value=${this._filters["ha-filter-floor-areas"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -521,6 +522,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-devices
.hass=${this.hass}
.type=${"scene"}
.value=${this._filters["ha-filter-devices"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -530,6 +532,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-entities
.hass=${this.hass}
.type=${"scene"}
.value=${this._filters["ha-filter-entities"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -539,6 +542,7 @@ class HaSceneDashboard 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"
@@ -557,6 +561,7 @@ class HaSceneDashboard 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"
@@ -454,6 +454,7 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
${this._mode === "live"
? html`
<state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>
@@ -544,6 +545,7 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
>
${this._mode === "live"
? html` <state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>`
@@ -483,6 +483,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@click=${this._showHelp}
></ha-icon-button>
<ha-filter-floor-areas
.hass=${this.hass}
.type=${"script"}
.value=${this._filters["ha-filter-floor-areas"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -492,6 +493,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-devices
.hass=${this.hass}
.type=${"script"}
.value=${this._filters["ha-filter-devices"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -501,6 +503,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-entities
.hass=${this.hass}
.type=${"script"}
.value=${this._filters["ha-filter-entities"]?.value}
@data-table-filter-changed=${this._filterChanged}
@@ -510,6 +513,7 @@ class HaScriptPicker 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"
@@ -528,6 +532,7 @@ class HaScriptPicker 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"
@@ -210,6 +210,7 @@ export class HaScriptTrace 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}
@@ -107,7 +107,10 @@ export class AssistPref extends LitElement {
return html`
<ha-card outlined>
<h1 class="card-header">
<voice-assistant-brand-icon .voiceAssistantId=${"conversation"}>
<voice-assistant-brand-icon
.voiceAssistantId=${"conversation"}
.hass=${this.hass}
>
</voice-assistant-brand-icon
>Assist
</h1>
@@ -64,7 +64,10 @@ export class CloudAlexaPref extends LitElement {
return html`
<ha-card outlined>
<h1 class="card-header">
<voice-assistant-brand-icon .voiceAssistantId=${"cloud.alexa"}>
<voice-assistant-brand-icon
.voiceAssistantId=${"cloud.alexa"}
.hass=${this.hass}
>
</voice-assistant-brand-icon
>${this.hass.localize("ui.panel.config.cloud.account.alexa.title")}
</h1>
@@ -49,9 +49,13 @@ export class CloudDiscover 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>
@@ -72,6 +72,7 @@ export class CloudGooglePref extends LitElement {
<h1 class="card-header">
<voice-assistant-brand-icon
.voiceAssistantId=${"cloud.google_assistant"}
.hass=${this.hass}
>
</voice-assistant-brand-icon
>${this.hass.localize("ui.panel.config.cloud.account.google.title")}
@@ -217,6 +217,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
<voice-assistant-brand-icon
slot="start"
.voiceAssistantId=${key}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
<span slot="headline">${voiceAssistants[key].name}</span>
@@ -32,6 +32,7 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
filter: this.manual ? "grayscale(100%)" : undefined,
})}
.voiceAssistantId=${this.assistant}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
${this.unsupported
+1
View File
@@ -255,6 +255,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
? html`
<div class="flex">
<ha-locations-editor
.hass=${this.hass}
.locations=${this._getZones(
this._storageItems,
this._stateItems
@@ -33,7 +33,11 @@ export class HomeFavoriteEntityListItem extends LitElement {
return html`
<ha-settings-row slim>
<state-badge slot="prefix" .stateObj=${stateObj}></state-badge>
<state-badge
slot="prefix"
.hass=${this.hass}
.stateObj=${stateObj}
></state-badge>
<span slot="heading">${primary}</span>
${secondary
? html`<span slot="description">${secondary}</span>`
+1
View File
@@ -548,6 +548,7 @@ class HaLogbookEntry extends LitElement {
></ha-state-icon>`;
}
return html`<state-badge
.hass=${this.hass}
.overrideIcon=${glyph.icon}
.overrideImage=${this._brandImage(glyph.domain)}
.stateColor=${false}

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