t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
- )
+ this._triggerIds = config?.triggers
+ ? getTriggersIds(ensureArray(config.triggers))
: [];
}
@@ -106,12 +111,12 @@ export class HaTriggerCondition extends LitElement {
const newValue = ev.detail.value;
if (typeof newValue.id === "string") {
- if (!this._triggers.some((trigger) => trigger.id === newValue.id)) {
+ if (!this._triggerIds.some((id) => id === newValue.id)) {
newValue.id = "";
}
} else if (Array.isArray(newValue.id)) {
- newValue.id = newValue.id.filter((id) =>
- this._triggers.some((trigger) => trigger.id === id)
+ newValue.id = newValue.id.filter((_id) =>
+ this._triggerIds.some((id) => id === _id)
);
if (!newValue.id.length) {
newValue.id = "";
diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts
index 23855571d1..741ecb99d2 100644
--- a/src/panels/config/automation/manual-automation-editor.ts
+++ b/src/panels/config/automation/manual-automation-editor.ts
@@ -78,7 +78,7 @@ export class HaManualAutomationEditor extends LitElement {
>
- ${!ensureArray(this.config.trigger)?.length
+ ${!ensureArray(this.config.triggers)?.length
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.description"
diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
index 31e996fab0..174233692e 100644
--- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
+++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
@@ -29,6 +29,7 @@ import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
+import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
@@ -50,7 +51,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
-import { TRIGGER_ICONS } from "../../../../data/trigger";
+import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
import {
showAlertDialog,
showConfirmationDialog,
@@ -64,6 +65,7 @@ import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
+import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
@@ -75,7 +77,6 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
-import { preventDefault } from "../../../../common/dom/prevent_default";
export interface TriggerElement extends LitElement {
trigger: Trigger;
@@ -87,7 +88,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
if (!name) {
return;
}
- const newVal = (ev.target as any)?.value;
+ const newVal = ev.detail?.value || (ev.currentTarget as any)?.value;
if ((element.trigger[name] || "") === newVal) {
return;
@@ -146,15 +147,17 @@ export default class HaAutomationTriggerRow extends LitElement {
protected render() {
if (!this.trigger) return nothing;
+ const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
+
const supported =
- customElements.get(`ha-automation-trigger-${this.trigger.trigger}`) !==
- undefined;
+ customElements.get(`ha-automation-trigger-${type}`) !== undefined;
+
const yamlMode = this._yamlMode || !supported;
const showId = "id" in this.trigger || this._requestShowId;
return html`
- ${this.trigger.enabled === false
+ ${"enabled" in this.trigger && this.trigger.enabled === false
? html`
${this.hass.localize(
@@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {
${describeTrigger(this.trigger, this.hass, this._entityReg)}
@@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement {
.path=${mdiDotsVertical}
>
-
+
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
-
+
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
@@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
-
- ${this.trigger.enabled === false
+
+ ${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
@@ -284,7 +296,8 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
@@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
${this._warnings
@@ -336,7 +350,7 @@ export default class HaAutomationTriggerRow extends LitElement {
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
- { platform: this.trigger.trigger }
+ { platform: type }
)}
`
: ""}
@@ -348,7 +362,7 @@ export default class HaAutomationTriggerRow extends LitElement {
>
`
: html`
- ${showId
+ ${showId && !isTriggerList(this.trigger)
? html`
- ${dynamicElement(
- `ha-automation-trigger-${this.trigger.trigger}`,
- {
- hass: this.hass,
- trigger: this.trigger,
- disabled: this.disabled,
- path: this.path,
- }
- )}
+ ${dynamicElement(`ha-automation-trigger-${type}`, {
+ hass: this.hass,
+ trigger: this.trigger,
+ disabled: this.disabled,
+ path: this.path,
+ })}
`}
@@ -546,6 +557,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _onDisable() {
+ if (isTriggerList(this.trigger)) return;
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
@@ -555,7 +567,9 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _idChanged(ev: CustomEvent) {
+ if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
+
if (newId === (this.trigger.id ?? "")) {
return;
}
@@ -583,6 +597,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _onUiChanged(ev: CustomEvent) {
+ if (isTriggerList(this.trigger)) return;
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
@@ -617,6 +632,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private async _renameTrigger(): Promise {
+ if (isTriggerList(this.trigger)) return;
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.change_alias"
diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts
index 39da68e029..5fe2a100a1 100644
--- a/src/panels/config/automation/trigger/ha-automation-trigger.ts
+++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts
@@ -18,7 +18,11 @@ import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
-import { AutomationClipboard, Trigger } from "../../../../data/automation";
+import {
+ AutomationClipboard,
+ Trigger,
+ TriggerList,
+} from "../../../../data/automation";
import { HomeAssistant, ItemPath } from "../../../../types";
import {
PASTE_VALUE,
@@ -26,6 +30,7 @@ import {
} from "../show-add-automation-element-dialog";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
+import { isTriggerList } from "../../../../data/trigger";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@@ -130,7 +135,11 @@ export default class HaAutomationTrigger extends LitElement {
showAddAutomationElementDialog(this, {
type: "trigger",
add: this._addTrigger,
- clipboardItem: this._clipboard?.trigger?.trigger,
+ clipboardItem: !this._clipboard?.trigger
+ ? undefined
+ : isTriggerList(this._clipboard.trigger)
+ ? "list"
+ : this._clipboard?.trigger?.trigger,
});
}
@@ -139,7 +148,7 @@ export default class HaAutomationTrigger extends LitElement {
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else {
- const trigger = value as Trigger["trigger"];
+ const trigger = value as Exclude["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {
diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts
new file mode 100644
index 0000000000..8a74813908
--- /dev/null
+++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts
@@ -0,0 +1,54 @@
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators";
+import { ensureArray } from "../../../../../common/array/ensure-array";
+import type { TriggerList } from "../../../../../data/automation";
+import type { HomeAssistant, ItemPath } from "../../../../../types";
+import "../ha-automation-trigger";
+import {
+ handleChangeEvent,
+ TriggerElement,
+} from "../ha-automation-trigger-row";
+
+@customElement("ha-automation-trigger-list")
+export class HaTriggerList extends LitElement implements TriggerElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public trigger!: TriggerList;
+
+ @property({ attribute: false }) public path?: ItemPath;
+
+ @property({ type: Boolean }) public disabled = false;
+
+ public static get defaultConfig(): TriggerList {
+ return {
+ triggers: [],
+ };
+ }
+
+ protected render() {
+ const triggers = ensureArray(this.trigger.triggers);
+
+ return html`
+
+ `;
+ }
+
+ private _valueChanged(ev: CustomEvent): void {
+ handleChangeEvent(this, ev);
+ }
+
+ static styles = css``;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-automation-trigger-list": HaTriggerList;
+ }
+}
diff --git a/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts b/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts
index b5933a0fdf..229059bcf8 100644
--- a/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts
+++ b/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts
@@ -99,24 +99,32 @@ export class CloudForgotPassword extends LitElement {
this._requestInProgress = true;
- try {
- await cloudForgotPassword(this.hass, email);
- // @ts-ignore
- fireEvent(this, "email-changed", { value: email });
- this._requestInProgress = false;
- // @ts-ignore
- fireEvent(this, "cloud-done", {
- flashMessage: this.hass.localize(
- "ui.panel.config.cloud.forgot_password.check_your_email"
- ),
- });
- } catch (err: any) {
- this._requestInProgress = false;
- this._error =
- err && err.body && err.body.message
- ? err.body.message
- : "Unknown error";
- }
+ const doResetPassword = async (username: string) => {
+ try {
+ await cloudForgotPassword(this.hass, username);
+ // @ts-ignore
+ fireEvent(this, "email-changed", { value: username });
+ this._requestInProgress = false;
+ // @ts-ignore
+ fireEvent(this, "cloud-done", {
+ flashMessage: this.hass.localize(
+ "ui.panel.config.cloud.forgot_password.check_your_email"
+ ),
+ });
+ } catch (err: any) {
+ this._requestInProgress = false;
+ const errCode = err && err.body && err.body.code;
+ if (errCode === "usernotfound" && username !== username.toLowerCase()) {
+ await doResetPassword(username.toLowerCase());
+ } else {
+ this._error =
+ err && err.body && err.body.message
+ ? err.body.message
+ : "Unknown error";
+ }
+ }
+ };
+ await doResetPassword(email);
}
static get styles() {
diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts
index 50571b07a1..c617dfd2b2 100644
--- a/src/panels/config/cloud/login/cloud-login.ts
+++ b/src/panels/config/cloud/login/cloud-login.ts
@@ -22,6 +22,7 @@ import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
+import "../../../../components/ha-password-field";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -142,14 +143,13 @@ export class CloudLogin extends LitElement {
"ui.panel.config.cloud.login.email_error_msg"
)}
>
-
+ >
{
+ try {
+ const result = await cloudLogin(this.hass, username, password);
+ fireEvent(this, "ha-refresh-cloud-status");
+ this.email = "";
+ this._password = "";
+ if (result.cloud_pipeline) {
+ if (
+ await showConfirmationDialog(this, {
+ title: this.hass.localize(
+ "ui.panel.config.cloud.login.cloud_pipeline_title"
+ ),
+ text: this.hass.localize(
+ "ui.panel.config.cloud.login.cloud_pipeline_text"
+ ),
+ })
+ ) {
+ setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
+ }
+ }
+ } catch (err: any) {
+ const errCode = err && err.body && err.body.code;
+ if (errCode === "PasswordChangeRequired") {
+ showAlertDialog(this, {
+ title: this.hass.localize(
+ "ui.panel.config.cloud.login.alert_password_change_required"
+ ),
+ });
+ navigate("/config/cloud/forgot-password");
+ return;
+ }
+ if (errCode === "usernotfound" && username !== username.toLowerCase()) {
+ await doLogin(username.toLowerCase());
+ return;
}
- }
- } catch (err: any) {
- const errCode = err && err.body && err.body.code;
- if (errCode === "PasswordChangeRequired") {
- showAlertDialog(this, {
- title: this.hass.localize(
- "ui.panel.config.cloud.login.alert_password_change_required"
- ),
- });
- navigate("/config/cloud/forgot-password");
- return;
- }
- this._password = "";
- this._requestInProgress = false;
+ this._password = "";
+ this._requestInProgress = false;
- if (errCode === "UserNotConfirmed") {
- this._error = this.hass.localize(
- "ui.panel.config.cloud.login.alert_email_confirm_necessary"
- );
- } else {
- this._error =
- err && err.body && err.body.message
- ? err.body.message
- : "Unknown error";
+ if (errCode === "UserNotConfirmed") {
+ this._error = this.hass.localize(
+ "ui.panel.config.cloud.login.alert_email_confirm_necessary"
+ );
+ } else {
+ this._error =
+ err && err.body && err.body.message
+ ? err.body.message
+ : "Unknown error";
+ }
+
+ emailField.focus();
}
+ };
- emailField.focus();
- }
+ await doLogin(email);
}
private _handleRegister() {
diff --git a/src/panels/config/cloud/register/cloud-register.ts b/src/panels/config/cloud/register/cloud-register.ts
index dfab49cfbe..10964e5cf0 100644
--- a/src/panels/config/cloud/register/cloud-register.ts
+++ b/src/panels/config/cloud/register/cloud-register.ts
@@ -11,6 +11,7 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
+import "../../../../components/ha-password-field";
@customElement("cloud-register")
export class CloudRegister extends LitElement {
@@ -145,14 +146,13 @@ export class CloudRegister extends LitElement {
"ui.panel.config.cloud.register.email_error_msg"
)}
>
-
+ >
{
+ try {
+ await cloudResendVerification(this.hass, username);
+ this._verificationEmailSent(username);
+ } catch (err: any) {
+ const errCode = err && err.body && err.body.code;
+ if (errCode === "usernotfound" && username !== username.toLowerCase()) {
+ await doResend(username.toLowerCase());
+ } else {
+ this._error =
+ err && err.body && err.body.message
+ ? err.body.message
+ : "Unknown error";
+ }
+ }
+ };
+
+ await doResend(email);
}
private _verificationEmailSent(email: string) {
diff --git a/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts
index 6fad700a32..2be86b5ee4 100644
--- a/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts
@@ -17,6 +17,30 @@ import type { DeviceAction } from "../../../ha-config-device-page";
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
import { navigate } from "../../../../../../common/navigate";
+export const getMatterDeviceDefaultActions = (
+ el: HTMLElement,
+ hass: HomeAssistant,
+ device: DeviceRegistryEntry
+): DeviceAction[] => {
+ if (device.via_device_id !== null) {
+ // only show device actions for top level nodes (so not bridged)
+ return [];
+ }
+
+ const actions: DeviceAction[] = [];
+
+ actions.push({
+ label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
+ icon: mdiChatQuestion,
+ action: () =>
+ showMatterPingNodeDialog(el, {
+ device_id: device.id,
+ }),
+ });
+
+ return actions;
+};
+
export const getMatterDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
@@ -75,14 +99,5 @@ export const getMatterDeviceActions = async (
});
}
- actions.push({
- label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
- icon: mdiChatQuestion,
- action: () =>
- showMatterPingNodeDialog(el, {
- device_id: device.id,
- }),
- });
-
return actions;
};
diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts
index 1ea9c90b1c..39372e9340 100644
--- a/src/panels/config/devices/ha-config-device-page.ts
+++ b/src/panels/config/devices/ha-config-device-page.ts
@@ -1119,12 +1119,17 @@ export class HaConfigDevicePage extends LitElement {
const matter = await import(
"./device-detail/integration-elements/matter/device-actions"
);
- const actions = await matter.getMatterDeviceActions(
+ const defaultActions = matter.getMatterDeviceDefaultActions(
this,
this.hass,
device
);
- deviceActions.push(...actions);
+ deviceActions.push(...defaultActions);
+
+ // load matter device actions async to avoid an UI with 0 actions when the matter integration needs very long to get node diagnostics
+ matter.getMatterDeviceActions(this, this.hass, device).then((actions) => {
+ this._deviceActions = [...actions, ...(this._deviceActions || [])];
+ });
}
this._deviceActions = deviceActions;
diff --git a/src/panels/config/network/supervisor-network.ts b/src/panels/config/network/supervisor-network.ts
index 28e9351a95..166b60b58f 100644
--- a/src/panels/config/network/supervisor-network.ts
+++ b/src/panels/config/network/supervisor-network.ts
@@ -14,6 +14,11 @@ import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
import "../../../components/ha-icon-button";
+import "../../../components/ha-password-field";
+import "../../../components/ha-radio";
+import type { HaRadio } from "../../../components/ha-radio";
+import "../../../components/ha-textfield";
+import type { HaTextField } from "../../../components/ha-textfield";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
AccessPoints,
@@ -29,10 +34,6 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { showIPDetailDialog } from "./show-ip-detail-dialog";
-import "../../../components/ha-textfield";
-import type { HaTextField } from "../../../components/ha-textfield";
-import "../../../components/ha-radio";
-import type { HaRadio } from "../../../components/ha-radio";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -214,8 +215,7 @@ export class HassioNetwork extends LitElement {
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
-
-
+
`
: ""}
`
diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts
index 640f1aa104..7e7e9e70cd 100644
--- a/src/panels/config/users/dialog-add-user.ts
+++ b/src/panels/config/users/dialog-add-user.ts
@@ -29,6 +29,7 @@ import {
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant, ValueChangedEvent } from "../../../types";
import { AddUserDialogParams } from "./show-dialog-add-user";
+import "../../../components/ha-password-field";
@customElement("dialog-add-user")
export class DialogAddUser extends LitElement {
@@ -87,6 +88,7 @@ export class DialogAddUser extends LitElement {
if (!this._params) {
return nothing;
}
+
return html`
-
+ >
-
+ >
${this.hass.localize(
@@ -311,7 +311,8 @@ export class DialogAddUser extends LitElement {
display: flex;
padding: 8px 0;
}
- ha-textfield {
+ ha-textfield,
+ ha-password-field {
display: block;
margin-bottom: 8px;
}
diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
index a169eeb179..8baafaf909 100644
--- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts
+++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
@@ -26,7 +26,7 @@ const FIX_ISSUES_ORDER = {
no_state: 0,
entity_no_longer_recorded: 1,
entity_not_recorded: 1,
- unsupported_state_class: 2,
+ state_class_removed: 2,
units_changed: 3,
};
@@ -273,11 +273,9 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
);
if (
result &&
- [
- "no_state",
- "entity_no_longer_recorded",
- "unsupported_state_class",
- ].includes(issue.type)
+ ["no_state", "entity_no_longer_recorded", "state_class_removed"].includes(
+ issue.type
+ )
) {
this._deletedStatistics.add(issue.data.statistic_id);
}
diff --git a/src/panels/developer-tools/statistics/fix-statistics.ts b/src/panels/developer-tools/statistics/fix-statistics.ts
index 8dd140e718..0329cf77be 100644
--- a/src/panels/developer-tools/statistics/fix-statistics.ts
+++ b/src/panels/developer-tools/statistics/fix-statistics.ts
@@ -103,31 +103,30 @@ export const fixStatisticsIssue = async (
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
- case "unsupported_state_class":
+ case "state_class_removed":
return showConfirmationDialog(element, {
title: localize(
- "ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.title"
+ "ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.title"
),
text: html`${localize(
- "ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_1",
+ "ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
statistic_id: issue.data.statistic_id,
- state_class: issue.data.state_class,
}
)}
${localize(
- "ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_2"
+ "ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_2"
)}
${localize(
- "ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_6",
+ "ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: localize("ui.common.delete"),
diff --git a/src/panels/lovelace/cards/heading/hui-heading-entity.ts b/src/panels/lovelace/cards/heading/hui-heading-entity.ts
deleted file mode 100644
index 8b12950e7f..0000000000
--- a/src/panels/lovelace/cards/heading/hui-heading-entity.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-import { HassEntity } from "home-assistant-js-websocket";
-import {
- CSSResultGroup,
- LitElement,
- PropertyValues,
- css,
- html,
- nothing,
-} from "lit";
-import { customElement, property } from "lit/decorators";
-import { ifDefined } from "lit/directives/if-defined";
-import { styleMap } from "lit/directives/style-map";
-import memoizeOne from "memoize-one";
-import { computeCssColor } from "../../../../common/color/compute-color";
-import {
- hsv2rgb,
- rgb2hex,
- rgb2hsv,
-} from "../../../../common/color/convert-color";
-import { MediaQueriesListener } from "../../../../common/dom/media_query";
-import { computeDomain } from "../../../../common/entity/compute_domain";
-import { stateActive } from "../../../../common/entity/state_active";
-import { stateColorCss } from "../../../../common/entity/state_color";
-import "../../../../components/ha-card";
-import "../../../../components/ha-icon";
-import "../../../../components/ha-icon-next";
-import "../../../../components/ha-state-icon";
-import { ActionHandlerEvent } from "../../../../data/lovelace/action_handler";
-import "../../../../state-display/state-display";
-import { HomeAssistant } from "../../../../types";
-import { actionHandler } from "../../common/directives/action-handler-directive";
-import { handleAction } from "../../common/handle-action";
-import { hasAction } from "../../common/has-action";
-import {
- attachConditionMediaQueriesListeners,
- checkConditionsMet,
-} from "../../common/validate-condition";
-import { DEFAULT_CONFIG } from "../../editor/heading-entity/hui-heading-entity-editor";
-import type { HeadingEntityConfig } from "../types";
-
-@customElement("hui-heading-entity")
-export class HuiHeadingEntity extends LitElement {
- @property({ attribute: false }) public hass!: HomeAssistant;
-
- @property({ attribute: false }) public config!: HeadingEntityConfig | string;
-
- @property({ type: Boolean }) public preview = false;
-
- private _listeners: MediaQueriesListener[] = [];
-
- private _handleAction(ev: ActionHandlerEvent) {
- const config: HeadingEntityConfig = {
- tap_action: {
- action: "none",
- },
- ...this._config(this.config),
- };
- handleAction(this, this.hass!, config, ev.detail.action!);
- }
-
- private _config = memoizeOne(
- (configOrString: HeadingEntityConfig | string): HeadingEntityConfig => {
- const config =
- typeof configOrString === "string"
- ? { entity: configOrString }
- : configOrString;
-
- return {
- ...DEFAULT_CONFIG,
- tap_action: {
- action: "none",
- },
- ...config,
- };
- }
- );
-
- public disconnectedCallback() {
- super.disconnectedCallback();
- this._clearMediaQueries();
- }
-
- public connectedCallback() {
- super.connectedCallback();
- this._listenMediaQueries();
- this._updateVisibility();
- }
-
- protected update(changedProps: PropertyValues): void {
- super.update(changedProps);
- if (changedProps.has("hass") || changedProps.has("preview")) {
- this._updateVisibility();
- }
- }
-
- private _updateVisibility(forceVisible?: boolean) {
- const config = this._config(this.config);
- const visible =
- forceVisible ||
- this.preview ||
- !config.visibility ||
- checkConditionsMet(config.visibility, this.hass);
- this.toggleAttribute("hidden", !visible);
- }
-
- private _clearMediaQueries() {
- this._listeners.forEach((unsub) => unsub());
- this._listeners = [];
- }
-
- private _listenMediaQueries() {
- const config = this._config(this.config);
- if (!config?.visibility) {
- return;
- }
- const conditions = config.visibility;
- const hasOnlyMediaQuery =
- conditions.length === 1 &&
- conditions[0].condition === "screen" &&
- !!conditions[0].media_query;
-
- this._listeners = attachConditionMediaQueriesListeners(
- config.visibility,
- (matches) => {
- this._updateVisibility(hasOnlyMediaQuery && matches);
- }
- );
- }
-
- private _computeStateColor = memoizeOne(
- (entity: HassEntity, color?: string) => {
- if (!color || color === "none") {
- return undefined;
- }
-
- if (color === "state") {
- // Use light color if the light support rgb
- if (
- computeDomain(entity.entity_id) === "light" &&
- entity.attributes.rgb_color
- ) {
- const hsvColor = rgb2hsv(entity.attributes.rgb_color);
-
- // Modify the real rgb color for better contrast
- if (hsvColor[1] < 0.4) {
- // Special case for very light color (e.g: white)
- if (hsvColor[1] < 0.1) {
- hsvColor[2] = 225;
- } else {
- hsvColor[1] = 0.4;
- }
- }
- return rgb2hex(hsv2rgb(hsvColor));
- }
- // Fallback to state color
- return stateColorCss(entity);
- }
-
- if (color) {
- // Use custom color if active
- return stateActive(entity) ? computeCssColor(color) : undefined;
- }
- return color;
- }
- );
-
- protected render() {
- const config = this._config(this.config);
-
- const stateObj = this.hass!.states[config.entity];
-
- if (!stateObj) {
- return nothing;
- }
-
- const color = this._computeStateColor(stateObj, config.color);
-
- const actionable = hasAction(config.tap_action);
-
- const style = {
- "--color": color,
- };
-
- return html`
-
- ${config.show_icon
- ? html`
-
- `
- : nothing}
- ${config.show_state
- ? html`
-
- `
- : nothing}
-
- `;
- }
-
- static get styles(): CSSResultGroup {
- return css`
- [role="button"] {
- cursor: pointer;
- }
- .entity {
- display: flex;
- flex-direction: row;
- white-space: nowrap;
- align-items: center;
- gap: 3px;
- color: var(--secondary-text-color);
- font-family: Roboto;
- font-size: 14px;
- font-style: normal;
- font-weight: 500;
- line-height: 20px; /* 142.857% */
- letter-spacing: 0.1px;
- --mdc-icon-size: 14px;
- --state-inactive-color: initial;
- }
- .entity ha-state-icon {
- --ha-icon-display: block;
- color: var(--color);
- }
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "hui-heading-entity": HuiHeadingEntity;
- }
-}
diff --git a/src/panels/lovelace/cards/hui-heading-card.ts b/src/panels/lovelace/cards/hui-heading-card.ts
index 63bad9a61f..5058f2560c 100644
--- a/src/panels/lovelace/cards/hui-heading-card.ts
+++ b/src/panels/lovelace/cards/hui-heading-card.ts
@@ -11,14 +11,25 @@ import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
+import "../heading-badges/hui-heading-badge";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
-import "./heading/hui-heading-entity";
import type { HeadingCardConfig } from "./types";
+export const migrateHeadingCardConfig = (
+ config: HeadingCardConfig
+): HeadingCardConfig => {
+ const newConfig = { ...config };
+ if (newConfig.entities) {
+ newConfig.badges = [...(newConfig.badges || []), ...newConfig.entities];
+ delete newConfig.entities;
+ }
+ return newConfig;
+};
+
@customElement("hui-heading-card")
export class HuiHeadingCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise {
@@ -45,7 +56,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
tap_action: {
action: "none",
},
- ...config,
+ ...migrateHeadingCardConfig(config),
};
}
@@ -73,6 +84,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
const style = this._config.heading_style || "title";
+ const badges = this._config.badges;
+
return html`
@@ -91,17 +104,17 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
: nothing}
${actionable ? html`` : nothing}
- ${this._config.entities?.length
+ ${badges?.length
? html`
-
- ${this._config.entities.map(
+
+ ${badges.map(
(config) => html`
-
-
+
`
)}
@@ -150,7 +163,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
.container .content:not(:has(p)) {
min-width: fit-content;
}
- .container .entities {
+ .container .badges {
flex: 0 0;
}
.content {
@@ -158,12 +171,12 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
flex-direction: row;
align-items: center;
gap: 8px;
- color: var(--primary-text-color);
- font-size: 16px;
- font-weight: 500;
- line-height: 24px;
+ color: var(--ha-heading-card-title-color, var(--primary-text-color));
+ font-size: var(--ha-heading-card-title-font-size, 16px);
+ font-weight: var(--ha-heading-card-title-font-weight, 400);
+ line-height: var(--ha-heading-card-title-line-height, 24px);
letter-spacing: 0.1px;
- --mdc-icon-size: 16px;
+ --mdc-icon-size: 18px;
}
.content ha-icon,
.content ha-icon-next {
@@ -181,12 +194,15 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
min-width: 0;
}
.content.subtitle {
- color: var(--secondary-text-color);
- font-size: 14px;
- font-weight: 500;
- line-height: 20px;
+ color: var(
+ --ha-heading-card-subtitle-color,
+ var(--secondary-text-color)
+ );
+ font-size: var(--ha-heading-card-subtitle-font-size, 14px);
+ font-weight: var(--ha-heading-card-subtitle-font-weight, 500);
+ line-height: var(--ha-heading-card-subtitle-line-height, 20px);
}
- .entities {
+ .badges {
display: flex;
flex-direction: row;
align-items: center;
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index e3d3257346..3c21617dfe 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -16,6 +16,7 @@ import {
LovelaceRowConfig,
} from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
export type AlarmPanelCardConfigState =
| "arm_away"
@@ -503,21 +504,12 @@ export interface TileCardConfig extends LovelaceCardConfig {
features?: LovelaceCardFeatureConfig[];
}
-export interface HeadingEntityConfig {
- entity: string;
- state_content?: string | string[];
- icon?: string;
- show_state?: boolean;
- show_icon?: boolean;
- color?: string;
- tap_action?: ActionConfig;
- visibility?: Condition[];
-}
-
export interface HeadingCardConfig extends LovelaceCardConfig {
heading_style?: "title" | "subtitle";
heading?: string;
icon?: string;
tap_action?: ActionConfig;
- entities?: (string | HeadingEntityConfig)[];
+ badges?: LovelaceHeadingBadgeConfig[];
+ /** @deprecated Use `badges` instead */
+ entities?: LovelaceHeadingBadgeConfig[];
}
diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts
index 33aaecb0af..7346e63b62 100644
--- a/src/panels/lovelace/create-element/create-element-base.ts
+++ b/src/panels/lovelace/create-element/create-element-base.ts
@@ -16,6 +16,7 @@ import type { ErrorCardConfig } from "../cards/types";
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
import {
LovelaceBadge,
LovelaceBadgeConstructor,
@@ -26,6 +27,8 @@ import {
LovelaceElementConstructor,
LovelaceHeaderFooter,
LovelaceHeaderFooterConstructor,
+ LovelaceHeadingBadge,
+ LovelaceHeadingBadgeConstructor,
LovelaceRowConstructor,
} from "../types";
@@ -72,6 +75,11 @@ interface CreateElementConfigTypes {
element: LovelaceSectionElement;
constructor: unknown;
};
+ "heading-badge": {
+ config: LovelaceHeadingBadgeConfig;
+ element: LovelaceHeadingBadge;
+ constructor: LovelaceHeadingBadgeConstructor;
+ };
}
export const createErrorCardElement = (config: ErrorCardConfig) => {
@@ -102,6 +110,20 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
return el;
};
+export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => {
+ const el = document.createElement("hui-error-heading-badge");
+ if (customElements.get("hui-error-heading-badge")) {
+ el.setConfig(config);
+ } else {
+ import("../heading-badges/hui-error-heading-badge");
+ customElements.whenDefined("hui-error-heading-badge").then(() => {
+ customElements.upgrade(el);
+ el.setConfig(config);
+ });
+ }
+ return el;
+};
+
export const createErrorCardConfig = (error, origConfig) => ({
type: "error",
error,
@@ -114,6 +136,12 @@ export const createErrorBadgeConfig = (error, origConfig) => ({
origConfig,
});
+export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
+ type: "error",
+ error,
+ origConfig,
+});
+
const _createElement =
(
tag: string,
config: CreateElementConfigTypes[T]["config"]
@@ -134,6 +162,11 @@ const _createErrorElement = (
if (tagSuffix === "badge") {
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
}
+ if (tagSuffix === "heading-badge") {
+ return createErrorHeadingBadgeElement(
+ createErrorHeadingBadgeConfig(error, config)
+ );
+ }
return createErrorCardElement(createErrorCardConfig(error, config));
};
diff --git a/src/panels/lovelace/create-element/create-heading-badge-element.ts b/src/panels/lovelace/create-element/create-heading-badge-element.ts
new file mode 100644
index 0000000000..e45bb6f14f
--- /dev/null
+++ b/src/panels/lovelace/create-element/create-heading-badge-element.ts
@@ -0,0 +1,22 @@
+import "../heading-badges/hui-entity-heading-badge";
+
+import {
+ createLovelaceElement,
+ getLovelaceElementClass,
+} from "./create-element-base";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
+
+const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
+
+export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) =>
+ createLovelaceElement(
+ "heading-badge",
+ config,
+ ALWAYS_LOADED_TYPES,
+ undefined,
+ undefined,
+ "entity"
+ );
+
+export const getHeadingBadgeElementClass = (type: string) =>
+ getLovelaceElementClass(type, "heading-badge", ALWAYS_LOADED_TYPES);
diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts
similarity index 67%
rename from src/panels/lovelace/editor/config-elements/hui-entities-editor.ts
rename to src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts
index e47abf3280..c102f3d37e 100644
--- a/src/panels/lovelace/editor/config-elements/hui-entities-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts
@@ -14,23 +14,21 @@ import "../../../../components/ha-list-item";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../../types";
-
-type EntityConfig = {
- entity: string;
-};
+import { LovelaceHeadingBadgeConfig } from "../../heading-badges/types";
declare global {
interface HASSDomEvents {
- "edit-entity": { index: number };
+ "edit-heading-badge": { index: number };
+ "heading-badges-changed": { badges: LovelaceHeadingBadgeConfig[] };
}
}
-@customElement("hui-entities-editor")
-export class HuiEntitiesEditor extends LitElement {
+@customElement("hui-heading-badges-editor")
+export class HuiHeadingBadgesEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
- public entities?: EntityConfig[];
+ public badges?: LovelaceHeadingBadgeConfig[];
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@@ -40,14 +38,30 @@ export class HuiEntitiesEditor extends LitElement {
private _opened = false;
- private _entitiesKeys = new WeakMap();
+ private _badgesKeys = new WeakMap();
- private _getKey(entity: EntityConfig) {
- if (!this._entitiesKeys.has(entity)) {
- this._entitiesKeys.set(entity, Math.random().toString());
+ private _getKey(badge: LovelaceHeadingBadgeConfig) {
+ if (!this._badgesKeys.has(badge)) {
+ this._badgesKeys.set(badge, Math.random().toString());
}
- return this._entitiesKeys.get(entity)!;
+ return this._badgesKeys.get(badge)!;
+ }
+
+ private _computeBadgeLabel(badge: LovelaceHeadingBadgeConfig) {
+ const type = badge.type ?? "entity";
+
+ if (type === "entity") {
+ const entityId = "entity" in badge ? (badge.entity as string) : undefined;
+ const stateObj = entityId ? this.hass.states[entityId] : undefined;
+ return (
+ (stateObj && stateObj.attributes.friendly_name) ||
+ entityId ||
+ type ||
+ "Unknown badge"
+ );
+ }
+ return type;
}
protected render() {
@@ -56,46 +70,35 @@ export class HuiEntitiesEditor extends LitElement {
}
return html`
- ${this.entities
+ ${this.badges
? html`
${repeat(
- this.entities,
- (entityConf) => this._getKey(entityConf),
- (entityConf, index) => {
- const editable = true;
-
- const entityId = entityConf.entity;
- const stateObj = this.hass.states[entityId];
- const name = stateObj
- ? stateObj.attributes.friendly_name
- : undefined;
+ this.badges,
+ (badge) => this._getKey(badge),
+ (badge, index) => {
+ const label = this._computeBadgeLabel(badge);
return html`
-
+
-
-
${name || entityId}
+
+ ${label}
- ${editable
- ? html`
-
- `
- : nothing}
+
* {
+ .badge .handle > * {
pointer-events: none;
}
- .entity-content {
+ .badge-content {
height: 60px;
font-size: 16px;
display: flex;
@@ -252,7 +258,7 @@ export class HuiEntitiesEditor extends LitElement {
flex-grow: 1;
}
- .entity-content div {
+ .badge-content div {
display: flex;
flex-direction: column;
}
@@ -291,6 +297,6 @@ export class HuiEntitiesEditor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
- "hui-entities-editor": HuiEntitiesEditor;
+ "hui-heading-badges-editor": HuiHeadingBadgesEditor;
}
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
index 727a4223d7..69c89e6bc8 100644
--- a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
@@ -22,15 +22,19 @@ import type {
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
-import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
+import { migrateHeadingCardConfig } from "../../cards/hui-heading-card";
+import type { HeadingCardConfig } from "../../cards/types";
import { UiAction } from "../../components/hui-action-editor";
+import {
+ EntityHeadingBadgeConfig,
+ LovelaceHeadingBadgeConfig,
+} from "../../heading-badges/types";
import type { LovelaceCardEditor } from "../../types";
-import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
-import { configElementStyle } from "./config-elements-style";
-import "./hui-entities-editor";
import { EditSubElementEvent } from "../types";
+import { configElementStyle } from "./config-elements-style";
+import "./hui-heading-badges-editor";
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
@@ -41,7 +45,7 @@ const cardConfigStruct = assign(
heading: optional(string()),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
- entities: optional(array(any())),
+ badges: optional(array(any())),
})
);
@@ -55,8 +59,8 @@ export class HuiHeadingCardEditor
@state() private _config?: HeadingCardConfig;
public setConfig(config: HeadingCardConfig): void {
- assert(config, cardConfigStruct);
- this._config = config;
+ this._config = migrateHeadingCardConfig(config);
+ assert(this._config, cardConfigStruct);
}
private _schema = memoizeOne(
@@ -103,8 +107,9 @@ export class HuiHeadingCardEditor
] as const satisfies readonly HaFormSchema[]
);
- private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
- processEditorEntities(entities || [])
+ private _badges = memoizeOne(
+ (badges: HeadingCardConfig["badges"]): LovelaceHeadingBadgeConfig[] =>
+ badges || []
);
protected render() {
@@ -138,19 +143,19 @@ export class HuiHeadingCardEditor
)}
-
-
+
`;
}
- private _entitiesChanged(ev: CustomEvent): void {
+ private _badgesChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
@@ -158,7 +163,7 @@ export class HuiHeadingCardEditor
const config = {
...this._config,
- entities: ev.detail.entities as HeadingEntityConfig[],
+ badges: ev.detail.badges as LovelaceHeadingBadgeConfig[],
};
fireEvent(this, "config-changed", { config });
@@ -175,22 +180,22 @@ export class HuiHeadingCardEditor
fireEvent(this, "config-changed", { config });
}
- private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
+ private _editBadge(ev: HASSDomEvent<{ index: number }>): void {
ev.stopPropagation();
const index = ev.detail.index;
- const config = this._config!.entities![index!];
+ const config = this._badges(this._config!.badges)[index];
fireEvent(this, "edit-sub-element", {
config: config,
- saveConfig: (newConfig) => this._updateEntity(index, newConfig),
- type: "heading-entity",
- } as EditSubElementEvent);
+ saveConfig: (newConfig) => this._updateBadge(index, newConfig),
+ type: "heading-badge",
+ } as EditSubElementEvent);
}
- private _updateEntity(index: number, entity: HeadingEntityConfig) {
- const entities = this._config!.entities!.concat();
- entities[index] = entity;
- const config = { ...this._config!, entities };
+ private _updateBadge(index: number, entity: EntityHeadingBadgeConfig) {
+ const badges = this._config!.badges!.concat();
+ badges[index] = entity;
+ const config = { ...this._config!, badges };
fireEvent(this, "config-changed", {
config: config,
});
diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
similarity index 92%
rename from src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts
rename to src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
index 6d0c4f3d51..b1446d4a12 100644
--- a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts
+++ b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
@@ -21,19 +21,21 @@ import type {
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
-import type { HeadingEntityConfig } from "../../cards/types";
import { Condition } from "../../common/validate-condition";
+import { EntityHeadingBadgeConfig } from "../../heading-badges/types";
import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct";
-export const DEFAULT_CONFIG: Partial = {
+export const DEFAULT_CONFIG: Partial = {
+ type: "entity",
show_state: true,
show_icon: true,
};
const entityConfigStruct = object({
+ type: optional(string()),
entity: string(),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
@@ -44,7 +46,7 @@ const entityConfigStruct = object({
visibility: optional(array(any())),
});
-type FormData = HeadingEntityConfig & {
+type FormData = EntityHeadingBadgeConfig & {
displayed_elements?: string[];
};
@@ -57,9 +59,9 @@ export class HuiHeadingEntityEditor
@property({ type: Boolean }) public preview = false;
- @state() private _config?: HeadingEntityConfig;
+ @state() private _config?: EntityHeadingBadgeConfig;
- public setConfig(config: HeadingEntityConfig): void {
+ public setConfig(config: EntityHeadingBadgeConfig): void {
assert(config, entityConfigStruct);
this._config = {
...DEFAULT_CONFIG,
@@ -150,12 +152,14 @@ export class HuiHeadingEntityEditor
] as const satisfies readonly HaFormSchema[]
);
- private _displayedElements = memoizeOne((config: HeadingEntityConfig) => {
- const elements: string[] = [];
- if (config.show_state) elements.push("state");
- if (config.show_icon) elements.push("icon");
- return elements;
- });
+ private _displayedElements = memoizeOne(
+ (config: EntityHeadingBadgeConfig) => {
+ const elements: string[] = [];
+ if (config.show_state) elements.push("state");
+ if (config.show_icon) elements.push("icon");
+ return elements;
+ }
+ );
protected render() {
if (!this.hass || !this._config) {
@@ -228,7 +232,7 @@ export class HuiHeadingEntityEditor
const conditions = ev.detail.value as Condition[];
- const newConfig: HeadingEntityConfig = {
+ const newConfig: EntityHeadingBadgeConfig = {
...this._config,
visibility: conditions,
};
diff --git a/src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts
new file mode 100644
index 0000000000..dadef34f8b
--- /dev/null
+++ b/src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts
@@ -0,0 +1,42 @@
+import { customElement } from "lit/decorators";
+import { getHeadingBadgeElementClass } from "../../create-element/create-heading-badge-element";
+import type { EntityHeadingBadgeConfig } from "../../heading-badges/types";
+import { LovelaceConfigForm, LovelaceHeadingBadgeEditor } from "../../types";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
+
+@customElement("hui-heading-badge-element-editor")
+export class HuiHeadingEntityElementEditor extends HuiTypedElementEditor {
+ protected get configElementType(): string | undefined {
+ return this.value?.type || "entity";
+ }
+
+ protected async getConfigElement(): Promise<
+ LovelaceHeadingBadgeEditor | undefined
+ > {
+ const elClass = await getHeadingBadgeElementClass(this.configElementType!);
+
+ // Check if a GUI editor exists
+ if (elClass && elClass.getConfigElement) {
+ return elClass.getConfigElement();
+ }
+
+ return undefined;
+ }
+
+ protected async getConfigForm(): Promise {
+ const elClass = await getHeadingBadgeElementClass(this.configElementType!);
+
+ // Check if a schema exists
+ if (elClass && elClass.getConfigForm) {
+ return elClass.getConfigForm();
+ }
+
+ return undefined;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-badge-element-editor": HuiHeadingEntityElementEditor;
+ }
+}
diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
deleted file mode 100644
index ada27a39d2..0000000000
--- a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { customElement } from "lit/decorators";
-import { HeadingEntityConfig } from "../../cards/types";
-import { HuiElementEditor } from "../hui-element-editor";
-import type { HuiHeadingEntityEditor } from "./hui-heading-entity-editor";
-
-@customElement("hui-heading-entity-element-editor")
-export class HuiHeadingEntityElementEditor extends HuiElementEditor {
- protected async getConfigElement(): Promise<
- HuiHeadingEntityEditor | undefined
- > {
- await import("./hui-heading-entity-editor");
- return document.createElement("hui-heading-entity-editor");
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "hui-heading-entity-element-editor": HuiHeadingEntityElementEditor;
- }
-}
diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts
index 1202d598a3..0d01341abc 100644
--- a/src/panels/lovelace/editor/hui-sub-element-editor.ts
+++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts
@@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types";
import "./entity-row-editor/hui-row-element-editor";
import "./feature-editor/hui-card-feature-element-editor";
import "./header-footer-editor/hui-header-footer-element-editor";
-import "./heading-entity/hui-heading-entity-element-editor";
+import "./heading-badge-editor/hui-heading-badge-element-editor";
import type { HuiElementEditor } from "./hui-element-editor";
import "./picture-element-editor/hui-picture-element-element-editor";
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
@@ -132,16 +132,16 @@ export class HuiSubElementEditor extends LitElement {
@GUImode-changed=${this._handleGUIModeChanged}
>
`;
- case "heading-entity":
+ case "heading-badge":
return html`
-
+ >
`;
default:
return nothing;
diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts
index a409f00729..c62c7595ab 100644
--- a/src/panels/lovelace/editor/types.ts
+++ b/src/panels/lovelace/editor/types.ts
@@ -9,7 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LovelaceElementConfig } from "../elements/types";
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
-import { HeadingEntityConfig } from "../cards/types";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
export interface YamlChangedEvent extends Event {
detail: {
@@ -97,10 +97,10 @@ export interface SubElementEditorConfig {
| LovelaceHeaderFooterConfig
| LovelaceCardFeatureConfig
| LovelaceElementConfig
- | HeadingEntityConfig;
+ | LovelaceHeadingBadgeConfig;
saveElementConfig?: (elementConfig: any) => void;
context?: any;
- type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
+ type: "header" | "footer" | "row" | "feature" | "element" | "heading-badge";
}
export interface EditSubElementEvent {
diff --git a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts
index a14e5b35a6..2d4c3c6bd3 100644
--- a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts
+++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts
@@ -379,7 +379,19 @@ export class HuiDialogEditView extends LitElement {
};
if (viewConf.type === SECTION_VIEW_LAYOUT && !viewConf.sections?.length) {
- viewConf.sections = [{ type: "grid", cards: [] }];
+ viewConf.sections = [
+ {
+ type: "grid",
+ cards: [
+ {
+ type: "heading",
+ heading: this.hass!.localize(
+ "ui.panel.lovelace.editor.section.default_section_title"
+ ),
+ },
+ ],
+ },
+ ];
} else if (!viewConf.cards?.length) {
viewConf.cards = [];
}
diff --git a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
new file mode 100644
index 0000000000..b6084a846d
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
@@ -0,0 +1,177 @@
+import { mdiAlertCircle } from "@mdi/js";
+import { HassEntity } from "home-assistant-js-websocket";
+import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { styleMap } from "lit/directives/style-map";
+import memoizeOne from "memoize-one";
+import { computeCssColor } from "../../../common/color/compute-color";
+import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
+import { computeDomain } from "../../../common/entity/compute_domain";
+import { stateActive } from "../../../common/entity/state_active";
+import { stateColorCss } from "../../../common/entity/state_color";
+import "../../../components/ha-heading-badge";
+import "../../../components/ha-state-icon";
+import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
+import "../../../state-display/state-display";
+import { HomeAssistant } from "../../../types";
+import { actionHandler } from "../common/directives/action-handler-directive";
+import { handleAction } from "../common/handle-action";
+import { hasAction } from "../common/has-action";
+import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
+import { LovelaceHeadingBadge, LovelaceHeadingBadgeEditor } from "../types";
+import { EntityHeadingBadgeConfig } from "./types";
+
+@customElement("hui-entity-heading-badge")
+export class HuiEntityHeadingBadge
+ extends LitElement
+ implements LovelaceHeadingBadge
+{
+ public static async getConfigElement(): Promise {
+ await import(
+ "../editor/heading-badge-editor/hui-entity-heading-badge-editor"
+ );
+ return document.createElement("hui-heading-entity-editor");
+ }
+
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @state() private _config?: EntityHeadingBadgeConfig;
+
+ @property({ type: Boolean }) public preview = false;
+
+ public setConfig(config): void {
+ this._config = {
+ ...DEFAULT_CONFIG,
+ tap_action: {
+ action: "none",
+ },
+ ...config,
+ };
+ }
+
+ private _handleAction(ev: ActionHandlerEvent) {
+ const config: EntityHeadingBadgeConfig = {
+ tap_action: {
+ action: "none",
+ },
+ ...this._config!,
+ };
+ handleAction(this, this.hass!, config, ev.detail.action!);
+ }
+
+ private _computeStateColor = memoizeOne(
+ (entity: HassEntity, color?: string) => {
+ if (!color || color === "none") {
+ return undefined;
+ }
+
+ if (color === "state") {
+ // Use light color if the light support rgb
+ if (
+ computeDomain(entity.entity_id) === "light" &&
+ entity.attributes.rgb_color
+ ) {
+ const hsvColor = rgb2hsv(entity.attributes.rgb_color);
+
+ // Modify the real rgb color for better contrast
+ if (hsvColor[1] < 0.4) {
+ // Special case for very light color (e.g: white)
+ if (hsvColor[1] < 0.1) {
+ hsvColor[2] = 225;
+ } else {
+ hsvColor[1] = 0.4;
+ }
+ }
+ return rgb2hex(hsv2rgb(hsvColor));
+ }
+ // Fallback to state color
+ return stateColorCss(entity);
+ }
+
+ if (color) {
+ // Use custom color if active
+ return stateActive(entity) ? computeCssColor(color) : undefined;
+ }
+ return color;
+ }
+ );
+
+ protected render() {
+ if (!this.hass || !this._config) {
+ return nothing;
+ }
+
+ const config = this._config;
+
+ const entityId = config.entity;
+ const stateObj = this.hass!.states[entityId];
+
+ if (!stateObj) {
+ return html`
+
+
+ -
+
+ `;
+ }
+
+ const color = this._computeStateColor(stateObj, config.color);
+
+ const style = {
+ "--icon-color": color,
+ };
+
+ return html`
+
+ ${config.show_icon
+ ? html`
+
+ `
+ : nothing}
+ ${config.show_state
+ ? html`
+
+ `
+ : nothing}
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ [role="button"] {
+ cursor: pointer;
+ }
+ ha-heading-badge {
+ --state-inactive-color: initial;
+ }
+ ha-heading-badge.error {
+ --icon-color: var(--red-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-entity-heading-badge": HuiEntityHeadingBadge;
+ }
+}
diff --git a/src/panels/lovelace/heading-badges/hui-error-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-error-heading-badge.ts
new file mode 100644
index 0000000000..9210592763
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/hui-error-heading-badge.ts
@@ -0,0 +1,94 @@
+import { mdiAlertCircle } from "@mdi/js";
+import { dump } from "js-yaml";
+import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { customElement, state } from "lit/decorators";
+import "../../../components/ha-badge";
+import "../../../components/ha-svg-icon";
+import { HomeAssistant } from "../../../types";
+import { showAlertDialog } from "../custom-card-helpers";
+import { LovelaceBadge } from "../types";
+import { ErrorBadgeConfig } from "./types";
+
+export const createErrorHeadingBadgeElement = (config) => {
+ const el = document.createElement("hui-error-heading-badge");
+ el.setConfig(config);
+ return el;
+};
+
+export const createErrorHeadingBadgeConfig = (error) => ({
+ type: "error",
+ error,
+});
+
+@customElement("hui-error-heading-badge")
+export class HuiErrorHeadingBadge extends LitElement implements LovelaceBadge {
+ public hass?: HomeAssistant;
+
+ @state() private _config?: ErrorBadgeConfig;
+
+ public setConfig(config: ErrorBadgeConfig): void {
+ this._config = config;
+ }
+
+ private _viewDetail() {
+ let dumped: string | undefined;
+
+ if (this._config!.origConfig) {
+ try {
+ dumped = dump(this._config!.origConfig);
+ } catch (err: any) {
+ dumped = `[Error dumping ${this._config!.origConfig}]`;
+ }
+ }
+
+ showAlertDialog(this, {
+ title: this._config?.error,
+ warning: true,
+ text: dumped ? html`${dumped}
` : "",
+ });
+ }
+
+ protected render() {
+ if (!this._config) {
+ return nothing;
+ }
+
+ return html`
+
+
+ ${this._config.error}
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-heading-badge {
+ --icon-color: var(--error-color);
+ color: var(--error-color);
+ }
+ .content {
+ max-width: 70px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ pre {
+ font-family: var(--code-font-family, monospace);
+ white-space: break-spaces;
+ user-select: text;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-error-heading-badge": HuiErrorHeadingBadge;
+ }
+}
diff --git a/src/panels/lovelace/heading-badges/hui-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-heading-badge.ts
new file mode 100644
index 0000000000..92c5b04ca8
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/hui-heading-badge.ts
@@ -0,0 +1,202 @@
+import { PropertyValues, ReactiveElement } from "lit";
+import { customElement, property } from "lit/decorators";
+import { fireEvent } from "../../../common/dom/fire_event";
+import { MediaQueriesListener } from "../../../common/dom/media_query";
+import "../../../components/ha-svg-icon";
+import type { HomeAssistant } from "../../../types";
+import {
+ attachConditionMediaQueriesListeners,
+ checkConditionsMet,
+} from "../common/validate-condition";
+import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
+import type { LovelaceHeadingBadge } from "../types";
+import { LovelaceHeadingBadgeConfig } from "./types";
+
+declare global {
+ interface HASSDomEvents {
+ "heading-badge-visibility-changed": { value: boolean };
+ "heading-badge-updated": undefined;
+ }
+}
+
+@customElement("hui-heading-badge")
+export class HuiHeadingBadge extends ReactiveElement {
+ @property({ type: Boolean }) public preview = false;
+
+ @property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
+
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ private _elementConfig?: LovelaceHeadingBadgeConfig;
+
+ public load() {
+ if (!this.config) {
+ throw new Error("Cannot build heading badge without config");
+ }
+ this._loadElement(this.config);
+ }
+
+ private _element?: LovelaceHeadingBadge;
+
+ private _listeners: MediaQueriesListener[] = [];
+
+ protected createRenderRoot() {
+ return this;
+ }
+
+ public disconnectedCallback() {
+ super.disconnectedCallback();
+ this._clearMediaQueries();
+ }
+
+ public connectedCallback() {
+ super.connectedCallback();
+ this._listenMediaQueries();
+ this._updateVisibility();
+ }
+
+ private _updateElement(config: LovelaceHeadingBadgeConfig) {
+ if (!this._element) {
+ return;
+ }
+ this._element.setConfig(config);
+ this._elementConfig = config;
+ fireEvent(this, "heading-badge-updated");
+ }
+
+ private _loadElement(config: LovelaceHeadingBadgeConfig) {
+ this._element = createHeadingBadgeElement(config);
+ this._elementConfig = config;
+ if (this.hass) {
+ this._element.hass = this.hass;
+ }
+ this._element.addEventListener(
+ "ll-upgrade",
+ (ev: Event) => {
+ ev.stopPropagation();
+ if (this.hass) {
+ this._element!.hass = this.hass;
+ }
+ fireEvent(this, "heading-badge-updated");
+ },
+ { once: true }
+ );
+ this._element.addEventListener(
+ "ll-rebuild",
+ (ev: Event) => {
+ ev.stopPropagation();
+ this._loadElement(config);
+ fireEvent(this, "heading-badge-updated");
+ },
+ { once: true }
+ );
+ while (this.lastChild) {
+ this.removeChild(this.lastChild);
+ }
+ this._updateVisibility();
+ }
+
+ protected willUpdate(changedProps: PropertyValues): void {
+ super.willUpdate(changedProps);
+
+ if (!this._element) {
+ this.load();
+ }
+ }
+
+ protected update(changedProps: PropertyValues) {
+ super.update(changedProps);
+
+ if (this._element) {
+ if (changedProps.has("config")) {
+ const elementConfig = this._elementConfig;
+ if (this.config !== elementConfig && this.config) {
+ const typeChanged = this.config?.type !== elementConfig?.type;
+ if (typeChanged) {
+ this._loadElement(this.config);
+ } else {
+ this._updateElement(this.config);
+ }
+ }
+ }
+ if (changedProps.has("hass")) {
+ try {
+ if (this.hass) {
+ this._element.hass = this.hass;
+ }
+ } catch (e: any) {
+ this._element = undefined;
+ this._elementConfig = undefined;
+ }
+ }
+ }
+
+ if (changedProps.has("hass") || changedProps.has("preview")) {
+ this._updateVisibility();
+ }
+ }
+
+ private _clearMediaQueries() {
+ this._listeners.forEach((unsub) => unsub());
+ this._listeners = [];
+ }
+
+ private _listenMediaQueries() {
+ this._clearMediaQueries();
+ if (!this.config?.visibility) {
+ return;
+ }
+ const conditions = this.config.visibility;
+ const hasOnlyMediaQuery =
+ conditions.length === 1 &&
+ conditions[0].condition === "screen" &&
+ !!conditions[0].media_query;
+
+ this._listeners = attachConditionMediaQueriesListeners(
+ this.config.visibility,
+ (matches) => {
+ this._updateVisibility(hasOnlyMediaQuery && matches);
+ }
+ );
+ }
+
+ private _updateVisibility(forceVisible?: boolean) {
+ if (!this._element || !this.hass) {
+ return;
+ }
+
+ if (this._element.hidden) {
+ this._setElementVisibility(false);
+ return;
+ }
+
+ const visible =
+ forceVisible ||
+ this.preview ||
+ !this.config?.visibility ||
+ checkConditionsMet(this.config.visibility, this.hass);
+ this._setElementVisibility(visible);
+ }
+
+ private _setElementVisibility(visible: boolean) {
+ if (!this._element) return;
+
+ if (this.hidden !== !visible) {
+ this.style.setProperty("display", visible ? "" : "none");
+ this.toggleAttribute("hidden", !visible);
+ fireEvent(this, "heading-badge-visibility-changed", { value: visible });
+ }
+
+ if (!visible && this._element.parentElement) {
+ this.removeChild(this._element);
+ } else if (visible && !this._element.parentElement) {
+ this.appendChild(this._element);
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-badge": HuiHeadingBadge;
+ }
+}
diff --git a/src/panels/lovelace/heading-badges/types.ts b/src/panels/lovelace/heading-badges/types.ts
new file mode 100644
index 0000000000..ac9a4a10cd
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/types.ts
@@ -0,0 +1,25 @@
+import { ActionConfig } from "../../../data/lovelace/config/action";
+import { Condition } from "../common/validate-condition";
+
+export type LovelaceHeadingBadgeConfig = {
+ type?: string;
+ [key: string]: any;
+ visibility?: Condition[];
+};
+
+export interface ErrorBadgeConfig extends LovelaceHeadingBadgeConfig {
+ type: string;
+ error: string;
+ origConfig: LovelaceHeadingBadgeConfig;
+}
+
+export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
+ type?: "entity";
+ entity: string;
+ state_content?: string | string[];
+ icon?: string;
+ show_state?: boolean;
+ show_icon?: boolean;
+ color?: string;
+ tap_action?: ActionConfig;
+}
diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts
index ca9ae97654..7a30f3e1f8 100644
--- a/src/panels/lovelace/types.ts
+++ b/src/panels/lovelace/types.ts
@@ -13,6 +13,7 @@ import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
import { LovelaceCardFeatureConfig } from "./card-features/types";
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
+import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
declare global {
// eslint-disable-next-line
@@ -178,3 +179,27 @@ export interface LovelaceCardFeatureEditor
extends LovelaceGenericElementEditor {
setConfig(config: LovelaceCardFeatureConfig): void;
}
+
+export interface LovelaceHeadingBadge extends HTMLElement {
+ hass?: HomeAssistant;
+ preview?: boolean;
+ setConfig(config: LovelaceHeadingBadgeConfig);
+}
+
+export interface LovelaceHeadingBadgeConstructor
+ extends Constructor {
+ getStubConfig?: (
+ hass: HomeAssistant,
+ stateObj?: HassEntity
+ ) => LovelaceHeadingBadgeConfig;
+ getConfigElement?: () => LovelaceHeadingBadgeEditor;
+ getConfigForm?: () => {
+ schema: HaFormSchema[];
+ assertConfig?: (config: LovelaceCardConfig) => void;
+ };
+}
+
+export interface LovelaceHeadingBadgeEditor
+ extends LovelaceGenericElementEditor {
+ setConfig(config: LovelaceHeadingBadgeConfig): void;
+}
diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts
index 4e3c7e23bd..b04d433e0e 100644
--- a/src/panels/lovelace/views/hui-sections-view.ts
+++ b/src/panels/lovelace/views/hui-sections-view.ts
@@ -249,7 +249,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
cards: [
{
type: "heading",
- heading: "New Section",
+ heading: this.hass!.localize(
+ "ui.panel.lovelace.editor.section.default_section_title"
+ ),
},
],
});
diff --git a/src/panels/profile/ha-change-password-card.ts b/src/panels/profile/ha-change-password-card.ts
index a09ac13a3e..063ca339d8 100644
--- a/src/panels/profile/ha-change-password-card.ts
+++ b/src/panels/profile/ha-change-password-card.ts
@@ -11,6 +11,7 @@ import { customElement, property, state } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-circular-progress";
import "../../components/ha-textfield";
+import "../../components/ha-password-field";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-alert";
@@ -52,47 +53,44 @@ class HaChangePasswordCard extends LitElement {
? html`${this._statusMsg}`
: ""}
-
+ >
${this._currentPassword
- ? html`
-
+ `
+ >`
: ""}
diff --git a/src/resources/codemirror.ts b/src/resources/codemirror.ts
index 3a9fb811df..59396f23f3 100644
--- a/src/resources/codemirror.ts
+++ b/src/resources/codemirror.ts
@@ -277,8 +277,17 @@ export const haSyntaxHighlighting = syntaxHighlighting(haHighlightStyle);
// A folding service for indent-based languages such as YAML.
export const foldingOnIndent = foldService.of((state, from, to) => {
const line = state.doc.lineAt(from);
+
+ // empty lines continue their indentation from surrounding lines
+ if (!line.length || !line.text.trim().length) {
+ return null;
+ }
+
+ let onlyEmptyNext = true;
+
const lineCount = state.doc.lines;
const indent = line.text.search(/\S|$/); // Indent level of the first line
+
let foldStart = from; // Start of the fold
let foldEnd = to; // End of the fold
@@ -291,7 +300,15 @@ export const foldingOnIndent = foldService.of((state, from, to) => {
const nextIndent = nextLine.text.search(/\S|$/); // Indent level of the next line
// If the next line is on a deeper indent level, add it to the fold
- if (nextIndent > indent) {
+ // empty lines continue their indentation from surrounding lines
+ if (
+ !nextLine.length ||
+ !nextLine.text.trim().length ||
+ nextIndent > indent
+ ) {
+ if (onlyEmptyNext) {
+ onlyEmptyNext = nextLine.text.trim().length === 0;
+ }
// include this line in the fold and continue
foldEnd = nextLine.to;
} else {
@@ -301,7 +318,10 @@ export const foldingOnIndent = foldService.of((state, from, to) => {
}
// Don't create fold if it's a single line
- if (state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number) {
+ if (
+ onlyEmptyNext ||
+ state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number
+ ) {
return null;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index 20677c99f7..005c8ef552 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -3093,6 +3093,13 @@
"picker": "When someone (or something) enters or leaves a zone.",
"full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}"
}
+ },
+ "list": {
+ "label": "List",
+ "description": {
+ "no_trigger": "When any trigger matches",
+ "full": "When any of {count} {count, plural,\n one {trigger}\n other {triggers}\n} triggers"
+ }
}
}
},
@@ -5669,7 +5676,8 @@
"section": {
"add_badge": "Add badge",
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
- "create_section": "Create section"
+ "create_section": "Create section",
+ "default_section_title": "New section"
},
"delete_section": {
"title": "Delete section",
@@ -6402,7 +6410,7 @@
"row": "Entity row editor",
"feature": "Feature editor",
"element": "Element editor",
- "heading-entity": "Entity editor",
+ "heading-badge": "Heading badge editor",
"element_type": "{type} element editor"
}
}
@@ -6949,7 +6957,7 @@
"no_issue": "No issue",
"issues": {
"units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.",
- "unsupported_state_class": "The state class ''{state_class}'' of this entity is not supported.",
+ "state_class_removed": "This entity no longer has a state class",
"entity_not_recorded": "This entity is excluded from being recorded.",
"entity_no_longer_recorded": "This entity is no longer being recorded.",
"no_state": "There is no state available for this entity."
@@ -6978,14 +6986,14 @@
"info_text_3_link": "See the recorder documentation for more information.",
"info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now."
},
- "unsupported_state_class": {
- "title": "Unsupported state class",
- "info_text_1": "The state class of ''{name}'' ({statistic_id}), ''{state_class}'', is not supported.",
+ "state_class_removed": {
+ "title": "The entity no longer has a state class",
+ "info_text_1": "We have generated statistics for ''{name}'' ({statistic_id}) in the past, but it no longer has a state class, therefore, we cannot track long term statistics for it anymore.",
"info_text_2": "Statistics cannot be generated until this entity has a supported state class.",
- "info_text_3": "If this state class was provided by an integration, this is a bug. Please report an issue.",
- "info_text_4": "If you have set this state class yourself, please correct it.",
+ "info_text_3": "If the state class was previously provided by an integration, this might be a bug. Please report an issue.",
+ "info_text_4": "If you previously set the state class yourself, please correct it.",
"info_text_4_link": "The different state classes and when to use which can be found in the developer documentation.",
- "info_text_5": "If the state class has permanently changed, you may want to delete the long term statistics of it from your database.",
+ "info_text_5": "If the state class has permanently been removed, you may want to delete the long term statistics of it from your database.",
"info_text_6": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?"
},
"units_changed": {