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`

${description}

` : nothing; @@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement { .disabled=${this.disabled} .computeLabel=${this._computeLabel} .computeHelper=${this._computeHelper} + .localizeValue=${this.localizeValue} > diff --git a/src/components/ha-form/ha-form-grid.ts b/src/components/ha-form/ha-form-grid.ts index 27c602531234..4eefedf4691c 100644 --- a/src/components/ha-form/ha-form-grid.ts +++ b/src/components/ha-form/ha-form-grid.ts @@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement { schema: HaFormSchema ) => string; + @property({ attribute: false }) public localizeValue?: ( + key: string + ) => string; + public async focus() { await this.updateComplete; this.renderRoot.querySelector("ha-form")?.focus(); @@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement { .disabled=${this.disabled} .computeLabel=${this.computeLabel} .computeHelper=${this.computeHelper} + .localizeValue=${this.localizeValue} > ` )} diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index a44067599049..b3e0e21de7ff 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement { localize: this.hass?.localize, computeLabel: this.computeLabel, computeHelper: this.computeHelper, + localizeValue: this.localizeValue, context: this._generateContext(item), ...this.getFormProperties(), })} From e778a9aa1d781146508cace47648b513e86b5e34 Mon Sep 17 00:00:00 2001 From: Erik Montnemery 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)0&#uX(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.

    `}
    -