From 49576189af4a7122d50eaccd7ff2ce92af25f879 Mon Sep 17 00:00:00 2001
From: selvalt7 <52136675+selvalt7@users.noreply.github.com>
Date: Fri, 27 Sep 2024 10:00:36 +0200
Subject: [PATCH 01/15] Use localizeValue in ha-form-expandable and
ha-form-grid (#22114)
Pass localizeValue to ha-form-expandable and ha-form-grid
---
src/components/ha-form/ha-form-expandable.ts | 5 +++++
src/components/ha-form/ha-form-grid.ts | 5 +++++
src/components/ha-form/ha-form.ts | 1 +
3 files changed, 11 insertions(+)
diff --git a/src/components/ha-form/ha-form-expandable.ts b/src/components/ha-form/ha-form-expandable.ts
index 1ffa6339b568..a751a5acd244 100644
--- a/src/components/ha-form/ha-form-expandable.ts
+++ b/src/components/ha-form/ha-form-expandable.ts
@@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
options?: { path?: string[] }
) => string;
+ @property({ attribute: false }) public localizeValue?: (
+ key: string
+ ) => string;
+
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`
Date: Fri, 27 Sep 2024 11:05:30 +0200
Subject: [PATCH 02/15] Improve statistics issues (#22110)
---
src/data/recorder.ts | 10 +++++-----
.../statistics/developer-tools-statistics.ts | 10 ++++------
.../statistics/fix-statistics.ts | 19 +++++++++----------
src/translations/en.json | 14 +++++++-------
4 files changed, 25 insertions(+), 28 deletions(-)
diff --git a/src/data/recorder.ts b/src/data/recorder.ts
index f4ab0cb80498..1b89af283492 100644
--- a/src/data/recorder.ts
+++ b/src/data/recorder.ts
@@ -50,7 +50,7 @@ export interface StatisticsMetaData {
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
"entity_not_recorded",
"entity_no_longer_recorded",
- "unsupported_state_class",
+ "state_class_removed",
"units_changed",
"no_state",
];
@@ -59,7 +59,7 @@ export type StatisticsValidationResult =
| StatisticsValidationResultNoState
| StatisticsValidationResultEntityNotRecorded
| StatisticsValidationResultEntityNoLongerRecorded
- | StatisticsValidationResultUnsupportedStateClass
+ | StatisticsValidationResultStateClassRemoved
| StatisticsValidationResultUnitsChanged;
export interface StatisticsValidationResultNoState {
@@ -77,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
data: { statistic_id: string };
}
-export interface StatisticsValidationResultUnsupportedStateClass {
- type: "unsupported_state_class";
- data: { statistic_id: string; state_class: string };
+export interface StatisticsValidationResultStateClassRemoved {
+ type: "state_class_removed";
+ data: { statistic_id: string };
}
export interface StatisticsValidationResultUnitsChanged {
diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
index a169eeb1799a..8baafaf90975 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 8dd140e718d7..0329cf77be97 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/translations/en.json b/src/translations/en.json
index 20677c99f7d9..c885fc97a003 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -6949,7 +6949,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 +6978,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": {
From 570ad38bacab71c14aebc438b08adcecb1c43140 Mon Sep 17 00:00:00 2001
From: Wendelin <12148533+wendevlin@users.noreply.github.com>
Date: Fri, 27 Sep 2024 11:10:33 +0200
Subject: [PATCH 03/15] Fix automation trigger condition and triggers
description (#22122)
* Fix config.triggers in automation-contition-trigger
* Fix config.triggers for automation triggers description
---
.../condition/types/ha-automation-condition-trigger.ts | 4 ++--
src/panels/config/automation/manual-automation-editor.ts | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts
index d9275dc705ac..713e665abe4f 100644
--- a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts
+++ b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts
@@ -94,8 +94,8 @@ export class HaTriggerCondition extends LitElement {
private _automationUpdated(config?: AutomationConfig) {
const seenIds = new Set();
- this._triggers = config?.trigger
- ? ensureArray(config.trigger).filter(
+ this._triggers = config?.triggers
+ ? ensureArray(config.triggers).filter(
(t) => t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
)
: [];
diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts
index 23855571d13f..741ecb99d25b 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"
From ac9654c1de2532b3fc375f0df0450953282bc628 Mon Sep 17 00:00:00 2001
From: Paul Bottein
Date: Fri, 27 Sep 2024 11:19:19 +0200
Subject: [PATCH 04/15] Add heading card when creating a new view (#22123)
---
.../editor/view-editor/hui-dialog-edit-view.ts | 14 +++++++++++++-
src/panels/lovelace/views/hui-sections-view.ts | 4 +++-
src/translations/en.json | 3 ++-
3 files changed, 18 insertions(+), 3 deletions(-)
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 a14e5b35a65a..2d4c3c6bd3fa 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/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts
index 4e3c7e23bdbd..b04d433e0e0f 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/translations/en.json b/src/translations/en.json
index c885fc97a003..ee8a3f15395e 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -5669,7 +5669,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",
From c721afa13759833ae6c7c2db1887dc0bff7eb849 Mon Sep 17 00:00:00 2001
From: Wendelin <12148533+wendevlin@users.noreply.github.com>
Date: Fri, 27 Sep 2024 11:37:07 +0200
Subject: [PATCH 05/15] Fix matter device actions (#22117)
* Fix matter device actions when matter integration loads forever
* Fix matter device-actions types path
* Move getMatterDeviceActions inside getDeviceActions in device page
---
.../matter/device-actions.ts | 33 ++++++++++++++-----
.../config/devices/ha-config-device-page.ts | 9 +++--
2 files changed, 31 insertions(+), 11 deletions(-)
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 6fad700a32fa..2be86b5ee48b 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 1ea9c90b1cdd..39372e9340d3 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;
From 468660d23547a726ee4410ac154ebca8fe70209d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joakim=20S=C3=B8rensen?=
Date: Fri, 27 Sep 2024 12:31:48 +0200
Subject: [PATCH 06/15] Adjust username handling in the cloud panel register
and login flows (#22118)
* Use lowercase when registering
* Fallback to lowercase username if usernotfound is recieved
* Adjust resend
* handle reset password
* limit with else
* return early
---
.../forgot-password/cloud-forgot-password.ts | 44 +++++----
src/panels/config/cloud/login/cloud-login.ts | 90 ++++++++++---------
.../config/cloud/register/cloud-register.ts | 37 +++++---
3 files changed, 98 insertions(+), 73 deletions(-)
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 b5933a0fdf00..229059bcf8e7 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 50571b07a17e..d31f7c725016 100644
--- a/src/panels/config/cloud/login/cloud-login.ts
+++ b/src/panels/config/cloud/login/cloud-login.ts
@@ -227,53 +227,61 @@ export class CloudLogin extends LitElement {
this._requestInProgress = true;
- try {
- const result = await cloudLogin(this.hass, email, password);
- fireEvent(this, "ha-refresh-cloud-status");
- this.email = "";
- this._password = "";
- if (result.cloud_pipeline) {
- if (
- await showConfirmationDialog(this, {
+ const doLogin = async (username: string) => {
+ 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.cloud_pipeline_title"
- ),
- text: this.hass.localize(
- "ui.panel.config.cloud.login.cloud_pipeline_text"
+ "ui.panel.config.cloud.login.alert_password_change_required"
),
- })
- ) {
- setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
+ });
+ navigate("/config/cloud/forgot-password");
+ return;
+ }
+ if (errCode === "usernotfound" && username !== username.toLowerCase()) {
+ await doLogin(username.toLowerCase());
+ return;
+ }
+
+ 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";
}
- }
- } 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;
-
- 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 dfab49cfbe26..65aea4bf0442 100644
--- a/src/panels/config/cloud/register/cloud-register.ts
+++ b/src/panels/config/cloud/register/cloud-register.ts
@@ -197,9 +197,6 @@ export class CloudRegister extends LitElement {
const emailField = this._emailField;
const passwordField = this._passwordField;
- const email = emailField.value;
- const password = passwordField.value;
-
if (!emailField.reportValidity()) {
passwordField.reportValidity();
emailField.focus();
@@ -211,6 +208,9 @@ export class CloudRegister extends LitElement {
return;
}
+ const email = emailField.value.toLowerCase();
+ const password = passwordField.value;
+
this._requestInProgress = true;
try {
@@ -229,22 +229,31 @@ export class CloudRegister extends LitElement {
private async _handleResendVerifyEmail() {
const emailField = this._emailField;
- const email = emailField.value;
-
if (!emailField.reportValidity()) {
emailField.focus();
return;
}
- try {
- await cloudResendVerification(this.hass, email);
- this._verificationEmailSent(email);
- } catch (err: any) {
- this._error =
- err && err.body && err.body.message
- ? err.body.message
- : "Unknown error";
- }
+ const email = emailField.value;
+
+ const doResend = async (username: string) => {
+ 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) {
From a92dab46c2ef2478d45082d57d4723f76324cb6d Mon Sep 17 00:00:00 2001
From: Paul Bottein
Date: Fri, 27 Sep 2024 12:33:15 +0200
Subject: [PATCH 07/15] Allow different types of heading badges (#22109)
* Allow different type of heading item
* Update editor
* Migrate entities to items
* Rename support for string entity
* Refactor
* Rename to badges and add error state
* Update font weight
* Feedback
* Feedback
---
src/components/ha-heading-badge.ts | 58 ++++
.../cards/heading/hui-heading-entity.ts | 248 ------------------
src/panels/lovelace/cards/hui-heading-card.ts | 31 ++-
src/panels/lovelace/cards/types.ts | 16 +-
.../create-element/create-element-base.ts | 33 +++
.../create-heading-badge-element.ts | 22 ++
...editor.ts => hui-heading-badges-editor.ts} | 126 ++++-----
.../hui-heading-card-editor.ts | 55 ++--
.../hui-entity-heading-badge-editor.ts} | 28 +-
.../hui-heading-badge-element-editor.ts | 42 +++
.../hui-heading-entity-element-editor.ts | 20 --
.../lovelace/editor/hui-sub-element-editor.ts | 8 +-
src/panels/lovelace/editor/types.ts | 6 +-
.../hui-entity-heading-badge.ts | 177 +++++++++++++
.../heading-badges/hui-error-heading-badge.ts | 94 +++++++
.../heading-badges/hui-heading-badge.ts | 202 ++++++++++++++
src/panels/lovelace/heading-badges/types.ts | 25 ++
src/panels/lovelace/types.ts | 25 ++
src/translations/en.json | 2 +-
19 files changed, 824 insertions(+), 394 deletions(-)
create mode 100644 src/components/ha-heading-badge.ts
delete mode 100644 src/panels/lovelace/cards/heading/hui-heading-entity.ts
create mode 100644 src/panels/lovelace/create-element/create-heading-badge-element.ts
rename src/panels/lovelace/editor/config-elements/{hui-entities-editor.ts => hui-heading-badges-editor.ts} (67%)
rename src/panels/lovelace/editor/{heading-entity/hui-heading-entity-editor.ts => heading-badge-editor/hui-entity-heading-badge-editor.ts} (92%)
create mode 100644 src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts
delete mode 100644 src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
create mode 100644 src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
create mode 100644 src/panels/lovelace/heading-badges/hui-error-heading-badge.ts
create mode 100644 src/panels/lovelace/heading-badges/hui-heading-badge.ts
create mode 100644 src/panels/lovelace/heading-badges/types.ts
diff --git a/src/components/ha-heading-badge.ts b/src/components/ha-heading-badge.ts
new file mode 100644
index 000000000000..adfa5dd79a2d
--- /dev/null
+++ b/src/components/ha-heading-badge.ts
@@ -0,0 +1,58 @@
+import { css, CSSResultGroup, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators";
+import { ifDefined } from "lit/directives/if-defined";
+
+type HeadingBadgeType = "text" | "button";
+
+@customElement("ha-heading-badge")
+export class HaBadge extends LitElement {
+ @property() public type: HeadingBadgeType = "text";
+
+ protected render() {
+ return html`
+
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ :host {
+ color: var(--secondary-text-color);
+ }
+ [role="button"] {
+ cursor: pointer;
+ }
+ .heading-badge {
+ display: flex;
+ flex-direction: row;
+ white-space: nowrap;
+ align-items: center;
+ gap: 3px;
+ font-family: Roboto;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: 0.1px;
+ --mdc-icon-size: 14px;
+ }
+ ::slotted([slot="icon"]) {
+ --ha-icon-display: block;
+ color: var(--icon-color, inherit);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-heading-badge": HaBadge;
+ }
+}
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 8b12950e7fde..000000000000
--- 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 63bad9a61fec..ee972ed1344c 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 {
@@ -186,7 +199,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
font-weight: 500;
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 e3d3257346d4..3c21617dfeaa 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 33aaecb0afc8..7346e63b620c 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 000000000000..e45bb6f14fef
--- /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 e47abf3280c4..c102f3d37ed2 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 727a4223d790..69c89e6bc804 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 6d0c4f3d5136..b1446d4a1254 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 000000000000..dadef34f8b01
--- /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 ada27a39d28b..000000000000
--- 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 1202d598a314..0d01341abc1c 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 a409f00729e2..c62c7595ab66 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/heading-badges/hui-entity-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
new file mode 100644
index 000000000000..b6084a846dc8
--- /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 000000000000..921059276359
--- /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 000000000000..92c5b04ca87e
--- /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 000000000000..ac9a4a10cd62
--- /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 ca9ae976549d..7a30f3e1f88c 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/translations/en.json b/src/translations/en.json
index ee8a3f15395e..e68d165f7736 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -6403,7 +6403,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"
}
}
From 4e8b58cd6c14f8791a71a68b8a3cac2e761337ad Mon Sep 17 00:00:00 2001
From: Bram Kragten
Date: Fri, 27 Sep 2024 12:34:28 +0200
Subject: [PATCH 08/15] Add password field element (#22121)
* Add password field element
* Update ha-password-field.ts
---
.../components/supervisor-backup-content.ts | 11 +-
.../dialogs/network/dialog-hassio-network.ts | 10 +-
src/components/ha-password-field.ts | 160 ++++++++++++++++++
src/components/ha-textfield.ts | 20 ++-
.../dialog-add-application-credential.ts | 8 +-
src/panels/config/cloud/login/cloud-login.ts | 6 +-
.../config/cloud/register/cloud-register.ts | 6 +-
.../config/network/supervisor-network.ts | 14 +-
src/panels/config/users/dialog-add-user.ts | 19 ++-
src/panels/profile/ha-change-password-card.ts | 16 +-
10 files changed, 219 insertions(+), 51 deletions(-)
create mode 100644 src/components/ha-password-field.ts
diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts
index ddbde0f4f386..31d2f3b78892 100644
--- a/hassio/src/components/supervisor-backup-content.ts
+++ b/hassio/src/components/supervisor-backup-content.ts
@@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
+import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
@@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupHasPassword
? html`
-
-
+
${!this.backup
- ? html`
- `
+ `
: ""}
`
: ""}
diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts
index 66b381e0c08a..4def6de2a12a 100644
--- a/hassio/src/dialogs/network/dialog-hassio-network.ts
+++ b/hassio/src/dialogs/network/dialog-hassio-network.ts
@@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
-import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
+import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
+import "../../../../src/components/ha-textfield";
+import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,
@@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
-import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -246,9 +247,8 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
-
-
+
`
: ""}
`
diff --git a/src/components/ha-password-field.ts b/src/components/ha-password-field.ts
new file mode 100644
index 000000000000..86127030533f
--- /dev/null
+++ b/src/components/ha-password-field.ts
@@ -0,0 +1,160 @@
+import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
+import { mdiEye, mdiEyeOff } from "@mdi/js";
+import { LitElement, css, html } from "lit";
+import { customElement, eventOptions, property, state } from "lit/decorators";
+import { HomeAssistant } from "../types";
+import "./ha-icon-button";
+import "./ha-textfield";
+
+@customElement("ha-password-field")
+export class HaPasswordField extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ type: Boolean }) public invalid?: boolean;
+
+ @property({ attribute: "error-message" }) public errorMessage?: string;
+
+ @property({ type: Boolean }) public icon = false;
+
+ @property({ type: Boolean }) public iconTrailing = false;
+
+ @property() public autocomplete?: string;
+
+ @property() public autocorrect?: string;
+
+ @property({ attribute: "input-spellcheck" })
+ public inputSpellcheck?: string;
+
+ @property({ type: String }) value = "";
+
+ @property({ type: String }) placeholder = "";
+
+ @property({ type: String }) label = "";
+
+ @property({ type: Boolean, reflect: true }) disabled = false;
+
+ @property({ type: Boolean }) required = false;
+
+ @property({ type: Number }) minLength = -1;
+
+ @property({ type: Number }) maxLength = -1;
+
+ @property({ type: Boolean, reflect: true }) outlined = false;
+
+ @property({ type: String }) helper = "";
+
+ @property({ type: Boolean }) validateOnInitialRender = false;
+
+ @property({ type: String }) validationMessage = "";
+
+ @property({ type: Boolean }) autoValidate = false;
+
+ @property({ type: String }) pattern = "";
+
+ @property({ type: Number }) size: number | null = null;
+
+ @property({ type: Boolean }) helperPersistent = false;
+
+ @property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
+ false;
+
+ @property({ type: Boolean }) endAligned = false;
+
+ @property({ type: String }) prefix = "";
+
+ @property({ type: String }) suffix = "";
+
+ @property({ type: String }) name = "";
+
+ @property({ type: String, attribute: "input-mode" })
+ inputMode!: string;
+
+ @property({ type: Boolean }) readOnly = false;
+
+ @property({ type: String }) autocapitalize = "";
+
+ @state() private _unmaskedPassword = false;
+
+ protected render() {
+ return html``}
+ @input=${this._handleInputChange}
+ >
+
`;
+ }
+
+ private _toggleUnmaskedPassword(): void {
+ this._unmaskedPassword = !this._unmaskedPassword;
+ }
+
+ @eventOptions({ passive: true })
+ private _handleInputChange(ev) {
+ this.value = ev.target.value;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ ha-textfield {
+ width: 100%;
+ }
+ ha-icon-button {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ inset-inline-start: initial;
+ inset-inline-end: 8px;
+ --mdc-icon-button-size: 40px;
+ --mdc-icon-size: 20px;
+ color: var(--secondary-text-color);
+ direction: var(--direction);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-password-field": HaPasswordField;
+ }
+}
diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts
index bf45e85a1fb2..4b2df4c09fea 100644
--- a/src/components/ha-textfield.ts
+++ b/src/components/ha-textfield.ts
@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-textfield")
export class HaTextField extends TextFieldBase {
- @property({ type: Boolean }) public invalid = false;
+ @property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
- (changedProperties.has("invalid") &&
- (this.invalid || changedProperties.get("invalid") !== undefined)) ||
+ changedProperties.has("invalid") ||
changedProperties.has("errorMessage")
) {
this.setCustomValidity(
- this.invalid ? this.errorMessage || "Invalid" : ""
+ this.invalid
+ ? this.errorMessage || this.validationMessage || "Invalid"
+ : ""
);
- this.reportValidity();
+ if (
+ this.invalid ||
+ this.validateOnInitialRender ||
+ (changedProperties.has("invalid") &&
+ changedProperties.get("invalid") !== undefined)
+ ) {
+ // Only report validity if the field is invalid or the invalid state has changed from
+ // true to false to prevent setting empty required fields to invalid on first render
+ this.reportValidity();
+ }
}
if (changedProperties.has("autocomplete")) {
if (this.autocomplete) {
diff --git a/src/panels/config/application_credentials/dialog-add-application-credential.ts b/src/panels/config/application_credentials/dialog-add-application-credential.ts
index e30e494b4730..d5e9ca4c8481 100644
--- a/src/panels/config/application_credentials/dialog-add-application-credential.ts
+++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts
@@ -5,12 +5,13 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
+import "../../../components/ha-button";
import "../../../components/ha-circular-progress";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-markdown";
+import "../../../components/ha-password-field";
import "../../../components/ha-textfield";
-import "../../../components/ha-button";
import {
ApplicationCredential,
ApplicationCredentialsConfig,
@@ -208,11 +209,10 @@ export class DialogAddApplicationCredential extends LitElement {
)}
helperPersistent
>
-
+ >
${this._loading
? html`
diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts
index d31f7c725016..c617dfd2b257 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"
)}
>
-
+ >
-
+ >
-
+
`
: ""}
`
diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts
index 640f1aa1047d..7e7e9e70cdd7 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/profile/ha-change-password-card.ts b/src/panels/profile/ha-change-password-card.ts
index a09ac13a3e6a..063ca339d82e 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`
-
+ `
+ >`
: ""}
From 442a8f11a7fda58712de17a5e7e1b013cd81d6c9 Mon Sep 17 00:00:00 2001
From: Paul Bottein
Date: Fri, 27 Sep 2024 13:45:18 +0200
Subject: [PATCH 09/15] Improve heading card style and add theme variables
(#22129)
improve heading card style and add theme variables
---
src/panels/lovelace/cards/hui-heading-card.ts | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/src/panels/lovelace/cards/hui-heading-card.ts b/src/panels/lovelace/cards/hui-heading-card.ts
index ee972ed1344c..5058f2560ce7 100644
--- a/src/panels/lovelace/cards/hui-heading-card.ts
+++ b/src/panels/lovelace/cards/hui-heading-card.ts
@@ -171,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 {
@@ -194,10 +194,13 @@ 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);
}
.badges {
display: flex;
From 1c12c2b714ca657c912e081a10422700a63c8cf1 Mon Sep 17 00:00:00 2001
From: Bram Kragten
Date: Fri, 27 Sep 2024 14:18:48 +0200
Subject: [PATCH 10/15] Fix codemirror fold for empty lines (#22130)
---
src/resources/codemirror.ts | 24 ++++++++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/resources/codemirror.ts b/src/resources/codemirror.ts
index 3a9fb811df66..59396f23f369 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;
}
From 94e321a3646e55aa7fbd1ac6346ea9f6620d3143 Mon Sep 17 00:00:00 2001
From: Paul Bottein
Date: Fri, 27 Sep 2024 16:56:22 +0200
Subject: [PATCH 11/15] Add UI support for trigger list (#22133)
* Add UI support for trigger list
* Update gallery
* Fix gallery
---
demo/src/stubs/config.ts | 9 +++
demo/src/stubs/tags.ts | 6 ++
.../src/pages/automation/describe-trigger.ts | 6 ++
.../src/pages/automation/editor-trigger.ts | 11 ++++
src/components/trace/hat-script-graph.ts | 2 +-
src/data/automation.ts | 3 +-
src/data/automation_i18n.ts | 15 +++++
src/data/trigger.ts | 7 +-
.../types/ha-automation-condition-trigger.ts | 38 +++++++----
.../trigger/ha-automation-trigger-row.ts | 64 ++++++++++++-------
.../trigger/ha-automation-trigger.ts | 15 ++++-
.../types/ha-automation-trigger-list.ts | 54 ++++++++++++++++
src/translations/en.json | 7 ++
13 files changed, 194 insertions(+), 43 deletions(-)
create mode 100644 demo/src/stubs/config.ts
create mode 100644 demo/src/stubs/tags.ts
create mode 100644 src/panels/config/automation/trigger/types/ha-automation-trigger-list.ts
diff --git a/demo/src/stubs/config.ts b/demo/src/stubs/config.ts
new file mode 100644
index 000000000000..73beb19e1edb
--- /dev/null
+++ b/demo/src/stubs/config.ts
@@ -0,0 +1,9 @@
+import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+
+export const mockConfig = (hass: MockHomeAssistant) => {
+ hass.mockWS("validate_config", () => ({
+ actions: { valid: true },
+ conditions: { valid: true },
+ triggers: { valid: true },
+ }));
+};
diff --git a/demo/src/stubs/tags.ts b/demo/src/stubs/tags.ts
new file mode 100644
index 000000000000..0634d4009333
--- /dev/null
+++ b/demo/src/stubs/tags.ts
@@ -0,0 +1,6 @@
+import { Tag } from "../../../src/data/tag";
+import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+
+export const mockTags = (hass: MockHomeAssistant) => {
+ hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
+};
diff --git a/gallery/src/pages/automation/describe-trigger.ts b/gallery/src/pages/automation/describe-trigger.ts
index f8b8117a83f8..8c462060b06d 100644
--- a/gallery/src/pages/automation/describe-trigger.ts
+++ b/gallery/src/pages/automation/describe-trigger.ts
@@ -58,6 +58,12 @@ const triggers = [
command: ["Turn on the lights", "Turn the lights on"],
},
{ trigger: "event", event_type: "homeassistant_started" },
+ {
+ triggers: [
+ { trigger: "state", entity_id: "light.kitchen", to: "on" },
+ { trigger: "state", entity_id: "light.kitchen", to: "off" },
+ ],
+ },
];
const initialTrigger: Trigger = {
diff --git a/gallery/src/pages/automation/editor-trigger.ts b/gallery/src/pages/automation/editor-trigger.ts
index a138a46e9efa..1d94c5676c78 100644
--- a/gallery/src/pages/automation/editor-trigger.ts
+++ b/gallery/src/pages/automation/editor-trigger.ts
@@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
+import { mockConfig } from "../../../../demo/src/stubs/config";
+import { mockTags } from "../../../../demo/src/stubs/tags";
+import { mockAuth } from "../../../../demo/src/stubs/auth";
import type { Trigger } from "../../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
@@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
+import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
@@ -116,6 +120,10 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
},
],
},
+ {
+ name: "Trigger list",
+ triggers: [{ ...HaTriggerList.defaultConfig }],
+ },
];
@customElement("demo-automation-editor-trigger")
@@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
+ mockConfig(hass);
+ mockTags(hass);
+ mockAuth(hass);
}
protected render(): TemplateResult {
diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts
index 80ad22cddb10..267e046168ec 100644
--- a/src/components/trace/hat-script-graph.ts
+++ b/src/components/trace/hat-script-graph.ts
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
- .notEnabled=${config.enabled === false}
+ .notEnabled=${"enabled" in config && config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${track ? "0" : "-1"}
>
diff --git a/src/data/automation.ts b/src/data/automation.ts
index ffd63f67e94a..f08170c2cc86 100644
--- a/src/data/automation.ts
+++ b/src/data/automation.ts
@@ -206,7 +206,8 @@ export type Trigger =
| TemplateTrigger
| EventTrigger
| DeviceTrigger
- | CalendarTrigger;
+ | CalendarTrigger
+ | TriggerList;
interface BaseCondition {
condition: string;
diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts
index 474db8345c5b..c6bbbbeb77c5 100644
--- a/src/data/automation_i18n.ts
+++ b/src/data/automation_i18n.ts
@@ -22,6 +22,7 @@ import {
formatListWithAnds,
formatListWithOrs,
} from "../common/string/format-list";
+import { isTriggerList } from "./trigger";
const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type";
@@ -98,6 +99,20 @@ const tryDescribeTrigger = (
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
+ if (isTriggerList(trigger)) {
+ const triggers = ensureArray(trigger.triggers);
+
+ if (!triggers || triggers.length === 0) {
+ return hass.localize(
+ `${triggerTranslationBaseKey}.list.description.no_trigger`
+ );
+ }
+ const count = triggers.length;
+ return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
+ count: count,
+ });
+ }
+
if (trigger.alias && !ignoreAlias) {
return trigger.alias;
}
diff --git a/src/data/trigger.ts b/src/data/trigger.ts
index c68fed612bce..88877e722fdc 100644
--- a/src/data/trigger.ts
+++ b/src/data/trigger.ts
@@ -5,6 +5,7 @@ import {
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
+ mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
@@ -21,7 +22,7 @@ import {
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
-import { AutomationElementGroup } from "./automation";
+import { AutomationElementGroup, Trigger, TriggerList } from "./automation";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
@@ -41,6 +42,7 @@ export const TRIGGER_ICONS = {
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
+ list: mdiFormatListBulleted,
};
export const TRIGGER_GROUPS: AutomationElementGroup = {
@@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
},
},
} as const;
+
+export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
+ "triggers" in trigger;
diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts
index 713e665abe4f..4f8462b9f0ec 100644
--- a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts
+++ b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts
@@ -15,6 +15,21 @@ import type {
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
+const getTriggersIds = (triggers: Trigger[]): string[] => {
+ const ids: Set = new Set();
+ triggers.forEach((trigger) => {
+ if ("triggers" in trigger) {
+ const newIds = getTriggersIds(ensureArray(trigger.triggers));
+ for (const id of newIds) {
+ ids.add(id);
+ }
+ } else if (trigger.id) {
+ ids.add(trigger.id);
+ }
+ });
+ return Array.from(ids);
+};
+
@customElement("ha-automation-condition-trigger")
export class HaTriggerCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -23,7 +38,7 @@ export class HaTriggerCondition extends LitElement {
@property({ type: Boolean }) public disabled = false;
- @state() private _triggers: Trigger[] = [];
+ @state() private _triggerIds: string[] = [];
private _unsub?: UnsubscribeFunc;
@@ -35,14 +50,14 @@ export class HaTriggerCondition extends LitElement {
}
private _schema = memoizeOne(
- (triggers: Trigger[]) =>
+ (triggerIds: string[]) =>
[
{
name: "id",
selector: {
select: {
multiple: true,
- options: triggers.map((trigger) => trigger.id!),
+ options: triggerIds,
},
},
required: true,
@@ -65,13 +80,13 @@ export class HaTriggerCondition extends LitElement {
}
protected render() {
- if (!this._triggers.length) {
+ if (!this._triggerIds.length) {
return this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
);
}
- const schema = this._schema(this._triggers);
+ const schema = this._schema(this._triggerIds);
return html`
t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
- )
+ this._triggerIds = config?.triggers
+ ? getTriggersIds(ensureArray(config.triggers))
: [];
}
@@ -106,12 +118,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/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
index 31e996fab0af..174233692e2b 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 39da68e02992..5fe2a100a186 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 000000000000..8a7481390868
--- /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/translations/en.json b/src/translations/en.json
index e68d165f7736..005c8ef5526b 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"
+ }
}
}
},
From 7a607637869583f520cfeb44c98da4f3be901a60 Mon Sep 17 00:00:00 2001
From: Bram Kragten
Date: Fri, 27 Sep 2024 16:56:38 +0200
Subject: [PATCH 12/15] Voice setup feedback (#22134)
* Voice setup feedback
* Update voice-assistant-setup-step-check.ts
---
public/static/images/logo_nabu_casa.png | Bin 0 -> 2956 bytes
src/dialogs/voice-assistant-setup/styles.ts | 20 ++-
.../voice-assistant-setup-dialog.ts | 55 ++++++-
.../voice-assistant-setup-step-addons.ts | 54 ++++---
.../voice-assistant-setup-step-area.ts | 2 +-
...e-assistant-setup-step-change-wake-word.ts | 5 +-
.../voice-assistant-setup-step-check.ts | 17 +--
.../voice-assistant-setup-step-cloud.ts | 20 ++-
.../voice-assistant-setup-step-pipeline.ts | 10 +-
.../voice-assistant-setup-step-success.ts | 138 ++++++++++++------
.../voice-assistant-setup-step-update.ts | 16 +-
.../voice-assistant-setup-step-wake-word.ts | 4 +-
12 files changed, 217 insertions(+), 124 deletions(-)
create mode 100644 public/static/images/logo_nabu_casa.png
diff --git a/public/static/images/logo_nabu_casa.png b/public/static/images/logo_nabu_casa.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea3817f2de52286ee39b685dec0cdb34a4db453a
GIT binary patch
literal 2956
zcmV;73v={|P)001xu1^@s6mZ@=W00009a7bBm000XU
z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP~s0U4__pv^m7wTCLU?
z+nXOhenc@~4XzbhwnGrp1G-@Rv$kTzudI5=dqC*kN0}w8<
z0$_dE9f5o#bAT;I{eur5J`AnFPNFsfQgVSPuWU+`Qp8(Q{|V}U&LZ|_kcOw$VCPT^
z0q@_xe~jVv48;zJcM9U2*#-a-l6>bj9oomZ*B_0cvRl|X0Y$zcim?bWwhaS3>}y!&
zJ)hJ`!iNtZcA@W5G8@|TcUnC_TaTbNJY^YD`PKE3*l$}3|I*ulwvH?#
zymm~|qYs|3hi81ub6X`Ke_Rc_TPK1?!m;jMio0wrpe_4;ThMWR9Ai7}^71lQ
z|48Cry3eTRFwYB~YTF`Y?zjE@eby85J9v)$)VZNe4aT@_TY@(n)*^*)J9aE3LKkn}
zx$OpMg#Bq10d*a%`kV*os}Z(=HYfR9A6AAvD+r2fI*QS$2R7B^0v<;L`8LxH@&-S^
z_}#mAV;%oX*VQ%zl7xH_P&|gDIl<7d5#2Kn=<`IAE~+rVP?sA%*#{kc?pp!S4d$x{
zo7@=9IsIo
zAvR8x^{r-5-$L7{+;^%TPV$1zauvJO{j3D=vd^6GlI$@*U=%vG0^o>7Qp0FGl0gxw
zzL!=Z)=EI$9F6JgV
z=Fw#5=4@
zN^A;x9&Vwg@ak#@RRZTH3sHV-If1^K4YMHIbCBd*LvJGWQ0G(22})A>A+^oaDXL(P
z9z6=IBK$T4d*lm>%u=v3Z{NOMTZd-B+qZ9H?(uvV3}TRksLdkAZ#=|kL&AjiDP|m(gnEd3UdS`dCHOx-~Z>!moI_sFd!j@
znq}b7axMu`t4oH4k?%y#fzTBsrRGA`oNMOm*RMS*=#rt!4PFm_h9rw8;|(_IrGfAA
zlC>VHz0n*26`A7PCAIs=HVqDcGDf;qGpHw{qXupkBr7}}me%h{`aGVGFISM)CkP$#
z9N$fJQjl+UoGMusgNAecQVn?TKT#LkIdQHcmT}EsrUTO4mW!pZyF~iT6KK1)&;`fZ_95~+v!ILX?!6ee+jxpQ&h4;gt~?{Ms{NeC!2p2o)z7H@3T
zB9gzW5b(n$Vq9<1o76d4j3Ku10ts+;i9$YyTeog;C}44GhSMsylsY<7XJ@%KPkm;a
zaha(N(V}w+0i7AoG1)2x$%i4YZv_EKN|FWXxk*BiBOxVQ`W{ktd+X}g+_ky6>;~IK
z+cV=|Z-V{T2zRW()aSGE9L@g0orqCk;8e>94VzDeR3orBZ43aKW!?4+O?_VTy3RrZNj{7As=8?Py&)A
zQBesCm`R3b*fINxW{wxSN#
zrocN~IKiJjW*26*=y#?ST$tVRaBt0lyFnZ*JK;r8yU_X4aHLkO<=pQ_0#WMb4H4t@
zzT>?}LXv`yNsvH?Vply4GX@n{PWYi>i^9Sj0Rt-lXF=}j-0wQ@jbz*L_x!OwljOqsjbYq(jAQ
z!SC1REP|DgV(}E5T>fO&44emM+cQRYBmtQqMrbAB!sl)3{O`N_D*`FfHMt1nNt#Ie
zT)_t-OGkCR_SzIHdOWfz5i@helftq6%2An59mV#qm5-G8zlS2ICszfci+w19#dUO)
zJ}-&*%#(&|;Y%7N4Mi$vCb4u*H!XSWyR~p?L>oaV8FAJ+-AlyhbWu5)+n=GWCD1B)
zvu^-mD`{|{1AK$yH_0oBJmh8>;pKcxxO|2S{@^@$o*~nVgz7kFJPjMYG`Zl1ilMFx
zKS#wEc5Uy?uG(jpB&g@ys=U`GTn9<)pp^h-oGLfkT;Dl8+$APi&V(dNI_Z3o4$<=O
zF&&pg%tK~BO;j&cWa?u-5zIxyyp)0uX(W;lWSyn(akhI{e|&3;ASN=cE>R;a`;?
zm5`oE|s)%6yyvSx-Q^zF)T^LZbQBeW9-alEL2)Z+mE2Nu<{Os0;A|e^cIe#f_1%ef$rLQm3&Rb%VA50000 {
@@ -113,19 +115,38 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
@closed=${this._dialogClosed}
.heading=${"Voice Satellite setup"}
hideActions
+ escapeKeyAction
+ scrimClickAction
>
${this._previousSteps.length
? html``
+ : this._step !== STEP.UPDATE
+ ? html``
+ : nothing}
+ ${this._step === STEP.WAKEWORD ||
+ this._step === STEP.AREA ||
+ this._step === STEP.PIPELINE
+ ? html`Skip`
: nothing}
-
+
${this._step === STEP.UPDATE
? html`
- Home Assistant Cloud:
-
-
- ${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
-
- ${this._showFirst
- ? html`
0.2 seconds
`
- : nothing}
- ${this._showFirst
- ? html`
- ${!this._showSecond ? "…" : "Turned on the lights"}
-
`
- : nothing}
- ${this._showSecond
- ? html`
0.4 seconds
`
- : nothing}
-
- Raspberry Pi 4:
+ Raspberry Pi 4
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
@@ -76,8 +59,28 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
? html`
5 seconds
`
: nothing}
+
Home Assistant Cloud
+
+
+ ${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
+
+ ${this._showFirst
+ ? html`
0.2 seconds
`
+ : nothing}
+ ${this._showFirst
+ ? html`
+ ${!this._showSecond ? "…" : "Turned on the lights"}
+
`
+ : nothing}
+ ${this._showSecond
+ ? html`
0.4 seconds
`
+ : nothing}
+
-
`;
}
diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts
index 4a229691b3d7..1f6e8740e2c8 100644
--- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts
+++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts
@@ -25,8 +25,8 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
Change wake word
- When you voice assistant knows where it is, it can better control the
- devices around it.
+ Some wake words are better for [your language] and voice than others.
+ Please try them out.
@@ -72,6 +72,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
ha-md-list {
width: 100%;
text-align: initial;
+ margin-bottom: 24px;
}
`,
];
diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts
index 0d61e8bd7360..0ce0fa9b8608 100644
--- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts
+++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts
@@ -22,7 +22,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
if (
this._status === "success" &&
changedProperties.has("hass") &&
- this.hass.states[this.assistEntityId!]?.state === "listening_wake_word"
+ this.hass.states[this.assistEntityId!]?.state === "idle"
) {
this._nextStep();
}
@@ -38,16 +38,13 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
`
: this._status === "timeout"
? html`
- Error
+ Voice assistant can not connect to Home Assistant
- Your device was unable to reach Home Assistant. Make sure you
- have setup your
- Home Assistant URL's
- correctly.
+ A good explanation what is happening and what action you should
+ take.
`
: html`
@@ -73,10 +70,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
fireEvent(this, "next-step", { noPrevious: true });
}
- private _close() {
- fireEvent(this, "closed");
- }
-
static styles = AssistantSetupStyles;
}
diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts
index 84a69f065e7a..72aec24ca28e 100644
--- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts
+++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts
@@ -10,16 +10,24 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
protected override render() {
return html`
-

-
Home Assistant Cloud
+

+
Supercharge your assistant with Home Assistant Cloud
- With Home Assistant Cloud, you get the best results for your voice
- assistant, sign up for a free trial now.
+ Speed up and take the load off your system by running your
+ text-to-speech and speech-to-text in our private and secure cloud.
+ Cloud also includes secure remote access to your system while
+ supporting the development of Home Assistant.
-
`;
}
@@ -160,6 +180,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
...pipeline,
tts_voice: ev.detail.value,
});
+ }
+
+ private _testTts() {
this._announce("Hello, how can I help you?");
}
@@ -170,8 +193,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
}
- private _changeWakeWord() {
- fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
+ private _testWakeWord() {
+ fireEvent(this, "next-step", {
+ step: STEP.WAKEWORD,
+ nextStep: STEP.SUCCESS,
+ });
}
private async _openPipeline() {
@@ -209,12 +235,28 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
text-align: initial;
}
ha-tts-voice-picker {
- margin-top: 16px;
display: block;
}
.footer {
margin-top: 24px;
}
+ .rows {
+ gap: 16px;
+ display: flex;
+ flex-direction: column;
+ }
+ .row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .row > *:first-child {
+ flex: 1;
+ margin-right: 4px;
+ }
+ .row ha-button {
+ width: 82px;
+ }
`,
];
}
diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts
index 9cff75a4750e..d0ac0f298ac3 100644
--- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts
+++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts
@@ -17,6 +17,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
+ if (!this.updateEntityId) {
+ this._nextStep();
+ return;
+ }
+
if (changedProperties.has("hass") && this.updateEntityId) {
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
if (oldHass) {
@@ -32,16 +37,9 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
}
}
- if (!changedProperties.has("updateEntityId")) {
- return;
- }
-
- if (!this.updateEntityId) {
- this._nextStep();
- return;
+ if (changedProperties.has("updateEntityId")) {
+ this._tryUpdate();
}
-
- this._tryUpdate();
}
protected override render() {
diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts
index 4c9b9306a0ad..4cd90f4ca2fe 100644
--- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts
+++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts
@@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
const entityState = this.hass.states[this.assistEntityId];
- if (entityState.state !== "listening_wake_word") {
+ if (entityState.state !== "idle") {
return html``;
}
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
To make sure the wake word works for you.
`}
-